robotcode-robot 0.68.2__py3-none-any.whl → 0.68.5__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -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()