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,1121 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import itertools
5
+ import os
6
+ from collections import defaultdict
7
+ from dataclasses import dataclass
8
+ from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
9
+
10
+ from robot.parsing.lexer.tokens import Token
11
+ from robot.parsing.model.blocks import Keyword, TestCase
12
+ from robot.parsing.model.statements import (
13
+ Arguments,
14
+ DocumentationOrMetadata,
15
+ Fixture,
16
+ KeywordCall,
17
+ LibraryImport,
18
+ ResourceImport,
19
+ Statement,
20
+ Template,
21
+ TemplateArguments,
22
+ TestCaseName,
23
+ TestTemplate,
24
+ Variable,
25
+ VariablesImport,
26
+ )
27
+ from robot.utils.escaping import split_from_equals, unescape
28
+ from robot.variables.search import contains_variable, search_variable
29
+ from robotcode.core.concurrent import check_current_task_canceled
30
+ from robotcode.core.lsp.types import (
31
+ CodeDescription,
32
+ Diagnostic,
33
+ DiagnosticRelatedInformation,
34
+ DiagnosticSeverity,
35
+ DiagnosticTag,
36
+ Location,
37
+ Position,
38
+ Range,
39
+ )
40
+ from robotcode.core.uri import Uri
41
+
42
+ from ..utils import get_robot_version
43
+ from ..utils.ast import (
44
+ is_not_variable_token,
45
+ range_from_node,
46
+ range_from_node_or_token,
47
+ range_from_token,
48
+ strip_variable_token,
49
+ tokenize_variables,
50
+ )
51
+ from ..utils.visitor import Visitor
52
+ from .entities import (
53
+ ArgumentDefinition,
54
+ CommandLineVariableDefinition,
55
+ EnvironmentVariableDefinition,
56
+ LibraryEntry,
57
+ LocalVariableDefinition,
58
+ VariableDefinition,
59
+ VariableDefinitionType,
60
+ VariableNotFoundDefinition,
61
+ )
62
+ from .errors import DIAGNOSTICS_SOURCE_NAME, Error
63
+ from .library_doc import (
64
+ KeywordDoc,
65
+ is_embedded_keyword,
66
+ )
67
+ from .model_helper import ModelHelper
68
+ from .namespace import KeywordFinder, Namespace
69
+
70
+ if get_robot_version() < (7, 0):
71
+ from robot.variables.search import VariableIterator
72
+ else:
73
+ from robot.variables.search import VariableMatches
74
+
75
+
76
+ @dataclass
77
+ class AnalyzerResult:
78
+ diagnostics: List[Diagnostic]
79
+ keyword_references: Dict[KeywordDoc, Set[Location]]
80
+ variable_references: Dict[VariableDefinition, Set[Location]]
81
+ local_variable_assignments: Dict[VariableDefinition, Set[Range]]
82
+ namespace_references: Dict[LibraryEntry, Set[Location]]
83
+
84
+
85
+ class NamespaceAnalyzer(Visitor, ModelHelper):
86
+ def __init__(
87
+ self,
88
+ model: ast.AST,
89
+ namespace: Namespace,
90
+ finder: KeywordFinder,
91
+ ignored_lines: List[int],
92
+ ) -> None:
93
+ super().__init__()
94
+
95
+ self.model = model
96
+ self.namespace = namespace
97
+ self.finder = finder
98
+ self._ignored_lines = ignored_lines
99
+
100
+ self.current_testcase_or_keyword_name: Optional[str] = None
101
+ self.test_template: Optional[TestTemplate] = None
102
+ self.template: Optional[Template] = None
103
+ self.node_stack: List[ast.AST] = []
104
+ self._diagnostics: List[Diagnostic] = []
105
+ self._keyword_references: Dict[KeywordDoc, Set[Location]] = defaultdict(set)
106
+ self._variable_references: Dict[VariableDefinition, Set[Location]] = defaultdict(set)
107
+ self._local_variable_assignments: Dict[VariableDefinition, Set[Range]] = defaultdict(set)
108
+ self._namespace_references: Dict[LibraryEntry, Set[Location]] = defaultdict(set)
109
+
110
+ def run(self) -> AnalyzerResult:
111
+ self._diagnostics = []
112
+ self._keyword_references = defaultdict(set)
113
+
114
+ self.visit(self.model)
115
+
116
+ return AnalyzerResult(
117
+ self._diagnostics,
118
+ self._keyword_references,
119
+ self._variable_references,
120
+ self._local_variable_assignments,
121
+ self._namespace_references,
122
+ )
123
+
124
+ def yield_argument_name_and_rest(self, node: ast.AST, token: Token) -> Iterator[Token]:
125
+ if isinstance(node, Arguments) and token.type == Token.ARGUMENT:
126
+ argument = next(
127
+ (
128
+ v
129
+ for v in itertools.dropwhile(
130
+ lambda t: t.type in Token.NON_DATA_TOKENS,
131
+ tokenize_variables(token, ignore_errors=True),
132
+ )
133
+ if v.type == Token.VARIABLE
134
+ ),
135
+ None,
136
+ )
137
+ if argument is None or argument.value == token.value:
138
+ yield token
139
+ else:
140
+ yield argument
141
+ i = len(argument.value)
142
+
143
+ for t in self.yield_argument_name_and_rest(
144
+ node,
145
+ Token(
146
+ token.type,
147
+ token.value[i:],
148
+ token.lineno,
149
+ token.col_offset + i,
150
+ token.error,
151
+ ),
152
+ ):
153
+ yield t
154
+ else:
155
+ yield token
156
+
157
+ def visit_Variable(self, node: Variable) -> None: # noqa: N802
158
+ name_token = node.get_token(Token.VARIABLE)
159
+ if name_token is None:
160
+ return
161
+
162
+ name = name_token.value
163
+
164
+ if name is not None:
165
+ match = search_variable(name, ignore_errors=True)
166
+ if not match.is_assign(allow_assign_mark=True):
167
+ return
168
+
169
+ if name.endswith("="):
170
+ name = name[:-1].rstrip()
171
+
172
+ r = range_from_token(
173
+ strip_variable_token(
174
+ Token(
175
+ name_token.type,
176
+ name,
177
+ name_token.lineno,
178
+ name_token.col_offset,
179
+ name_token.error,
180
+ )
181
+ )
182
+ )
183
+
184
+ var_def = next(
185
+ (
186
+ v
187
+ for v in self.namespace.get_own_variables()
188
+ if v.name_token is not None and range_from_token(v.name_token) == r
189
+ ),
190
+ None,
191
+ )
192
+
193
+ if var_def is None:
194
+ return
195
+
196
+ cmd_line_var = self.namespace.find_variable(
197
+ name,
198
+ skip_commandline_variables=False,
199
+ position=r.start,
200
+ ignore_error=True,
201
+ )
202
+ if isinstance(cmd_line_var, CommandLineVariableDefinition):
203
+ if self.namespace.document is not None:
204
+ self._variable_references[cmd_line_var].add(Location(self.namespace.document.document_uri, r))
205
+
206
+ if var_def not in self._variable_references:
207
+ self._variable_references[var_def] = set()
208
+
209
+ def generic_visit(self, node: ast.AST) -> None:
210
+ check_current_task_canceled()
211
+
212
+ super().generic_visit(node)
213
+
214
+ def visit(self, node: ast.AST) -> None:
215
+ check_current_task_canceled()
216
+
217
+ self.node_stack.append(node)
218
+ try:
219
+ severity = (
220
+ DiagnosticSeverity.HINT
221
+ if isinstance(node, (DocumentationOrMetadata, TestCaseName))
222
+ else DiagnosticSeverity.ERROR
223
+ )
224
+
225
+ if isinstance(node, KeywordCall) and node.keyword:
226
+ kw_doc = self.finder.find_keyword(node.keyword, raise_keyword_error=False)
227
+ if kw_doc is not None and kw_doc.longname == "BuiltIn.Comment":
228
+ severity = DiagnosticSeverity.HINT
229
+
230
+ if isinstance(node, Statement) and not isinstance(node, (TestTemplate, Template)):
231
+ for token1 in (
232
+ t
233
+ for t in node.tokens
234
+ if not (isinstance(node, Variable) and t.type == Token.VARIABLE)
235
+ and t.error is None
236
+ and contains_variable(t.value, "$@&%")
237
+ ):
238
+ for token in self.yield_argument_name_and_rest(node, token1):
239
+ if isinstance(node, Arguments) and token.value == "@{}":
240
+ continue
241
+
242
+ for var_token, var in self.iter_variables_from_token(
243
+ token,
244
+ self.namespace,
245
+ self.node_stack,
246
+ range_from_token(token).start,
247
+ skip_commandline_variables=False,
248
+ return_not_found=True,
249
+ ):
250
+ if isinstance(var, VariableNotFoundDefinition):
251
+ self.append_diagnostics(
252
+ range=range_from_token(var_token),
253
+ message=f"Variable '{var.name}' not found.",
254
+ severity=severity,
255
+ source=DIAGNOSTICS_SOURCE_NAME,
256
+ code=Error.VARIABLE_NOT_FOUND,
257
+ )
258
+ else:
259
+ if isinstance(var, EnvironmentVariableDefinition) and var.default_value is None:
260
+ env_name = var.name[2:-1]
261
+ if os.environ.get(env_name, None) is None:
262
+ self.append_diagnostics(
263
+ range=range_from_token(var_token),
264
+ message=f"Environment variable '{var.name}' not found.",
265
+ severity=severity,
266
+ source=DIAGNOSTICS_SOURCE_NAME,
267
+ code=Error.ENVIROMMENT_VARIABLE_NOT_FOUND,
268
+ )
269
+
270
+ if self.namespace.document is not None:
271
+ if isinstance(var, EnvironmentVariableDefinition):
272
+ (
273
+ var_token.value,
274
+ _,
275
+ _,
276
+ ) = var_token.value.partition("=")
277
+
278
+ var_range = range_from_token(var_token)
279
+
280
+ suite_var = None
281
+ if isinstance(var, CommandLineVariableDefinition):
282
+ suite_var = self.namespace.find_variable(
283
+ var.name,
284
+ skip_commandline_variables=True,
285
+ ignore_error=True,
286
+ )
287
+ if suite_var is not None and suite_var.type != VariableDefinitionType.VARIABLE:
288
+ suite_var = None
289
+
290
+ if var.name_range != var_range:
291
+ self._variable_references[var].add(
292
+ Location(
293
+ self.namespace.document.document_uri,
294
+ var_range,
295
+ )
296
+ )
297
+ if suite_var is not None:
298
+ self._variable_references[suite_var].add(
299
+ Location(
300
+ self.namespace.document.document_uri,
301
+ var_range,
302
+ )
303
+ )
304
+ if token1.type == Token.ASSIGN and isinstance(
305
+ var,
306
+ (
307
+ LocalVariableDefinition,
308
+ ArgumentDefinition,
309
+ ),
310
+ ):
311
+ self._local_variable_assignments[var].add(var_range)
312
+
313
+ elif var not in self._variable_references and token1.type in [
314
+ Token.ASSIGN,
315
+ Token.ARGUMENT,
316
+ Token.VARIABLE,
317
+ ]:
318
+ self._variable_references[var] = set()
319
+ if suite_var is not None:
320
+ self._variable_references[suite_var] = set()
321
+
322
+ if (
323
+ isinstance(node, Statement)
324
+ and isinstance(node, self.get_expression_statement_types())
325
+ and (token := node.get_token(Token.ARGUMENT)) is not None
326
+ ):
327
+ for var_token, var in self.iter_expression_variables_from_token(
328
+ token,
329
+ self.namespace,
330
+ self.node_stack,
331
+ range_from_token(token).start,
332
+ skip_commandline_variables=False,
333
+ return_not_found=True,
334
+ ):
335
+ if isinstance(var, VariableNotFoundDefinition):
336
+ self.append_diagnostics(
337
+ range=range_from_token(var_token),
338
+ message=f"Variable '{var.name}' not found.",
339
+ severity=DiagnosticSeverity.ERROR,
340
+ source=DIAGNOSTICS_SOURCE_NAME,
341
+ code=Error.VARIABLE_NOT_FOUND,
342
+ )
343
+ else:
344
+ if self.namespace.document is not None:
345
+ var_range = range_from_token(var_token)
346
+
347
+ if var.name_range != var_range:
348
+ self._variable_references[var].add(
349
+ Location(
350
+ self.namespace.document.document_uri,
351
+ range_from_token(var_token),
352
+ )
353
+ )
354
+
355
+ if isinstance(var, CommandLineVariableDefinition):
356
+ suite_var = self.namespace.find_variable(
357
+ var.name,
358
+ skip_commandline_variables=True,
359
+ ignore_error=True,
360
+ )
361
+ if suite_var is not None and suite_var.type == VariableDefinitionType.VARIABLE:
362
+ self._variable_references[suite_var].add(
363
+ Location(
364
+ self.namespace.document.document_uri,
365
+ range_from_token(var_token),
366
+ )
367
+ )
368
+
369
+ super().visit(node)
370
+ finally:
371
+ self.node_stack = self.node_stack[:-1]
372
+
373
+ def _should_ignore(self, range: Range) -> bool:
374
+ import builtins
375
+
376
+ for line_no in builtins.range(range.start.line, range.end.line + 1):
377
+ if line_no in self._ignored_lines:
378
+ return True
379
+
380
+ return False
381
+
382
+ def append_diagnostics(
383
+ self,
384
+ range: Range,
385
+ message: str,
386
+ severity: Optional[DiagnosticSeverity] = None,
387
+ code: Union[int, str, None] = None,
388
+ code_description: Optional[CodeDescription] = None,
389
+ source: Optional[str] = None,
390
+ tags: Optional[List[DiagnosticTag]] = None,
391
+ related_information: Optional[List[DiagnosticRelatedInformation]] = None,
392
+ data: Optional[Any] = None,
393
+ ) -> None:
394
+ if self._should_ignore(range):
395
+ return
396
+
397
+ self._diagnostics.append(
398
+ Diagnostic(
399
+ range,
400
+ message,
401
+ severity,
402
+ code,
403
+ code_description,
404
+ source or DIAGNOSTICS_SOURCE_NAME,
405
+ tags,
406
+ related_information,
407
+ data,
408
+ )
409
+ )
410
+
411
+ def _analyze_keyword_call(
412
+ self,
413
+ keyword: Optional[str],
414
+ node: ast.AST,
415
+ keyword_token: Token,
416
+ argument_tokens: List[Token],
417
+ analyse_run_keywords: bool = True,
418
+ allow_variables: bool = False,
419
+ ignore_errors_if_contains_variables: bool = False,
420
+ ) -> Optional[KeywordDoc]:
421
+ result: Optional[KeywordDoc] = None
422
+
423
+ try:
424
+ if not allow_variables and not is_not_variable_token(keyword_token):
425
+ return None
426
+
427
+ if (
428
+ self.finder.find_keyword(
429
+ keyword_token.value,
430
+ raise_keyword_error=False,
431
+ handle_bdd_style=False,
432
+ )
433
+ is None
434
+ ):
435
+ keyword_token = self.strip_bdd_prefix(self.namespace, keyword_token)
436
+
437
+ kw_range = range_from_token(keyword_token)
438
+
439
+ lib_entry = None
440
+ lib_range = None
441
+ kw_namespace = None
442
+
443
+ result = self.finder.find_keyword(keyword, raise_keyword_error=False)
444
+
445
+ if keyword is not None:
446
+ (
447
+ lib_entry,
448
+ kw_namespace,
449
+ ) = self.get_namespace_info_from_keyword_token(self.namespace, keyword_token)
450
+
451
+ if lib_entry and kw_namespace:
452
+ r = range_from_token(keyword_token)
453
+ lib_range = r
454
+ r.end.character = r.start.character + len(kw_namespace)
455
+ kw_range.start.character = r.end.character + 1
456
+ lib_range.end.character = kw_range.start.character - 1
457
+
458
+ if (
459
+ result is not None
460
+ and lib_entry is not None
461
+ and kw_namespace
462
+ and result.parent is not None
463
+ and result.parent != lib_entry.library_doc
464
+ ):
465
+ lib_entry = None
466
+ kw_range = range_from_token(keyword_token)
467
+
468
+ if kw_namespace and lib_entry is not None and lib_range is not None:
469
+ if self.namespace.document is not None:
470
+ entries = [lib_entry]
471
+ if self.finder.multiple_keywords_result is not None:
472
+ entries = next(
473
+ (v for k, v in (self.namespace.get_namespaces()).items() if k == kw_namespace),
474
+ entries,
475
+ )
476
+ for entry in entries:
477
+ self._namespace_references[entry].add(Location(self.namespace.document.document_uri, lib_range))
478
+
479
+ if not ignore_errors_if_contains_variables or is_not_variable_token(keyword_token):
480
+ for e in self.finder.diagnostics:
481
+ self.append_diagnostics(
482
+ range=kw_range,
483
+ message=e.message,
484
+ severity=e.severity,
485
+ code=e.code,
486
+ )
487
+
488
+ if result is None:
489
+ if self.namespace.document is not None and self.finder.multiple_keywords_result is not None:
490
+ for d in self.finder.multiple_keywords_result:
491
+ self._keyword_references[d].add(Location(self.namespace.document.document_uri, kw_range))
492
+ else:
493
+ if self.namespace.document is not None:
494
+ self._keyword_references[result].add(Location(self.namespace.document.document_uri, kw_range))
495
+
496
+ if result.errors:
497
+ self.append_diagnostics(
498
+ range=kw_range,
499
+ message="Keyword definition contains errors.",
500
+ severity=DiagnosticSeverity.ERROR,
501
+ related_information=[
502
+ DiagnosticRelatedInformation(
503
+ location=Location(
504
+ uri=str(
505
+ Uri.from_path(
506
+ err.source
507
+ if err.source is not None
508
+ else result.source
509
+ if result.source is not None
510
+ else "/<unknown>"
511
+ )
512
+ ),
513
+ range=Range(
514
+ start=Position(
515
+ line=err.line_no - 1 if err.line_no is not None else max(result.line_no, 0),
516
+ character=0,
517
+ ),
518
+ end=Position(
519
+ line=err.line_no - 1 if err.line_no is not None else max(result.line_no, 0),
520
+ character=0,
521
+ ),
522
+ ),
523
+ ),
524
+ message=err.message,
525
+ )
526
+ for err in result.errors
527
+ ],
528
+ )
529
+
530
+ if result.is_deprecated:
531
+ self.append_diagnostics(
532
+ range=kw_range,
533
+ message=f"Keyword '{result.name}' is deprecated"
534
+ f"{f': {result.deprecated_message}' if result.deprecated_message else ''}.",
535
+ severity=DiagnosticSeverity.HINT,
536
+ tags=[DiagnosticTag.DEPRECATED],
537
+ code=Error.DEPRECATED_KEYWORD,
538
+ )
539
+ if result.is_error_handler:
540
+ self.append_diagnostics(
541
+ range=kw_range,
542
+ message=f"Keyword definition contains errors: {result.error_handler_message}",
543
+ severity=DiagnosticSeverity.ERROR,
544
+ code=Error.KEYWORD_CONTAINS_ERRORS,
545
+ )
546
+ if result.is_reserved():
547
+ self.append_diagnostics(
548
+ range=kw_range,
549
+ message=f"'{result.name}' is a reserved keyword.",
550
+ severity=DiagnosticSeverity.ERROR,
551
+ code=Error.RESERVED_KEYWORD,
552
+ )
553
+
554
+ if get_robot_version() >= (6, 0) and result.is_resource_keyword and result.is_private():
555
+ if self.namespace.source != result.source:
556
+ self.append_diagnostics(
557
+ range=kw_range,
558
+ message=f"Keyword '{result.longname}' is private and should only be called by"
559
+ f" keywords in the same file.",
560
+ severity=DiagnosticSeverity.WARNING,
561
+ code=Error.PRIVATE_KEYWORD,
562
+ )
563
+
564
+ if not isinstance(node, (Template, TestTemplate)):
565
+ try:
566
+ if result.arguments_spec is not None:
567
+ result.arguments_spec.resolve(
568
+ [v.value for v in argument_tokens],
569
+ None,
570
+ resolve_variables_until=result.args_to_process,
571
+ resolve_named=not result.is_any_run_keyword(),
572
+ )
573
+ except (SystemExit, KeyboardInterrupt):
574
+ raise
575
+ except BaseException as e:
576
+ self.append_diagnostics(
577
+ range=Range(
578
+ start=kw_range.start,
579
+ end=range_from_token(argument_tokens[-1]).end if argument_tokens else kw_range.end,
580
+ ),
581
+ message=str(e),
582
+ severity=DiagnosticSeverity.ERROR,
583
+ code=type(e).__qualname__,
584
+ )
585
+
586
+ except (SystemExit, KeyboardInterrupt):
587
+ raise
588
+ except BaseException as e:
589
+ self.append_diagnostics(
590
+ range=range_from_node_or_token(node, keyword_token),
591
+ message=str(e),
592
+ severity=DiagnosticSeverity.ERROR,
593
+ code=type(e).__qualname__,
594
+ )
595
+
596
+ if self.namespace.document is not None and result is not None:
597
+ if result.longname in [
598
+ "BuiltIn.Evaluate",
599
+ "BuiltIn.Should Be True",
600
+ "BuiltIn.Should Not Be True",
601
+ "BuiltIn.Skip If",
602
+ "BuiltIn.Continue For Loop If",
603
+ "BuiltIn.Exit For Loop If",
604
+ "BuiltIn.Return From Keyword If",
605
+ "BuiltIn.Run Keyword And Return If",
606
+ "BuiltIn.Pass Execution If",
607
+ "BuiltIn.Run Keyword If",
608
+ "BuiltIn.Run Keyword Unless",
609
+ ]:
610
+ tokens = argument_tokens
611
+ if tokens and (token := tokens[0]):
612
+ for (
613
+ var_token,
614
+ var,
615
+ ) in self.iter_expression_variables_from_token(
616
+ token,
617
+ self.namespace,
618
+ self.node_stack,
619
+ range_from_token(token).start,
620
+ skip_commandline_variables=False,
621
+ return_not_found=True,
622
+ ):
623
+ if isinstance(var, VariableNotFoundDefinition):
624
+ self.append_diagnostics(
625
+ range=range_from_token(var_token),
626
+ message=f"Variable '{var.name}' not found.",
627
+ severity=DiagnosticSeverity.ERROR,
628
+ code=Error.VARIABLE_NOT_FOUND,
629
+ )
630
+ else:
631
+ if self.namespace.document is not None:
632
+ self._variable_references[var].add(
633
+ Location(
634
+ self.namespace.document.document_uri,
635
+ range_from_token(var_token),
636
+ )
637
+ )
638
+
639
+ if isinstance(var, CommandLineVariableDefinition):
640
+ suite_var = self.namespace.find_variable(
641
+ var.name,
642
+ skip_commandline_variables=True,
643
+ ignore_error=True,
644
+ )
645
+ if suite_var is not None and suite_var.type == VariableDefinitionType.VARIABLE:
646
+ self._variable_references[suite_var].add(
647
+ Location(
648
+ self.namespace.document.document_uri,
649
+ range_from_token(var_token),
650
+ )
651
+ )
652
+ if result.argument_definitions:
653
+ for arg in argument_tokens:
654
+ name, value = split_from_equals(arg.value)
655
+ if value is not None and name:
656
+ arg_def = next(
657
+ (e for e in result.argument_definitions if e.name[2:-1] == name),
658
+ None,
659
+ )
660
+ if arg_def is not None:
661
+ name_token = Token(Token.ARGUMENT, name, arg.lineno, arg.col_offset)
662
+ self._variable_references[arg_def].add(
663
+ Location(
664
+ self.namespace.document.document_uri,
665
+ range_from_token(name_token),
666
+ )
667
+ )
668
+
669
+ if result is not None and analyse_run_keywords:
670
+ self._analyse_run_keyword(result, node, argument_tokens)
671
+
672
+ return result
673
+
674
+ def _analyse_run_keyword(
675
+ self,
676
+ keyword_doc: Optional[KeywordDoc],
677
+ node: ast.AST,
678
+ argument_tokens: List[Token],
679
+ ) -> List[Token]:
680
+ if keyword_doc is None or not keyword_doc.is_any_run_keyword():
681
+ return argument_tokens
682
+
683
+ if keyword_doc.is_run_keyword() and len(argument_tokens) > 0:
684
+ self._analyze_keyword_call(
685
+ unescape(argument_tokens[0].value),
686
+ node,
687
+ argument_tokens[0],
688
+ argument_tokens[1:],
689
+ allow_variables=True,
690
+ ignore_errors_if_contains_variables=True,
691
+ )
692
+
693
+ return argument_tokens[1:]
694
+
695
+ if keyword_doc.is_run_keyword_with_condition() and len(argument_tokens) > (
696
+ cond_count := keyword_doc.run_keyword_condition_count()
697
+ ):
698
+ self._analyze_keyword_call(
699
+ unescape(argument_tokens[cond_count].value),
700
+ node,
701
+ argument_tokens[cond_count],
702
+ argument_tokens[cond_count + 1 :],
703
+ allow_variables=True,
704
+ ignore_errors_if_contains_variables=True,
705
+ )
706
+ return argument_tokens[cond_count + 1 :]
707
+
708
+ if keyword_doc.is_run_keywords():
709
+ has_and = False
710
+ while argument_tokens:
711
+ t = argument_tokens[0]
712
+ argument_tokens = argument_tokens[1:]
713
+ if t.value == "AND":
714
+ self.append_diagnostics(
715
+ range=range_from_token(t),
716
+ message=f"Incorrect use of {t.value}.",
717
+ severity=DiagnosticSeverity.ERROR,
718
+ code=Error.INCORRECT_USE,
719
+ )
720
+ continue
721
+
722
+ and_token = next((e for e in argument_tokens if e.value == "AND"), None)
723
+ args = []
724
+ if and_token is not None:
725
+ args = argument_tokens[: argument_tokens.index(and_token)]
726
+ argument_tokens = argument_tokens[argument_tokens.index(and_token) + 1 :]
727
+ has_and = True
728
+ elif has_and:
729
+ args = argument_tokens
730
+ argument_tokens = []
731
+
732
+ self._analyze_keyword_call(
733
+ unescape(t.value),
734
+ node,
735
+ t,
736
+ args,
737
+ allow_variables=True,
738
+ ignore_errors_if_contains_variables=True,
739
+ )
740
+
741
+ return []
742
+
743
+ if keyword_doc.is_run_keyword_if() and len(argument_tokens) > 1:
744
+
745
+ def skip_args() -> List[Token]:
746
+ nonlocal argument_tokens
747
+ result = []
748
+ while argument_tokens:
749
+ if argument_tokens[0].value in ["ELSE", "ELSE IF"]:
750
+ break
751
+ if argument_tokens:
752
+ result.append(argument_tokens[0])
753
+ argument_tokens = argument_tokens[1:]
754
+
755
+ return result
756
+
757
+ result = self.finder.find_keyword(argument_tokens[1].value)
758
+
759
+ if result is not None and result.is_any_run_keyword():
760
+ argument_tokens = argument_tokens[2:]
761
+
762
+ argument_tokens = self._analyse_run_keyword(result, node, argument_tokens)
763
+ else:
764
+ kwt = argument_tokens[1]
765
+ argument_tokens = argument_tokens[2:]
766
+
767
+ args = skip_args()
768
+
769
+ self._analyze_keyword_call(
770
+ unescape(kwt.value),
771
+ node,
772
+ kwt,
773
+ args,
774
+ analyse_run_keywords=False,
775
+ allow_variables=True,
776
+ ignore_errors_if_contains_variables=True,
777
+ )
778
+
779
+ while argument_tokens:
780
+ if argument_tokens[0].value == "ELSE" and len(argument_tokens) > 1:
781
+ kwt = argument_tokens[1]
782
+ argument_tokens = argument_tokens[2:]
783
+
784
+ args = skip_args()
785
+
786
+ result = self._analyze_keyword_call(
787
+ unescape(kwt.value),
788
+ node,
789
+ kwt,
790
+ args,
791
+ analyse_run_keywords=False,
792
+ )
793
+
794
+ if result is not None and result.is_any_run_keyword():
795
+ argument_tokens = self._analyse_run_keyword(result, node, argument_tokens)
796
+
797
+ break
798
+
799
+ if argument_tokens[0].value == "ELSE IF" and len(argument_tokens) > 2:
800
+ kwt = argument_tokens[2]
801
+ argument_tokens = argument_tokens[3:]
802
+
803
+ args = skip_args()
804
+
805
+ result = self._analyze_keyword_call(
806
+ unescape(kwt.value),
807
+ node,
808
+ kwt,
809
+ args,
810
+ analyse_run_keywords=False,
811
+ )
812
+
813
+ if result is not None and result.is_any_run_keyword():
814
+ argument_tokens = self._analyse_run_keyword(result, node, argument_tokens)
815
+ else:
816
+ break
817
+
818
+ return argument_tokens
819
+
820
+ def visit_Fixture(self, node: Fixture) -> None: # noqa: N802
821
+ keyword_token = node.get_token(Token.NAME)
822
+
823
+ # TODO: calculate possible variables in NAME
824
+
825
+ if (
826
+ keyword_token is not None
827
+ and keyword_token.value is not None
828
+ and keyword_token.value.upper() not in ("", "NONE")
829
+ ):
830
+ self._analyze_keyword_call(
831
+ node.name,
832
+ node,
833
+ keyword_token,
834
+ [e for e in node.get_tokens(Token.ARGUMENT)],
835
+ allow_variables=True,
836
+ ignore_errors_if_contains_variables=True,
837
+ )
838
+
839
+ self.generic_visit(node)
840
+
841
+ def visit_TestTemplate(self, node: TestTemplate) -> None: # noqa: N802
842
+ keyword_token = node.get_token(Token.NAME)
843
+
844
+ if keyword_token is not None and keyword_token.value.upper() not in (
845
+ "",
846
+ "NONE",
847
+ ):
848
+ self._analyze_keyword_call(
849
+ node.value,
850
+ node,
851
+ keyword_token,
852
+ [],
853
+ analyse_run_keywords=False,
854
+ allow_variables=True,
855
+ )
856
+
857
+ self.test_template = node
858
+ self.generic_visit(node)
859
+
860
+ def visit_Template(self, node: Template) -> None: # noqa: N802
861
+ keyword_token = node.get_token(Token.NAME)
862
+
863
+ if keyword_token is not None and keyword_token.value.upper() not in (
864
+ "",
865
+ "NONE",
866
+ ):
867
+ self._analyze_keyword_call(
868
+ node.value,
869
+ node,
870
+ keyword_token,
871
+ [],
872
+ analyse_run_keywords=False,
873
+ allow_variables=True,
874
+ )
875
+ self.template = node
876
+ self.generic_visit(node)
877
+
878
+ def visit_KeywordCall(self, node: KeywordCall) -> None: # noqa: N802
879
+ keyword_token = node.get_token(Token.KEYWORD)
880
+
881
+ if node.assign and keyword_token is None:
882
+ self.append_diagnostics(
883
+ range=range_from_node_or_token(node, node.get_token(Token.ASSIGN)),
884
+ message="Keyword name cannot be empty.",
885
+ severity=DiagnosticSeverity.ERROR,
886
+ code=Error.KEYWORD_NAME_EMPTY,
887
+ )
888
+ else:
889
+ self._analyze_keyword_call(
890
+ node.keyword,
891
+ node,
892
+ keyword_token,
893
+ [e for e in node.get_tokens(Token.ARGUMENT)],
894
+ )
895
+
896
+ if not self.current_testcase_or_keyword_name:
897
+ self.append_diagnostics(
898
+ range=range_from_node_or_token(node, node.get_token(Token.ASSIGN)),
899
+ message="Code is unreachable.",
900
+ severity=DiagnosticSeverity.HINT,
901
+ tags=[DiagnosticTag.UNNECESSARY],
902
+ code=Error.CODE_UNREACHABLE,
903
+ )
904
+
905
+ self.generic_visit(node)
906
+
907
+ def visit_TestCase(self, node: TestCase) -> None: # noqa: N802
908
+ if not node.name:
909
+ name_token = node.header.get_token(Token.TESTCASE_NAME)
910
+ self.append_diagnostics(
911
+ range=range_from_node_or_token(node, name_token),
912
+ message="Test case name cannot be empty.",
913
+ severity=DiagnosticSeverity.ERROR,
914
+ code=Error.TESTCASE_NAME_EMPTY,
915
+ )
916
+
917
+ self.current_testcase_or_keyword_name = node.name
918
+ try:
919
+ self.generic_visit(node)
920
+ finally:
921
+ self.current_testcase_or_keyword_name = None
922
+ self.template = None
923
+
924
+ def visit_Keyword(self, node: Keyword) -> None: # noqa: N802
925
+ if node.name:
926
+ name_token = node.header.get_token(Token.KEYWORD_NAME)
927
+ kw_doc = self.get_keyword_definition_at_token(self.namespace.get_library_doc(), name_token)
928
+
929
+ if kw_doc is not None and kw_doc not in self._keyword_references:
930
+ self._keyword_references[kw_doc] = set()
931
+
932
+ if (
933
+ get_robot_version() < (6, 1)
934
+ and is_embedded_keyword(node.name)
935
+ and any(isinstance(v, Arguments) and len(v.values) > 0 for v in node.body)
936
+ ):
937
+ self.append_diagnostics(
938
+ range=range_from_node_or_token(node, name_token),
939
+ message="Keyword cannot have both normal and embedded arguments.",
940
+ severity=DiagnosticSeverity.ERROR,
941
+ code=Error.KEYWORD_CONTAINS_NORMAL_AND_EMBBEDED_ARGUMENTS,
942
+ )
943
+ else:
944
+ name_token = node.header.get_token(Token.KEYWORD_NAME)
945
+ self.append_diagnostics(
946
+ range=range_from_node_or_token(node, name_token),
947
+ message="Keyword name cannot be empty.",
948
+ severity=DiagnosticSeverity.ERROR,
949
+ code=Error.KEYWORD_NAME_EMPTY,
950
+ )
951
+
952
+ self.current_testcase_or_keyword_name = node.name
953
+ try:
954
+ self.generic_visit(node)
955
+ finally:
956
+ self.current_testcase_or_keyword_name = None
957
+
958
+ def _format_template(self, template: str, arguments: Tuple[str, ...]) -> Tuple[str, Tuple[str, ...]]:
959
+ if get_robot_version() < (7, 0):
960
+ variables = VariableIterator(template, identifiers="$")
961
+ count = len(variables)
962
+ if count == 0 or count != len(arguments):
963
+ return template, arguments
964
+ temp = []
965
+ for (before, _, after), arg in zip(variables, arguments):
966
+ temp.extend([before, arg])
967
+ temp.append(after)
968
+ return "".join(temp), ()
969
+
970
+ variables = VariableMatches(template, identifiers="$")
971
+ count = len(variables)
972
+ if count == 0 or count != len(arguments):
973
+ return template, arguments
974
+ temp = []
975
+ for var, arg in zip(variables, arguments):
976
+ temp.extend([var.before, arg])
977
+ temp.append(var.after)
978
+ return "".join(temp), ()
979
+
980
+ def visit_TemplateArguments(self, node: TemplateArguments) -> None: # noqa: N802
981
+ template = self.template or self.test_template
982
+ if template is not None and template.value is not None and template.value.upper() not in ("", "NONE"):
983
+ argument_tokens = node.get_tokens(Token.ARGUMENT)
984
+ args = tuple(t.value for t in argument_tokens)
985
+ keyword = template.value
986
+ keyword, args = self._format_template(keyword, args)
987
+
988
+ result = self.finder.find_keyword(keyword)
989
+ if result is not None:
990
+ try:
991
+ if result.arguments_spec is not None:
992
+ result.arguments_spec.resolve(
993
+ args,
994
+ None,
995
+ resolve_variables_until=result.args_to_process,
996
+ resolve_named=not result.is_any_run_keyword(),
997
+ )
998
+ except (SystemExit, KeyboardInterrupt):
999
+ raise
1000
+ except BaseException as e:
1001
+ self.append_diagnostics(
1002
+ range=range_from_node(node, skip_non_data=True),
1003
+ message=str(e),
1004
+ severity=DiagnosticSeverity.ERROR,
1005
+ code=type(e).__qualname__,
1006
+ )
1007
+
1008
+ for d in self.finder.diagnostics:
1009
+ self.append_diagnostics(
1010
+ range=range_from_node(node, skip_non_data=True),
1011
+ message=d.message,
1012
+ severity=d.severity,
1013
+ code=d.code,
1014
+ )
1015
+
1016
+ self.generic_visit(node)
1017
+
1018
+ def visit_ForceTags(self, node: Statement) -> None: # noqa: N802
1019
+ if get_robot_version() >= (6, 0):
1020
+ tag = node.get_token(Token.FORCE_TAGS)
1021
+ if tag is not None and tag.value.upper() == "FORCE TAGS":
1022
+ self.append_diagnostics(
1023
+ range=range_from_node_or_token(node, tag),
1024
+ message="`Force Tags` is deprecated in favour of new `Test Tags` setting.",
1025
+ severity=DiagnosticSeverity.INFORMATION,
1026
+ tags=[DiagnosticTag.DEPRECATED],
1027
+ code=Error.DEPRECATED_FORCE_TAG,
1028
+ )
1029
+
1030
+ def visit_TestTags(self, node: Statement) -> None: # noqa: N802
1031
+ if get_robot_version() >= (6, 0):
1032
+ tag = node.get_token(Token.FORCE_TAGS)
1033
+ if tag is not None and tag.value.upper() == "FORCE TAGS":
1034
+ self.append_diagnostics(
1035
+ range=range_from_node_or_token(node, tag),
1036
+ message="`Force Tags` is deprecated in favour of new `Test Tags` setting.",
1037
+ severity=DiagnosticSeverity.INFORMATION,
1038
+ tags=[DiagnosticTag.DEPRECATED],
1039
+ code=Error.DEPRECATED_FORCE_TAG,
1040
+ )
1041
+
1042
+ def visit_Tags(self, node: Statement) -> None: # noqa: N802
1043
+ if get_robot_version() >= (6, 0):
1044
+ for tag in node.get_tokens(Token.ARGUMENT):
1045
+ if tag.value and tag.value.startswith("-"):
1046
+ self.append_diagnostics(
1047
+ range=range_from_node_or_token(node, tag),
1048
+ message=f"Settings tags starting with a hyphen using the '[Tags]' setting "
1049
+ f"is deprecated. In Robot Framework 7.0 this syntax will be used "
1050
+ f"for removing tags. Escape '{tag.value}' like '\\{tag.value}' to use the "
1051
+ f"literal value and to avoid this warning.",
1052
+ severity=DiagnosticSeverity.WARNING,
1053
+ tags=[DiagnosticTag.DEPRECATED],
1054
+ code=Error.DEPRECATED_HYPHEN_TAG,
1055
+ )
1056
+
1057
+ def visit_ReturnSetting(self, node: Statement) -> None: # noqa: N802
1058
+ if get_robot_version() >= (7, 0):
1059
+ token = node.get_token(Token.RETURN_SETTING)
1060
+ if token is not None and token.error:
1061
+ self.append_diagnostics(
1062
+ range=range_from_node_or_token(node, token),
1063
+ message=token.error,
1064
+ severity=DiagnosticSeverity.WARNING,
1065
+ tags=[DiagnosticTag.DEPRECATED],
1066
+ code=Error.DEPRECATED_RETURN_SETTING,
1067
+ )
1068
+
1069
+ def _check_import_name(self, value: Optional[str], node: ast.AST, type: str) -> None:
1070
+ if not value:
1071
+ self.append_diagnostics(
1072
+ range=range_from_node(node),
1073
+ message=f"{type} setting requires value.",
1074
+ severity=DiagnosticSeverity.ERROR,
1075
+ code=Error.IMPORT_REQUIRES_VALUE,
1076
+ )
1077
+
1078
+ def visit_VariablesImport(self, node: VariablesImport) -> None: # noqa: N802
1079
+ if get_robot_version() >= (6, 1):
1080
+ self._check_import_name(node.name, node, "Variables")
1081
+
1082
+ name_token = node.get_token(Token.NAME)
1083
+ if name_token is None:
1084
+ return
1085
+
1086
+ entries = self.namespace.get_import_entries()
1087
+ if entries and self.namespace.document:
1088
+ for v in entries.values():
1089
+ if v.import_source == self.namespace.source and v.import_range == range_from_token(name_token):
1090
+ if v not in self._namespace_references:
1091
+ self._namespace_references[v] = set()
1092
+
1093
+ def visit_ResourceImport(self, node: ResourceImport) -> None: # noqa: N802
1094
+ if get_robot_version() >= (6, 1):
1095
+ self._check_import_name(node.name, node, "Resource")
1096
+
1097
+ name_token = node.get_token(Token.NAME)
1098
+ if name_token is None:
1099
+ return
1100
+
1101
+ entries = self.namespace.get_import_entries()
1102
+ if entries and self.namespace.document:
1103
+ for v in entries.values():
1104
+ if v.import_source == self.namespace.source and v.import_range == range_from_token(name_token):
1105
+ if v not in self._namespace_references:
1106
+ self._namespace_references[v] = set()
1107
+
1108
+ def visit_LibraryImport(self, node: LibraryImport) -> None: # noqa: N802
1109
+ if get_robot_version() >= (6, 1):
1110
+ self._check_import_name(node.name, node, "Library")
1111
+
1112
+ name_token = node.get_token(Token.NAME)
1113
+ if name_token is None:
1114
+ return
1115
+
1116
+ entries = self.namespace.get_import_entries()
1117
+ if entries and self.namespace.document:
1118
+ for v in entries.values():
1119
+ if v.import_source == self.namespace.source and v.import_range == range_from_token(name_token):
1120
+ if v not in self._namespace_references:
1121
+ self._namespace_references[v] = set()