robotcode-robot 0.93.1__py3-none-any.whl → 0.94.0__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.
- robotcode/robot/__version__.py +1 -1
- robotcode/robot/diagnostics/document_cache_helper.py +11 -6
- robotcode/robot/diagnostics/entities.py +2 -2
- robotcode/robot/diagnostics/errors.py +6 -1
- robotcode/robot/diagnostics/imports_manager.py +77 -37
- robotcode/robot/diagnostics/keyword_finder.py +458 -0
- robotcode/robot/diagnostics/library_doc.py +20 -14
- robotcode/robot/diagnostics/model_helper.py +42 -27
- robotcode/robot/diagnostics/namespace.py +142 -552
- robotcode/robot/diagnostics/namespace_analyzer.py +952 -462
- robotcode/robot/utils/markdownformatter.py +8 -11
- robotcode/robot/utils/visitor.py +7 -13
- {robotcode_robot-0.93.1.dist-info → robotcode_robot-0.94.0.dist-info}/METADATA +2 -2
- robotcode_robot-0.94.0.dist-info/RECORD +31 -0
- robotcode_robot-0.93.1.dist-info/RECORD +0 -30
- {robotcode_robot-0.93.1.dist-info → robotcode_robot-0.94.0.dist-info}/WHEEL +0 -0
- {robotcode_robot-0.93.1.dist-info → robotcode_robot-0.94.0.dist-info}/licenses/LICENSE.txt +0 -0
@@ -1,25 +1,26 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
import ast
|
4
2
|
import itertools
|
5
3
|
import os
|
4
|
+
import token as python_token
|
6
5
|
from collections import defaultdict
|
6
|
+
from concurrent.futures import CancelledError
|
7
7
|
from dataclasses import dataclass
|
8
|
-
from
|
8
|
+
from io import StringIO
|
9
|
+
from pathlib import Path
|
10
|
+
from tokenize import TokenError, generate_tokens
|
11
|
+
from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
|
9
12
|
|
10
|
-
|
13
|
+
from robot.errors import VariableError
|
11
14
|
from robot.parsing.lexer.tokens import Token
|
12
|
-
from robot.parsing.model.blocks import Keyword, TestCase
|
15
|
+
from robot.parsing.model.blocks import File, Keyword, TestCase, VariableSection
|
13
16
|
from robot.parsing.model.statements import (
|
14
17
|
Arguments,
|
15
|
-
DefaultTags,
|
16
|
-
DocumentationOrMetadata,
|
17
18
|
Fixture,
|
18
19
|
KeywordCall,
|
20
|
+
KeywordName,
|
19
21
|
LibraryImport,
|
20
22
|
ResourceImport,
|
21
23
|
Statement,
|
22
|
-
Tags,
|
23
24
|
Template,
|
24
25
|
TemplateArguments,
|
25
26
|
TestCaseName,
|
@@ -28,7 +29,8 @@ from robot.parsing.model.statements import (
|
|
28
29
|
VariablesImport,
|
29
30
|
)
|
30
31
|
from robot.utils.escaping import split_from_equals, unescape
|
31
|
-
from robot.variables.
|
32
|
+
from robot.variables.finders import NOT_FOUND, NumberFinder
|
33
|
+
from robot.variables.search import contains_variable, is_scalar_assign, is_variable, search_variable
|
32
34
|
from robotcode.core.concurrent import check_current_task_canceled
|
33
35
|
from robotcode.core.lsp.types import (
|
34
36
|
CodeDescription,
|
@@ -54,53 +56,31 @@ from ..utils.ast import (
|
|
54
56
|
from ..utils.visitor import Visitor
|
55
57
|
from .entities import (
|
56
58
|
ArgumentDefinition,
|
57
|
-
CommandLineVariableDefinition,
|
58
59
|
EnvironmentVariableDefinition,
|
60
|
+
GlobalVariableDefinition,
|
59
61
|
LibraryEntry,
|
60
62
|
LocalVariableDefinition,
|
63
|
+
TestVariableDefinition,
|
61
64
|
VariableDefinition,
|
62
65
|
VariableDefinitionType,
|
66
|
+
VariableMatcher,
|
63
67
|
VariableNotFoundDefinition,
|
64
68
|
)
|
65
69
|
from .errors import DIAGNOSTICS_SOURCE_NAME, Error
|
66
|
-
from .
|
67
|
-
|
68
|
-
is_embedded_keyword,
|
69
|
-
)
|
70
|
+
from .keyword_finder import KeywordFinder
|
71
|
+
from .library_doc import KeywordDoc, is_embedded_keyword
|
70
72
|
from .model_helper import ModelHelper
|
71
|
-
|
73
|
+
|
74
|
+
if TYPE_CHECKING:
|
75
|
+
from .namespace import Namespace
|
72
76
|
|
73
77
|
if get_robot_version() < (7, 0):
|
74
78
|
from robot.variables.search import VariableIterator
|
75
79
|
|
76
|
-
VARIABLE_NOT_FOUND_HINT_TYPES: Tuple[Any, ...] = (
|
77
|
-
DocumentationOrMetadata,
|
78
|
-
TestCaseName,
|
79
|
-
Tags,
|
80
|
-
robot.parsing.model.statements.ForceTags,
|
81
|
-
DefaultTags,
|
82
|
-
)
|
83
|
-
|
84
|
-
IN_SETTING_TYPES: Tuple[Any, ...] = (
|
85
|
-
DocumentationOrMetadata,
|
86
|
-
Tags,
|
87
|
-
robot.parsing.model.statements.ForceTags,
|
88
|
-
DefaultTags,
|
89
|
-
Template,
|
90
|
-
)
|
91
80
|
else:
|
81
|
+
from robot.parsing.model.statements import Var
|
92
82
|
from robot.variables.search import VariableMatches
|
93
83
|
|
94
|
-
VARIABLE_NOT_FOUND_HINT_TYPES = (
|
95
|
-
DocumentationOrMetadata,
|
96
|
-
TestCaseName,
|
97
|
-
Tags,
|
98
|
-
robot.parsing.model.statements.TestTags,
|
99
|
-
DefaultTags,
|
100
|
-
)
|
101
|
-
|
102
|
-
IN_SETTING_TYPES = (DocumentationOrMetadata, Tags, robot.parsing.model.statements.TestTags, DefaultTags, Template)
|
103
|
-
|
104
84
|
|
105
85
|
@dataclass
|
106
86
|
class AnalyzerResult:
|
@@ -110,35 +90,66 @@ class AnalyzerResult:
|
|
110
90
|
local_variable_assignments: Dict[VariableDefinition, Set[Range]]
|
111
91
|
namespace_references: Dict[LibraryEntry, Set[Location]]
|
112
92
|
|
93
|
+
# TODO Tag references
|
113
94
|
|
114
|
-
|
95
|
+
|
96
|
+
class NamespaceAnalyzer(Visitor):
|
115
97
|
def __init__(
|
116
98
|
self,
|
117
99
|
model: ast.AST,
|
118
|
-
namespace: Namespace,
|
100
|
+
namespace: "Namespace",
|
119
101
|
finder: KeywordFinder,
|
120
102
|
) -> None:
|
121
103
|
super().__init__()
|
122
104
|
|
123
|
-
self.
|
124
|
-
self.
|
125
|
-
self.
|
105
|
+
self._model = model
|
106
|
+
self._namespace = namespace
|
107
|
+
self._finder = finder
|
126
108
|
|
127
|
-
self.
|
128
|
-
self.
|
129
|
-
self.
|
130
|
-
self.
|
109
|
+
self._current_testcase_or_keyword_name: Optional[str] = None
|
110
|
+
self._current_keyword_doc: Optional[KeywordDoc] = None
|
111
|
+
self._test_template: Optional[TestTemplate] = None
|
112
|
+
self._template: Optional[Template] = None
|
113
|
+
self._node_stack: List[ast.AST] = []
|
131
114
|
self._diagnostics: List[Diagnostic] = []
|
132
115
|
self._keyword_references: Dict[KeywordDoc, Set[Location]] = defaultdict(set)
|
133
116
|
self._variable_references: Dict[VariableDefinition, Set[Location]] = defaultdict(set)
|
134
117
|
self._local_variable_assignments: Dict[VariableDefinition, Set[Range]] = defaultdict(set)
|
135
118
|
self._namespace_references: Dict[LibraryEntry, Set[Location]] = defaultdict(set)
|
136
119
|
|
120
|
+
self._variables: Dict[VariableMatcher, VariableDefinition] = {
|
121
|
+
**{v.matcher: v for v in self._namespace.get_builtin_variables()},
|
122
|
+
**{v.matcher: v for v in self._namespace.get_imported_variables()},
|
123
|
+
**{v.matcher: v for v in self._namespace.get_command_line_variables()},
|
124
|
+
}
|
125
|
+
|
126
|
+
self._overridden_variables: Dict[VariableDefinition, VariableDefinition] = {}
|
127
|
+
|
128
|
+
self._in_setting = False
|
129
|
+
|
130
|
+
self._suite_variables = self._variables.copy()
|
131
|
+
|
137
132
|
def run(self) -> AnalyzerResult:
|
138
133
|
self._diagnostics = []
|
139
134
|
self._keyword_references = defaultdict(set)
|
140
135
|
|
141
|
-
|
136
|
+
if isinstance(self._model, File):
|
137
|
+
for node in self._model.sections:
|
138
|
+
if isinstance(node, VariableSection):
|
139
|
+
self._visit_VariableSection(node)
|
140
|
+
|
141
|
+
self._suite_variables = self._variables.copy()
|
142
|
+
try:
|
143
|
+
self.visit(self._model)
|
144
|
+
except (SystemExit, KeyboardInterrupt, CancelledError):
|
145
|
+
raise
|
146
|
+
except BaseException as e:
|
147
|
+
self._append_diagnostics(
|
148
|
+
range_from_node(self._model),
|
149
|
+
message=f"Fatal: can't analyze namespace '{e}')",
|
150
|
+
severity=DiagnosticSeverity.ERROR,
|
151
|
+
code=type(e).__qualname__,
|
152
|
+
)
|
142
153
|
|
143
154
|
return AnalyzerResult(
|
144
155
|
self._diagnostics,
|
@@ -148,40 +159,12 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
148
159
|
self._namespace_references,
|
149
160
|
)
|
150
161
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
154
|
-
(
|
155
|
-
v
|
156
|
-
for v in itertools.dropwhile(
|
157
|
-
lambda t: t.type in Token.NON_DATA_TOKENS,
|
158
|
-
tokenize_variables(token, ignore_errors=True),
|
159
|
-
)
|
160
|
-
if v.type == Token.VARIABLE
|
161
|
-
),
|
162
|
-
None,
|
163
|
-
)
|
164
|
-
if argument is None or argument.value == token.value:
|
165
|
-
yield token
|
166
|
-
else:
|
167
|
-
yield argument
|
168
|
-
i = len(argument.value)
|
169
|
-
|
170
|
-
for t in self.yield_argument_name_and_rest(
|
171
|
-
node,
|
172
|
-
Token(
|
173
|
-
token.type,
|
174
|
-
token.value[i:],
|
175
|
-
token.lineno,
|
176
|
-
token.col_offset + i,
|
177
|
-
token.error,
|
178
|
-
),
|
179
|
-
):
|
180
|
-
yield t
|
181
|
-
else:
|
182
|
-
yield token
|
162
|
+
def _visit_VariableSection(self, node: VariableSection) -> None: # noqa: N802
|
163
|
+
for v in node.body:
|
164
|
+
if isinstance(v, Variable):
|
165
|
+
self._visit_Variable(v)
|
183
166
|
|
184
|
-
def
|
167
|
+
def _visit_Variable(self, node: Variable) -> None: # noqa: N802
|
185
168
|
name_token = node.get_token(Token.VARIABLE)
|
186
169
|
if name_token is None:
|
187
170
|
return
|
@@ -196,257 +179,332 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
196
179
|
if name.endswith("="):
|
197
180
|
name = name[:-1].rstrip()
|
198
181
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
182
|
+
stripped_name_token = strip_variable_token(
|
183
|
+
Token(name_token.type, name, name_token.lineno, name_token.col_offset, name_token.error)
|
184
|
+
)
|
185
|
+
r = range_from_token(stripped_name_token)
|
186
|
+
|
187
|
+
existing_var = self._find_variable(name)
|
188
|
+
|
189
|
+
values = node.get_values(Token.ARGUMENT)
|
190
|
+
has_value = bool(values)
|
191
|
+
value = tuple(
|
192
|
+
s.replace(
|
193
|
+
"${CURDIR}",
|
194
|
+
str(Path(self._namespace.source).parent).replace("\\", "\\\\"),
|
208
195
|
)
|
196
|
+
for s in values
|
209
197
|
)
|
210
198
|
|
211
|
-
var_def =
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
199
|
+
var_def = VariableDefinition(
|
200
|
+
name=name,
|
201
|
+
name_token=stripped_name_token,
|
202
|
+
line_no=node.lineno,
|
203
|
+
col_offset=node.col_offset,
|
204
|
+
end_line_no=node.lineno,
|
205
|
+
end_col_offset=node.end_col_offset,
|
206
|
+
source=self._namespace.source,
|
207
|
+
has_value=has_value,
|
208
|
+
resolvable=True,
|
209
|
+
value=value,
|
218
210
|
)
|
219
211
|
|
220
|
-
|
221
|
-
|
212
|
+
add_to_references = True
|
213
|
+
first_overidden_reference: Optional[VariableDefinition] = None
|
214
|
+
if existing_var is not None:
|
222
215
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
216
|
+
self._variable_references[existing_var].add(Location(self._namespace.document_uri, r))
|
217
|
+
if existing_var not in self._overridden_variables:
|
218
|
+
self._overridden_variables[existing_var] = var_def
|
219
|
+
else:
|
220
|
+
add_to_references = False
|
221
|
+
first_overidden_reference = self._overridden_variables[existing_var]
|
222
|
+
self._variable_references[first_overidden_reference].add(Location(self._namespace.document_uri, r))
|
223
|
+
|
224
|
+
if add_to_references and existing_var.type in [
|
225
|
+
VariableDefinitionType.GLOBAL_VARIABLE,
|
226
|
+
VariableDefinitionType.COMMAND_LINE_VARIABLE,
|
227
|
+
]:
|
228
|
+
self._append_diagnostics(
|
229
|
+
r,
|
230
|
+
"Overridden by command line variable.",
|
231
|
+
DiagnosticSeverity.HINT,
|
232
|
+
Error.OVERRIDDEN_BY_COMMANDLINE,
|
233
|
+
)
|
234
|
+
else:
|
235
|
+
if not add_to_references or existing_var.source == self._namespace.source:
|
236
|
+
self._append_diagnostics(
|
237
|
+
r,
|
238
|
+
f"Variable '{name}' already defined.",
|
239
|
+
DiagnosticSeverity.INFORMATION,
|
240
|
+
Error.VARIABLE_ALREADY_DEFINED,
|
241
|
+
tags=[DiagnosticTag.UNNECESSARY],
|
242
|
+
related_information=(
|
243
|
+
[
|
244
|
+
*(
|
245
|
+
[
|
246
|
+
DiagnosticRelatedInformation(
|
247
|
+
location=Location(
|
248
|
+
uri=str(Uri.from_path(first_overidden_reference.source)),
|
249
|
+
range=range_from_token(first_overidden_reference.name_token),
|
250
|
+
),
|
251
|
+
message="Already defined here.",
|
252
|
+
)
|
253
|
+
]
|
254
|
+
if not add_to_references
|
255
|
+
and first_overidden_reference is not None
|
256
|
+
and first_overidden_reference.source
|
257
|
+
else []
|
258
|
+
),
|
259
|
+
*(
|
260
|
+
[
|
261
|
+
DiagnosticRelatedInformation(
|
262
|
+
location=Location(
|
263
|
+
uri=str(Uri.from_path(existing_var.source)),
|
264
|
+
range=range_from_token(existing_var.name_token),
|
265
|
+
),
|
266
|
+
message="Already defined here.",
|
267
|
+
)
|
268
|
+
]
|
269
|
+
if existing_var.source
|
270
|
+
else []
|
271
|
+
),
|
272
|
+
]
|
273
|
+
),
|
274
|
+
)
|
275
|
+
else:
|
276
|
+
self._append_diagnostics(
|
277
|
+
r,
|
278
|
+
f"Variable '{name}' is being overwritten.",
|
279
|
+
DiagnosticSeverity.HINT,
|
280
|
+
Error.VARIABLE_OVERRIDDEN,
|
281
|
+
related_information=(
|
282
|
+
[
|
283
|
+
DiagnosticRelatedInformation(
|
284
|
+
location=Location(
|
285
|
+
uri=str(Uri.from_path(existing_var.source)),
|
286
|
+
range=range_from_token(existing_var.name_token),
|
287
|
+
),
|
288
|
+
message="Already defined here.",
|
289
|
+
)
|
290
|
+
]
|
291
|
+
if existing_var.source
|
292
|
+
else None
|
293
|
+
),
|
294
|
+
)
|
232
295
|
|
233
|
-
|
296
|
+
else:
|
297
|
+
self._variables[var_def.matcher] = var_def
|
298
|
+
|
299
|
+
if add_to_references:
|
234
300
|
self._variable_references[var_def] = set()
|
235
301
|
|
236
|
-
|
237
|
-
name_token = node.get_token(Token.VARIABLE)
|
238
|
-
if name_token is None:
|
239
|
-
return
|
302
|
+
if get_robot_version() >= (7, 0):
|
240
303
|
|
241
|
-
|
304
|
+
def visit_Var(self, node: Statement) -> None: # noqa: N802
|
305
|
+
self._analyze_statement_variables(node)
|
242
306
|
|
243
|
-
|
244
|
-
|
245
|
-
if not match.is_assign(allow_assign_mark=True):
|
307
|
+
variable = node.get_token(Token.VARIABLE)
|
308
|
+
if variable is None:
|
246
309
|
return
|
247
310
|
|
248
|
-
|
249
|
-
|
311
|
+
try:
|
312
|
+
var_name = variable.value
|
313
|
+
if var_name.endswith("="):
|
314
|
+
var_name = var_name[:-1].rstrip()
|
250
315
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
316
|
+
if not is_variable(var_name):
|
317
|
+
return
|
318
|
+
|
319
|
+
scope = cast(Var, node).scope
|
320
|
+
if scope:
|
321
|
+
scope = scope.upper()
|
322
|
+
|
323
|
+
if scope in ("SUITE",):
|
324
|
+
var_type = VariableDefinition
|
325
|
+
elif scope in ("TEST", "TASK"):
|
326
|
+
var_type = TestVariableDefinition
|
327
|
+
elif scope in ("GLOBAL",):
|
328
|
+
var_type = GlobalVariableDefinition
|
329
|
+
else:
|
330
|
+
var_type = LocalVariableDefinition
|
331
|
+
|
332
|
+
var = var_type(
|
333
|
+
name=var_name,
|
334
|
+
name_token=strip_variable_token(variable),
|
335
|
+
line_no=variable.lineno,
|
336
|
+
col_offset=variable.col_offset,
|
337
|
+
end_line_no=variable.lineno,
|
338
|
+
end_col_offset=variable.end_col_offset,
|
339
|
+
source=self._namespace.source,
|
260
340
|
)
|
261
|
-
)
|
262
341
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
nodes=self.node_stack,
|
267
|
-
position=range_from_token(node.get_token(Token.VAR)).start,
|
268
|
-
ignore_error=True,
|
269
|
-
)
|
270
|
-
if var_def is not None:
|
271
|
-
if var_def.name_range != r:
|
272
|
-
if self.namespace.document is not None:
|
273
|
-
self._variable_references[var_def].add(Location(self.namespace.document.document_uri, r))
|
342
|
+
if var.matcher not in self._variables:
|
343
|
+
self._variables[var.matcher] = var
|
344
|
+
self._variable_references[var] = set()
|
274
345
|
else:
|
275
|
-
|
276
|
-
self._variable_references[var_def] = set()
|
346
|
+
existing_var = self._variables[var.matcher]
|
277
347
|
|
278
|
-
|
279
|
-
|
348
|
+
location = Location(self._namespace.document_uri, range_from_token(strip_variable_token(variable)))
|
349
|
+
self._variable_references[existing_var].add(location)
|
350
|
+
if existing_var in self._overridden_variables:
|
351
|
+
self._variable_references[self._overridden_variables[existing_var]].add(location)
|
280
352
|
|
281
|
-
|
353
|
+
except VariableError:
|
354
|
+
pass
|
282
355
|
|
283
|
-
|
284
|
-
|
285
|
-
else:
|
286
|
-
variable_statements = (Variable, robot.parsing.model.statements.Var)
|
356
|
+
def visit_Statement(self, node: Statement) -> None: # noqa: N802
|
357
|
+
self._analyze_statement_variables(node)
|
287
358
|
|
288
|
-
def
|
289
|
-
|
359
|
+
def _analyze_statement_variables(
|
360
|
+
self, node: Statement, severity: DiagnosticSeverity = DiagnosticSeverity.ERROR
|
361
|
+
) -> None:
|
362
|
+
for token in node.get_tokens(Token.ARGUMENT):
|
363
|
+
self._analyze_token_variables(token, severity)
|
364
|
+
|
365
|
+
def _analyze_statement_expression_variables(
|
366
|
+
self, node: Statement, severity: DiagnosticSeverity = DiagnosticSeverity.ERROR
|
367
|
+
) -> None:
|
290
368
|
|
291
|
-
|
369
|
+
for token in node.get_tokens(Token.ARGUMENT):
|
370
|
+
self._analyze_token_variables(token, severity)
|
371
|
+
self._analyze_token_expression_variables(token, severity)
|
372
|
+
|
373
|
+
def _visit_settings_statement(
|
374
|
+
self, node: Statement, severity: DiagnosticSeverity = DiagnosticSeverity.ERROR
|
375
|
+
) -> None:
|
376
|
+
self._in_setting = True
|
292
377
|
try:
|
293
|
-
|
378
|
+
self._analyze_statement_variables(node, severity)
|
379
|
+
finally:
|
380
|
+
self._in_setting = False
|
294
381
|
|
295
|
-
|
296
|
-
|
297
|
-
|
382
|
+
def _analyze_token_expression_variables(
|
383
|
+
self, token: Token, severity: DiagnosticSeverity = DiagnosticSeverity.ERROR
|
384
|
+
) -> None:
|
385
|
+
for var_token, var in self._iter_expression_variables_from_token(token):
|
386
|
+
self._handle_find_variable_result(token, var_token, var, severity)
|
298
387
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
)
|
312
|
-
|
313
|
-
|
314
|
-
|
388
|
+
def _append_error_from_node(
|
389
|
+
self,
|
390
|
+
node: ast.AST,
|
391
|
+
msg: str,
|
392
|
+
only_start: bool = True,
|
393
|
+
) -> None:
|
394
|
+
from robot.parsing.model.statements import Statement
|
395
|
+
|
396
|
+
if hasattr(node, "header") and hasattr(node, "body"):
|
397
|
+
if node.header is not None:
|
398
|
+
node = node.header
|
399
|
+
elif node.body:
|
400
|
+
stmt = next((n for n in node.body if isinstance(n, Statement)), None)
|
401
|
+
if stmt is not None:
|
402
|
+
node = stmt
|
403
|
+
|
404
|
+
self._append_diagnostics(
|
405
|
+
range=range_from_node(node, True, only_start),
|
406
|
+
message=msg,
|
407
|
+
severity=DiagnosticSeverity.ERROR,
|
408
|
+
code=Error.MODEL_ERROR,
|
409
|
+
)
|
315
410
|
|
316
|
-
|
317
|
-
|
318
|
-
self.namespace,
|
319
|
-
self.node_stack,
|
320
|
-
range_from_token(token).start,
|
321
|
-
skip_commandline_variables=False,
|
322
|
-
skip_local_variables=in_setting,
|
323
|
-
return_not_found=True,
|
324
|
-
):
|
325
|
-
if isinstance(var, VariableNotFoundDefinition):
|
326
|
-
self.append_diagnostics(
|
327
|
-
range=range_from_token(var_token),
|
328
|
-
message=f"Variable '{var.name}' not found.",
|
329
|
-
severity=severity,
|
330
|
-
source=DIAGNOSTICS_SOURCE_NAME,
|
331
|
-
code=Error.VARIABLE_NOT_FOUND,
|
332
|
-
)
|
333
|
-
else:
|
334
|
-
if isinstance(var, EnvironmentVariableDefinition) and var.default_value is None:
|
335
|
-
env_name = var.name[2:-1]
|
336
|
-
if os.environ.get(env_name, None) is None:
|
337
|
-
self.append_diagnostics(
|
338
|
-
range=range_from_token(var_token),
|
339
|
-
message=f"Environment variable '{var.name}' not found.",
|
340
|
-
severity=severity,
|
341
|
-
source=DIAGNOSTICS_SOURCE_NAME,
|
342
|
-
code=Error.ENVIROMMENT_VARIABLE_NOT_FOUND,
|
343
|
-
)
|
411
|
+
def visit(self, node: ast.AST) -> None:
|
412
|
+
check_current_task_canceled()
|
344
413
|
|
345
|
-
|
346
|
-
if isinstance(var, EnvironmentVariableDefinition):
|
347
|
-
(
|
348
|
-
var_token.value,
|
349
|
-
_,
|
350
|
-
_,
|
351
|
-
) = var_token.value.partition("=")
|
352
|
-
|
353
|
-
var_range = range_from_token(var_token)
|
354
|
-
|
355
|
-
suite_var = None
|
356
|
-
if isinstance(var, CommandLineVariableDefinition):
|
357
|
-
suite_var = self.namespace.find_variable(
|
358
|
-
var.name,
|
359
|
-
skip_commandline_variables=True,
|
360
|
-
ignore_error=True,
|
361
|
-
)
|
362
|
-
if suite_var is not None and suite_var.type != VariableDefinitionType.VARIABLE:
|
363
|
-
suite_var = None
|
364
|
-
|
365
|
-
if var.name_range != var_range:
|
366
|
-
self._variable_references[var].add(
|
367
|
-
Location(
|
368
|
-
self.namespace.document.document_uri,
|
369
|
-
var_range,
|
370
|
-
)
|
371
|
-
)
|
372
|
-
if suite_var is not None:
|
373
|
-
self._variable_references[suite_var].add(
|
374
|
-
Location(
|
375
|
-
self.namespace.document.document_uri,
|
376
|
-
var_range,
|
377
|
-
)
|
378
|
-
)
|
379
|
-
if token1.type == Token.ASSIGN and isinstance(
|
380
|
-
var,
|
381
|
-
(
|
382
|
-
LocalVariableDefinition,
|
383
|
-
ArgumentDefinition,
|
384
|
-
),
|
385
|
-
):
|
386
|
-
self._local_variable_assignments[var].add(var_range)
|
387
|
-
|
388
|
-
elif var not in self._variable_references and token1.type in [
|
389
|
-
Token.ASSIGN,
|
390
|
-
Token.ARGUMENT,
|
391
|
-
Token.VARIABLE,
|
392
|
-
]:
|
393
|
-
self._variable_references[var] = set()
|
394
|
-
if suite_var is not None:
|
395
|
-
self._variable_references[suite_var] = set()
|
414
|
+
already_added_errors = set()
|
396
415
|
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
skip_commandline_variables=False,
|
408
|
-
skip_local_variables=in_setting,
|
409
|
-
return_not_found=True,
|
410
|
-
):
|
411
|
-
if isinstance(var, VariableNotFoundDefinition):
|
412
|
-
self.append_diagnostics(
|
413
|
-
range=range_from_token(var_token),
|
414
|
-
message=f"Variable '{var.name}' not found.",
|
416
|
+
if isinstance(node, Statement):
|
417
|
+
errors = node.get_tokens(Token.ERROR, Token.FATAL_ERROR)
|
418
|
+
if errors:
|
419
|
+
for error in errors:
|
420
|
+
if error.error is not None and error.error not in already_added_errors:
|
421
|
+
already_added_errors.add(error.error)
|
422
|
+
|
423
|
+
self._append_diagnostics(
|
424
|
+
range=range_from_token(error),
|
425
|
+
message=error.error if error.error is not None else "(No Message).",
|
415
426
|
severity=DiagnosticSeverity.ERROR,
|
416
|
-
|
417
|
-
code=Error.VARIABLE_NOT_FOUND,
|
427
|
+
code=Error.TOKEN_ERROR,
|
418
428
|
)
|
419
|
-
else:
|
420
|
-
if self.namespace.document is not None:
|
421
|
-
var_range = range_from_token(var_token)
|
422
|
-
|
423
|
-
if var.name_range != var_range:
|
424
|
-
self._variable_references[var].add(
|
425
|
-
Location(
|
426
|
-
self.namespace.document.document_uri,
|
427
|
-
range_from_token(var_token),
|
428
|
-
)
|
429
|
-
)
|
430
|
-
|
431
|
-
if isinstance(var, CommandLineVariableDefinition):
|
432
|
-
suite_var = self.namespace.find_variable(
|
433
|
-
var.name,
|
434
|
-
skip_commandline_variables=True,
|
435
|
-
ignore_error=True,
|
436
|
-
)
|
437
|
-
if suite_var is not None and suite_var.type == VariableDefinitionType.VARIABLE:
|
438
|
-
self._variable_references[suite_var].add(
|
439
|
-
Location(
|
440
|
-
self.namespace.document.document_uri,
|
441
|
-
range_from_token(var_token),
|
442
|
-
)
|
443
|
-
)
|
444
429
|
|
430
|
+
if hasattr(node, "error"):
|
431
|
+
error = node.error
|
432
|
+
if error is not None and error not in already_added_errors:
|
433
|
+
already_added_errors.add(error)
|
434
|
+
self._append_error_from_node(node, error or "(No Message).")
|
435
|
+
|
436
|
+
if hasattr(node, "errors"):
|
437
|
+
errors = node.errors
|
438
|
+
if errors:
|
439
|
+
for error in errors:
|
440
|
+
if error is not None and error not in already_added_errors:
|
441
|
+
already_added_errors.add(error)
|
442
|
+
self._append_error_from_node(node, error or "(No Message).")
|
443
|
+
|
444
|
+
self._node_stack.append(node)
|
445
|
+
try:
|
445
446
|
super().visit(node)
|
446
447
|
finally:
|
447
|
-
self.
|
448
|
+
self._node_stack.pop()
|
448
449
|
|
449
|
-
def
|
450
|
+
def _analyze_token_variables(self, token: Token, severity: DiagnosticSeverity = DiagnosticSeverity.ERROR) -> None:
|
451
|
+
for var_token, var in self._iter_variables_from_token(token):
|
452
|
+
self._handle_find_variable_result(token, var_token, var, severity)
|
453
|
+
|
454
|
+
def _handle_find_variable_result(
|
455
|
+
self,
|
456
|
+
token: Token,
|
457
|
+
var_token: Token,
|
458
|
+
var: VariableDefinition,
|
459
|
+
severity: DiagnosticSeverity = DiagnosticSeverity.ERROR,
|
460
|
+
) -> None:
|
461
|
+
if var.type == VariableDefinitionType.VARIABLE_NOT_FOUND:
|
462
|
+
self._append_diagnostics(
|
463
|
+
range=range_from_token(var_token),
|
464
|
+
message=f"Variable '{var.name}' not found.",
|
465
|
+
severity=severity,
|
466
|
+
code=Error.VARIABLE_NOT_FOUND,
|
467
|
+
)
|
468
|
+
else:
|
469
|
+
if (
|
470
|
+
var.type == VariableDefinitionType.ENVIRONMENT_VARIABLE
|
471
|
+
and cast(EnvironmentVariableDefinition, var).default_value is None
|
472
|
+
):
|
473
|
+
env_name = var.name[2:-1]
|
474
|
+
if os.environ.get(env_name, None) is None:
|
475
|
+
self._append_diagnostics(
|
476
|
+
range=range_from_token(var_token),
|
477
|
+
message=f"Environment variable '{var.name}' not found.",
|
478
|
+
severity=severity,
|
479
|
+
code=Error.ENVIROMMENT_VARIABLE_NOT_FOUND,
|
480
|
+
)
|
481
|
+
|
482
|
+
if var.type == VariableDefinitionType.ENVIRONMENT_VARIABLE:
|
483
|
+
(
|
484
|
+
var_token.value,
|
485
|
+
_,
|
486
|
+
_,
|
487
|
+
) = var_token.value.partition("=")
|
488
|
+
|
489
|
+
var_range = range_from_token(var_token)
|
490
|
+
|
491
|
+
suite_var = None
|
492
|
+
if var.type in [
|
493
|
+
VariableDefinitionType.COMMAND_LINE_VARIABLE,
|
494
|
+
VariableDefinitionType.GLOBAL_VARIABLE,
|
495
|
+
VariableDefinitionType.TEST_VARIABLE,
|
496
|
+
VariableDefinitionType.VARIABLE,
|
497
|
+
]:
|
498
|
+
suite_var = self._overridden_variables.get(var, None)
|
499
|
+
|
500
|
+
if suite_var is not None and suite_var.type != VariableDefinitionType.VARIABLE:
|
501
|
+
suite_var = None
|
502
|
+
|
503
|
+
self._variable_references[var].add(Location(self._namespace.document_uri, var_range))
|
504
|
+
if suite_var is not None:
|
505
|
+
self._variable_references[suite_var].add(Location(self._namespace.document_uri, var_range))
|
506
|
+
|
507
|
+
def _append_diagnostics(
|
450
508
|
self,
|
451
509
|
range: Range,
|
452
510
|
message: str,
|
@@ -475,43 +533,39 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
475
533
|
|
476
534
|
def _analyze_keyword_call(
|
477
535
|
self,
|
478
|
-
keyword: Optional[str],
|
479
536
|
node: ast.AST,
|
480
537
|
keyword_token: Token,
|
481
538
|
argument_tokens: List[Token],
|
482
|
-
|
539
|
+
analyze_run_keywords: bool = True,
|
483
540
|
allow_variables: bool = False,
|
484
541
|
ignore_errors_if_contains_variables: bool = False,
|
485
542
|
) -> Optional[KeywordDoc]:
|
486
543
|
result: Optional[KeywordDoc] = None
|
487
544
|
|
545
|
+
keyword = unescape(keyword_token.value)
|
546
|
+
|
488
547
|
try:
|
548
|
+
lib_entry = None
|
549
|
+
lib_range = None
|
550
|
+
kw_namespace = None
|
551
|
+
|
489
552
|
if not allow_variables and not is_not_variable_token(keyword_token):
|
490
553
|
return None
|
491
554
|
|
492
|
-
|
493
|
-
self.finder.find_keyword(
|
494
|
-
keyword_token.value,
|
495
|
-
raise_keyword_error=False,
|
496
|
-
handle_bdd_style=False,
|
497
|
-
)
|
498
|
-
is None
|
499
|
-
):
|
500
|
-
keyword_token = self.strip_bdd_prefix(self.namespace, keyword_token)
|
555
|
+
result = self._finder.find_keyword(keyword, raise_keyword_error=False, handle_bdd_style=False)
|
501
556
|
|
502
|
-
|
557
|
+
if result is None:
|
558
|
+
keyword_token = ModelHelper.strip_bdd_prefix(self._namespace, keyword_token)
|
503
559
|
|
504
|
-
|
505
|
-
lib_range = None
|
506
|
-
kw_namespace = None
|
560
|
+
result = self._finder.find_keyword(keyword, raise_keyword_error=False)
|
507
561
|
|
508
|
-
|
562
|
+
kw_range = range_from_token(keyword_token)
|
509
563
|
|
510
|
-
if keyword
|
564
|
+
if keyword:
|
511
565
|
(
|
512
566
|
lib_entry,
|
513
567
|
kw_namespace,
|
514
|
-
) =
|
568
|
+
) = ModelHelper.get_namespace_info_from_keyword_token(self._namespace, keyword_token)
|
515
569
|
|
516
570
|
if lib_entry and kw_namespace:
|
517
571
|
r = range_from_token(keyword_token)
|
@@ -531,19 +585,18 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
531
585
|
kw_range = range_from_token(keyword_token)
|
532
586
|
|
533
587
|
if kw_namespace and lib_entry is not None and lib_range is not None:
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
self._namespace_references[entry].add(Location(self.namespace.document.document_uri, lib_range))
|
588
|
+
entries = [lib_entry]
|
589
|
+
if self._finder.multiple_keywords_result is not None:
|
590
|
+
entries = next(
|
591
|
+
(v for k, v in (self._namespace.get_namespaces()).items() if k == kw_namespace),
|
592
|
+
entries,
|
593
|
+
)
|
594
|
+
for entry in entries:
|
595
|
+
self._namespace_references[entry].add(Location(self._namespace.document_uri, lib_range))
|
543
596
|
|
544
597
|
if not ignore_errors_if_contains_variables or is_not_variable_token(keyword_token):
|
545
|
-
for e in self.
|
546
|
-
self.
|
598
|
+
for e in self._finder.diagnostics:
|
599
|
+
self._append_diagnostics(
|
547
600
|
range=kw_range,
|
548
601
|
message=e.message,
|
549
602
|
severity=e.severity,
|
@@ -551,15 +604,15 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
551
604
|
)
|
552
605
|
|
553
606
|
if result is None:
|
554
|
-
if self.
|
555
|
-
for d in self.
|
556
|
-
self._keyword_references[d].add(Location(self.
|
607
|
+
if self._finder.multiple_keywords_result is not None:
|
608
|
+
for d in self._finder.multiple_keywords_result:
|
609
|
+
self._keyword_references[d].add(Location(self._namespace.document_uri, kw_range))
|
557
610
|
else:
|
558
|
-
|
559
|
-
|
611
|
+
|
612
|
+
self._keyword_references[result].add(Location(self._namespace.document_uri, kw_range))
|
560
613
|
|
561
614
|
if result.errors:
|
562
|
-
self.
|
615
|
+
self._append_diagnostics(
|
563
616
|
range=kw_range,
|
564
617
|
message="Keyword definition contains errors.",
|
565
618
|
severity=DiagnosticSeverity.ERROR,
|
@@ -591,7 +644,7 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
591
644
|
)
|
592
645
|
|
593
646
|
if result.is_deprecated:
|
594
|
-
self.
|
647
|
+
self._append_diagnostics(
|
595
648
|
range=kw_range,
|
596
649
|
message=f"Keyword '{result.name}' is deprecated"
|
597
650
|
f"{f': {result.deprecated_message}' if result.deprecated_message else ''}.",
|
@@ -600,14 +653,14 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
600
653
|
code=Error.DEPRECATED_KEYWORD,
|
601
654
|
)
|
602
655
|
if result.is_error_handler:
|
603
|
-
self.
|
656
|
+
self._append_diagnostics(
|
604
657
|
range=kw_range,
|
605
658
|
message=f"Keyword definition contains errors: {result.error_handler_message}",
|
606
659
|
severity=DiagnosticSeverity.ERROR,
|
607
660
|
code=Error.KEYWORD_CONTAINS_ERRORS,
|
608
661
|
)
|
609
662
|
if result.is_reserved():
|
610
|
-
self.
|
663
|
+
self._append_diagnostics(
|
611
664
|
range=kw_range,
|
612
665
|
message=f"'{result.name}' is a reserved keyword.",
|
613
666
|
severity=DiagnosticSeverity.ERROR,
|
@@ -615,8 +668,8 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
615
668
|
)
|
616
669
|
|
617
670
|
if get_robot_version() >= (6, 0) and result.is_resource_keyword and result.is_private():
|
618
|
-
if self.
|
619
|
-
self.
|
671
|
+
if self._namespace.source != result.source:
|
672
|
+
self._append_diagnostics(
|
620
673
|
range=kw_range,
|
621
674
|
message=f"Keyword '{result.longname}' is private and should only be called by"
|
622
675
|
f" keywords in the same file.",
|
@@ -636,7 +689,7 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
636
689
|
except (SystemExit, KeyboardInterrupt):
|
637
690
|
raise
|
638
691
|
except BaseException as e:
|
639
|
-
self.
|
692
|
+
self._append_diagnostics(
|
640
693
|
range=Range(
|
641
694
|
start=kw_range.start,
|
642
695
|
end=range_from_token(argument_tokens[-1]).end if argument_tokens else kw_range.end,
|
@@ -649,14 +702,14 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
649
702
|
except (SystemExit, KeyboardInterrupt):
|
650
703
|
raise
|
651
704
|
except BaseException as e:
|
652
|
-
self.
|
705
|
+
self._append_diagnostics(
|
653
706
|
range=range_from_node_or_token(node, keyword_token),
|
654
707
|
message=str(e),
|
655
708
|
severity=DiagnosticSeverity.ERROR,
|
656
709
|
code=type(e).__qualname__,
|
657
710
|
)
|
658
711
|
|
659
|
-
if
|
712
|
+
if result is not None:
|
660
713
|
if result.longname in [
|
661
714
|
"BuiltIn.Evaluate",
|
662
715
|
"BuiltIn.Should Be True",
|
@@ -672,46 +725,8 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
672
725
|
]:
|
673
726
|
tokens = argument_tokens
|
674
727
|
if tokens and (token := tokens[0]):
|
675
|
-
|
676
|
-
var_token,
|
677
|
-
var,
|
678
|
-
) in self.iter_expression_variables_from_token(
|
679
|
-
token,
|
680
|
-
self.namespace,
|
681
|
-
self.node_stack,
|
682
|
-
range_from_token(token).start,
|
683
|
-
skip_commandline_variables=False,
|
684
|
-
return_not_found=True,
|
685
|
-
):
|
686
|
-
if isinstance(var, VariableNotFoundDefinition):
|
687
|
-
self.append_diagnostics(
|
688
|
-
range=range_from_token(var_token),
|
689
|
-
message=f"Variable '{var.name}' not found.",
|
690
|
-
severity=DiagnosticSeverity.ERROR,
|
691
|
-
code=Error.VARIABLE_NOT_FOUND,
|
692
|
-
)
|
693
|
-
else:
|
694
|
-
if self.namespace.document is not None:
|
695
|
-
self._variable_references[var].add(
|
696
|
-
Location(
|
697
|
-
self.namespace.document.document_uri,
|
698
|
-
range_from_token(var_token),
|
699
|
-
)
|
700
|
-
)
|
728
|
+
self._analyze_token_expression_variables(token)
|
701
729
|
|
702
|
-
if isinstance(var, CommandLineVariableDefinition):
|
703
|
-
suite_var = self.namespace.find_variable(
|
704
|
-
var.name,
|
705
|
-
skip_commandline_variables=True,
|
706
|
-
ignore_error=True,
|
707
|
-
)
|
708
|
-
if suite_var is not None and suite_var.type == VariableDefinitionType.VARIABLE:
|
709
|
-
self._variable_references[suite_var].add(
|
710
|
-
Location(
|
711
|
-
self.namespace.document.document_uri,
|
712
|
-
range_from_token(var_token),
|
713
|
-
)
|
714
|
-
)
|
715
730
|
if result.argument_definitions:
|
716
731
|
for arg in argument_tokens:
|
717
732
|
name, value = split_from_equals(arg.value)
|
@@ -724,17 +739,17 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
724
739
|
name_token = Token(Token.ARGUMENT, name, arg.lineno, arg.col_offset)
|
725
740
|
self._variable_references[arg_def].add(
|
726
741
|
Location(
|
727
|
-
self.
|
742
|
+
self._namespace.document_uri,
|
728
743
|
range_from_token(name_token),
|
729
744
|
)
|
730
745
|
)
|
731
746
|
|
732
|
-
if result is not None and
|
733
|
-
self.
|
747
|
+
if result is not None and analyze_run_keywords:
|
748
|
+
self._analyze_run_keyword(result, node, argument_tokens)
|
734
749
|
|
735
750
|
return result
|
736
751
|
|
737
|
-
def
|
752
|
+
def _analyze_run_keyword(
|
738
753
|
self,
|
739
754
|
keyword_doc: Optional[KeywordDoc],
|
740
755
|
node: ast.AST,
|
@@ -745,7 +760,6 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
745
760
|
|
746
761
|
if keyword_doc.is_run_keyword() and len(argument_tokens) > 0:
|
747
762
|
self._analyze_keyword_call(
|
748
|
-
unescape(argument_tokens[0].value),
|
749
763
|
node,
|
750
764
|
argument_tokens[0],
|
751
765
|
argument_tokens[1:],
|
@@ -759,7 +773,6 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
759
773
|
cond_count := keyword_doc.run_keyword_condition_count()
|
760
774
|
):
|
761
775
|
self._analyze_keyword_call(
|
762
|
-
unescape(argument_tokens[cond_count].value),
|
763
776
|
node,
|
764
777
|
argument_tokens[cond_count],
|
765
778
|
argument_tokens[cond_count + 1 :],
|
@@ -774,7 +787,7 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
774
787
|
t = argument_tokens[0]
|
775
788
|
argument_tokens = argument_tokens[1:]
|
776
789
|
if t.value == "AND":
|
777
|
-
self.
|
790
|
+
self._append_diagnostics(
|
778
791
|
range=range_from_token(t),
|
779
792
|
message=f"Incorrect use of {t.value}.",
|
780
793
|
severity=DiagnosticSeverity.ERROR,
|
@@ -793,7 +806,6 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
793
806
|
argument_tokens = []
|
794
807
|
|
795
808
|
self._analyze_keyword_call(
|
796
|
-
unescape(t.value),
|
797
809
|
node,
|
798
810
|
t,
|
799
811
|
args,
|
@@ -817,12 +829,12 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
817
829
|
|
818
830
|
return result
|
819
831
|
|
820
|
-
result = self.
|
832
|
+
result = self._finder.find_keyword(argument_tokens[1].value)
|
821
833
|
|
822
834
|
if result is not None and result.is_any_run_keyword():
|
823
835
|
argument_tokens = argument_tokens[2:]
|
824
836
|
|
825
|
-
argument_tokens = self.
|
837
|
+
argument_tokens = self._analyze_run_keyword(result, node, argument_tokens)
|
826
838
|
else:
|
827
839
|
kwt = argument_tokens[1]
|
828
840
|
argument_tokens = argument_tokens[2:]
|
@@ -830,11 +842,10 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
830
842
|
args = skip_args()
|
831
843
|
|
832
844
|
self._analyze_keyword_call(
|
833
|
-
unescape(kwt.value),
|
834
845
|
node,
|
835
846
|
kwt,
|
836
847
|
args,
|
837
|
-
|
848
|
+
analyze_run_keywords=False,
|
838
849
|
allow_variables=True,
|
839
850
|
ignore_errors_if_contains_variables=True,
|
840
851
|
)
|
@@ -847,15 +858,14 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
847
858
|
args = skip_args()
|
848
859
|
|
849
860
|
result = self._analyze_keyword_call(
|
850
|
-
unescape(kwt.value),
|
851
861
|
node,
|
852
862
|
kwt,
|
853
863
|
args,
|
854
|
-
|
864
|
+
analyze_run_keywords=False,
|
855
865
|
)
|
856
866
|
|
857
867
|
if result is not None and result.is_any_run_keyword():
|
858
|
-
argument_tokens = self.
|
868
|
+
argument_tokens = self._analyze_run_keyword(result, node, argument_tokens)
|
859
869
|
|
860
870
|
break
|
861
871
|
|
@@ -866,15 +876,14 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
866
876
|
args = skip_args()
|
867
877
|
|
868
878
|
result = self._analyze_keyword_call(
|
869
|
-
unescape(kwt.value),
|
870
879
|
node,
|
871
880
|
kwt,
|
872
881
|
args,
|
873
|
-
|
882
|
+
analyze_run_keywords=False,
|
874
883
|
)
|
875
884
|
|
876
885
|
if result is not None and result.is_any_run_keyword():
|
877
|
-
argument_tokens = self.
|
886
|
+
argument_tokens = self._analyze_run_keyword(result, node, argument_tokens)
|
878
887
|
else:
|
879
888
|
break
|
880
889
|
|
@@ -885,13 +894,11 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
885
894
|
|
886
895
|
# TODO: calculate possible variables in NAME
|
887
896
|
|
888
|
-
if (
|
889
|
-
keyword_token
|
890
|
-
|
891
|
-
|
892
|
-
):
|
897
|
+
if keyword_token is not None and keyword_token.value and keyword_token.value.upper() not in ("", "NONE"):
|
898
|
+
self._analyze_token_variables(keyword_token)
|
899
|
+
self._analyze_statement_variables(node)
|
900
|
+
|
893
901
|
self._analyze_keyword_call(
|
894
|
-
node.name,
|
895
902
|
node,
|
896
903
|
keyword_token,
|
897
904
|
[e for e in node.get_tokens(Token.ARGUMENT)],
|
@@ -899,8 +906,6 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
899
906
|
ignore_errors_if_contains_variables=True,
|
900
907
|
)
|
901
908
|
|
902
|
-
self.generic_visit(node)
|
903
|
-
|
904
909
|
def visit_TestTemplate(self, node: TestTemplate) -> None: # noqa: N802
|
905
910
|
keyword_token = node.get_token(Token.NAME)
|
906
911
|
|
@@ -909,16 +914,14 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
909
914
|
"NONE",
|
910
915
|
):
|
911
916
|
self._analyze_keyword_call(
|
912
|
-
node.value,
|
913
917
|
node,
|
914
918
|
keyword_token,
|
915
919
|
[],
|
916
|
-
|
920
|
+
analyze_run_keywords=False,
|
917
921
|
allow_variables=True,
|
918
922
|
)
|
919
923
|
|
920
|
-
self.
|
921
|
-
self.generic_visit(node)
|
924
|
+
self._test_template = node
|
922
925
|
|
923
926
|
def visit_Template(self, node: Template) -> None: # noqa: N802
|
924
927
|
keyword_token = node.get_token(Token.NAME)
|
@@ -928,36 +931,37 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
928
931
|
"NONE",
|
929
932
|
):
|
930
933
|
self._analyze_keyword_call(
|
931
|
-
node.value,
|
932
934
|
node,
|
933
935
|
keyword_token,
|
934
936
|
[],
|
935
|
-
|
937
|
+
analyze_run_keywords=False,
|
936
938
|
allow_variables=True,
|
937
939
|
)
|
938
|
-
self.
|
939
|
-
self.generic_visit(node)
|
940
|
+
self._template = node
|
940
941
|
|
941
942
|
def visit_KeywordCall(self, node: KeywordCall) -> None: # noqa: N802
|
942
943
|
keyword_token = node.get_token(Token.KEYWORD)
|
943
944
|
|
944
945
|
if node.assign and keyword_token is None:
|
945
|
-
self.
|
946
|
+
self._append_diagnostics(
|
946
947
|
range=range_from_node_or_token(node, node.get_token(Token.ASSIGN)),
|
947
948
|
message="Keyword name cannot be empty.",
|
948
949
|
severity=DiagnosticSeverity.ERROR,
|
949
950
|
code=Error.KEYWORD_NAME_EMPTY,
|
950
951
|
)
|
951
|
-
|
952
|
-
self._analyze_keyword_call(
|
953
|
-
node.keyword,
|
954
|
-
node,
|
955
|
-
keyword_token,
|
956
|
-
[e for e in node.get_tokens(Token.ARGUMENT)],
|
957
|
-
)
|
952
|
+
return
|
958
953
|
|
959
|
-
|
960
|
-
|
954
|
+
self._analyze_token_variables(keyword_token)
|
955
|
+
self._analyze_statement_variables(node)
|
956
|
+
|
957
|
+
self._analyze_keyword_call(
|
958
|
+
node,
|
959
|
+
keyword_token,
|
960
|
+
[e for e in node.get_tokens(Token.ARGUMENT)],
|
961
|
+
)
|
962
|
+
|
963
|
+
if not self._current_testcase_or_keyword_name:
|
964
|
+
self._append_diagnostics(
|
961
965
|
range=range_from_node_or_token(node, node.get_token(Token.ASSIGN)),
|
962
966
|
message="Code is unreachable.",
|
963
967
|
severity=DiagnosticSeverity.HINT,
|
@@ -965,39 +969,49 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
965
969
|
code=Error.CODE_UNREACHABLE,
|
966
970
|
)
|
967
971
|
|
968
|
-
self.
|
972
|
+
self._analyze_assign_statement(node)
|
969
973
|
|
970
974
|
def visit_TestCase(self, node: TestCase) -> None: # noqa: N802
|
971
975
|
if not node.name:
|
972
976
|
name_token = node.header.get_token(Token.TESTCASE_NAME)
|
973
|
-
self.
|
977
|
+
self._append_diagnostics(
|
974
978
|
range=range_from_node_or_token(node, name_token),
|
975
979
|
message="Test case name cannot be empty.",
|
976
980
|
severity=DiagnosticSeverity.ERROR,
|
977
981
|
code=Error.TESTCASE_NAME_EMPTY,
|
978
982
|
)
|
979
983
|
|
980
|
-
self.
|
984
|
+
self._current_testcase_or_keyword_name = node.name
|
985
|
+
old_variables = self._variables
|
986
|
+
self._variables = self._variables.copy()
|
981
987
|
try:
|
982
988
|
self.generic_visit(node)
|
983
989
|
finally:
|
984
|
-
self.
|
985
|
-
self.
|
990
|
+
self._variables = old_variables
|
991
|
+
self._current_testcase_or_keyword_name = None
|
992
|
+
self._template = None
|
993
|
+
|
994
|
+
def visit_TestCaseName(self, node: TestCaseName) -> None: # noqa: N802
|
995
|
+
name_token = node.get_token(Token.TESTCASE_NAME)
|
996
|
+
if name_token is not None and name_token.value:
|
997
|
+
self._analyze_token_variables(name_token, DiagnosticSeverity.HINT)
|
986
998
|
|
987
999
|
def visit_Keyword(self, node: Keyword) -> None: # noqa: N802
|
988
1000
|
if node.name:
|
989
1001
|
name_token = node.header.get_token(Token.KEYWORD_NAME)
|
990
|
-
|
1002
|
+
self._current_keyword_doc = ModelHelper.get_keyword_definition_at_token(
|
1003
|
+
self._namespace.get_library_doc(), name_token
|
1004
|
+
)
|
991
1005
|
|
992
|
-
if
|
993
|
-
self._keyword_references[
|
1006
|
+
if self._current_keyword_doc is not None and self._current_keyword_doc not in self._keyword_references:
|
1007
|
+
self._keyword_references[self._current_keyword_doc] = set()
|
994
1008
|
|
995
1009
|
if (
|
996
1010
|
get_robot_version() < (6, 1)
|
997
1011
|
and is_embedded_keyword(node.name)
|
998
1012
|
and any(isinstance(v, Arguments) and len(v.values) > 0 for v in node.body)
|
999
1013
|
):
|
1000
|
-
self.
|
1014
|
+
self._append_diagnostics(
|
1001
1015
|
range=range_from_node_or_token(node, name_token),
|
1002
1016
|
message="Keyword cannot have both normal and embedded arguments.",
|
1003
1017
|
severity=DiagnosticSeverity.ERROR,
|
@@ -1005,18 +1019,237 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1005
1019
|
)
|
1006
1020
|
else:
|
1007
1021
|
name_token = node.header.get_token(Token.KEYWORD_NAME)
|
1008
|
-
self.
|
1022
|
+
self._append_diagnostics(
|
1009
1023
|
range=range_from_node_or_token(node, name_token),
|
1010
1024
|
message="Keyword name cannot be empty.",
|
1011
1025
|
severity=DiagnosticSeverity.ERROR,
|
1012
1026
|
code=Error.KEYWORD_NAME_EMPTY,
|
1013
1027
|
)
|
1014
1028
|
|
1015
|
-
self.
|
1029
|
+
self._current_testcase_or_keyword_name = node.name
|
1030
|
+
old_variables = self._variables
|
1031
|
+
self._variables = self._variables.copy()
|
1016
1032
|
try:
|
1033
|
+
arguments = next((v for v in node.body if isinstance(v, Arguments)), None)
|
1034
|
+
if arguments is not None:
|
1035
|
+
self._visit_Arguments(arguments)
|
1036
|
+
|
1017
1037
|
self.generic_visit(node)
|
1018
1038
|
finally:
|
1019
|
-
self.
|
1039
|
+
self._variables = old_variables
|
1040
|
+
self._current_testcase_or_keyword_name = None
|
1041
|
+
self._current_keyword_doc = None
|
1042
|
+
|
1043
|
+
def visit_KeywordName(self, node: KeywordName) -> None: # noqa: N802
|
1044
|
+
name_token = node.get_token(Token.KEYWORD_NAME)
|
1045
|
+
|
1046
|
+
if name_token is not None and name_token.value:
|
1047
|
+
|
1048
|
+
for variable_token in filter(
|
1049
|
+
lambda e: e.type == Token.VARIABLE,
|
1050
|
+
tokenize_variables(name_token, identifiers="$", ignore_errors=True),
|
1051
|
+
):
|
1052
|
+
if variable_token.value:
|
1053
|
+
match = search_variable(variable_token.value, "$", ignore_errors=True)
|
1054
|
+
if match.base is None:
|
1055
|
+
continue
|
1056
|
+
name = match.base.split(":", 1)[0]
|
1057
|
+
full_name = f"{match.identifier}{{{name}}}"
|
1058
|
+
var_token = strip_variable_token(variable_token)
|
1059
|
+
var_token.value = name
|
1060
|
+
arg_def = ArgumentDefinition(
|
1061
|
+
name=full_name,
|
1062
|
+
name_token=var_token,
|
1063
|
+
line_no=variable_token.lineno,
|
1064
|
+
col_offset=variable_token.col_offset,
|
1065
|
+
end_line_no=variable_token.lineno,
|
1066
|
+
end_col_offset=variable_token.end_col_offset,
|
1067
|
+
source=self._namespace.source,
|
1068
|
+
keyword_doc=self._current_keyword_doc,
|
1069
|
+
)
|
1070
|
+
|
1071
|
+
self._variables[arg_def.matcher] = arg_def
|
1072
|
+
self._variable_references[arg_def] = set()
|
1073
|
+
|
1074
|
+
def _get_variable_token(self, token: Token) -> Optional[Token]:
|
1075
|
+
return next(
|
1076
|
+
(
|
1077
|
+
v
|
1078
|
+
for v in itertools.dropwhile(
|
1079
|
+
lambda t: t.type in Token.NON_DATA_TOKENS,
|
1080
|
+
tokenize_variables(token, ignore_errors=True, extra_types={Token.VARIABLE}),
|
1081
|
+
)
|
1082
|
+
if v.type == Token.VARIABLE
|
1083
|
+
),
|
1084
|
+
None,
|
1085
|
+
)
|
1086
|
+
|
1087
|
+
def _visit_Arguments(self, node: Statement) -> None: # noqa: N802
|
1088
|
+
args: Dict[VariableMatcher, VariableDefinition] = {}
|
1089
|
+
|
1090
|
+
arguments = node.get_tokens(Token.ARGUMENT)
|
1091
|
+
|
1092
|
+
for argument_token in arguments:
|
1093
|
+
try:
|
1094
|
+
argument = self._get_variable_token(argument_token)
|
1095
|
+
|
1096
|
+
if argument is not None and argument.value != "@{}":
|
1097
|
+
if len(argument_token.value) > len(argument.value):
|
1098
|
+
self._analyze_token_variables(
|
1099
|
+
Token(
|
1100
|
+
argument_token.type,
|
1101
|
+
argument_token.value[len(argument.value) :],
|
1102
|
+
argument_token.lineno,
|
1103
|
+
argument_token.col_offset + len(argument.value),
|
1104
|
+
argument_token.error,
|
1105
|
+
)
|
1106
|
+
)
|
1107
|
+
|
1108
|
+
matcher = VariableMatcher(argument.value)
|
1109
|
+
|
1110
|
+
if matcher not in args:
|
1111
|
+
arg_def = ArgumentDefinition(
|
1112
|
+
name=argument.value,
|
1113
|
+
name_token=strip_variable_token(argument),
|
1114
|
+
line_no=argument.lineno,
|
1115
|
+
col_offset=argument.col_offset,
|
1116
|
+
end_line_no=argument.lineno,
|
1117
|
+
end_col_offset=argument.end_col_offset,
|
1118
|
+
source=self._namespace.source,
|
1119
|
+
keyword_doc=self._current_keyword_doc,
|
1120
|
+
)
|
1121
|
+
|
1122
|
+
args[matcher] = arg_def
|
1123
|
+
|
1124
|
+
self._variables[arg_def.matcher] = arg_def
|
1125
|
+
if arg_def not in self._variable_references:
|
1126
|
+
self._variable_references[arg_def] = set()
|
1127
|
+
else:
|
1128
|
+
self._variable_references[args[matcher]].add(
|
1129
|
+
Location(
|
1130
|
+
self._namespace.document_uri,
|
1131
|
+
range_from_token(strip_variable_token(argument)),
|
1132
|
+
)
|
1133
|
+
)
|
1134
|
+
|
1135
|
+
except VariableError:
|
1136
|
+
pass
|
1137
|
+
|
1138
|
+
def _analyze_assign_statement(self, node: Statement) -> None:
|
1139
|
+
for assign_token in node.get_tokens(Token.ASSIGN):
|
1140
|
+
variable_token = self._get_variable_token(assign_token)
|
1141
|
+
|
1142
|
+
try:
|
1143
|
+
if variable_token is not None:
|
1144
|
+
matcher = VariableMatcher(variable_token.value)
|
1145
|
+
existing_var = next(
|
1146
|
+
(
|
1147
|
+
v
|
1148
|
+
for k, v in self._variables.items()
|
1149
|
+
if k == matcher
|
1150
|
+
and v.type in [VariableDefinitionType.ARGUMENT, VariableDefinitionType.LOCAL_VARIABLE]
|
1151
|
+
),
|
1152
|
+
None,
|
1153
|
+
)
|
1154
|
+
if existing_var is None:
|
1155
|
+
var_def = LocalVariableDefinition(
|
1156
|
+
name=variable_token.value,
|
1157
|
+
name_token=strip_variable_token(variable_token),
|
1158
|
+
line_no=variable_token.lineno,
|
1159
|
+
col_offset=variable_token.col_offset,
|
1160
|
+
end_line_no=variable_token.lineno,
|
1161
|
+
end_col_offset=variable_token.end_col_offset,
|
1162
|
+
source=self._namespace.source,
|
1163
|
+
)
|
1164
|
+
self._variables[matcher] = var_def
|
1165
|
+
self._variable_references[var_def] = set()
|
1166
|
+
self._local_variable_assignments[var_def].add(var_def.range)
|
1167
|
+
else:
|
1168
|
+
self._variable_references[existing_var].add(
|
1169
|
+
Location(
|
1170
|
+
self._namespace.document_uri,
|
1171
|
+
range_from_token(strip_variable_token(variable_token)),
|
1172
|
+
)
|
1173
|
+
)
|
1174
|
+
|
1175
|
+
except VariableError:
|
1176
|
+
pass
|
1177
|
+
|
1178
|
+
def visit_InlineIfHeader(self, node: Statement) -> None: # noqa: N802
|
1179
|
+
self._analyze_statement_expression_variables(node)
|
1180
|
+
|
1181
|
+
self._analyze_assign_statement(node)
|
1182
|
+
|
1183
|
+
def visit_ForHeader(self, node: Statement) -> None: # noqa: N802
|
1184
|
+
self._analyze_statement_variables(node)
|
1185
|
+
|
1186
|
+
variables = node.get_tokens(Token.VARIABLE)
|
1187
|
+
for variable in variables:
|
1188
|
+
variable_token = self._get_variable_token(variable)
|
1189
|
+
if variable_token is not None:
|
1190
|
+
existing_var = self._find_variable(variable_token.value)
|
1191
|
+
|
1192
|
+
if existing_var is None or existing_var.type not in [
|
1193
|
+
VariableDefinitionType.ARGUMENT,
|
1194
|
+
VariableDefinitionType.LOCAL_VARIABLE,
|
1195
|
+
]:
|
1196
|
+
var_def = LocalVariableDefinition(
|
1197
|
+
name=variable_token.value,
|
1198
|
+
name_token=strip_variable_token(variable_token),
|
1199
|
+
line_no=variable_token.lineno,
|
1200
|
+
col_offset=variable_token.col_offset,
|
1201
|
+
end_line_no=variable_token.lineno,
|
1202
|
+
end_col_offset=variable_token.end_col_offset,
|
1203
|
+
source=self._namespace.source,
|
1204
|
+
)
|
1205
|
+
self._variables[var_def.matcher] = var_def
|
1206
|
+
self._variable_references[var_def] = set()
|
1207
|
+
else:
|
1208
|
+
if existing_var.type in [
|
1209
|
+
VariableDefinitionType.ARGUMENT,
|
1210
|
+
VariableDefinitionType.LOCAL_VARIABLE,
|
1211
|
+
]:
|
1212
|
+
self._variable_references[existing_var].add(
|
1213
|
+
Location(
|
1214
|
+
self._namespace.document_uri,
|
1215
|
+
range_from_token(strip_variable_token(variable_token)),
|
1216
|
+
)
|
1217
|
+
)
|
1218
|
+
|
1219
|
+
def visit_ExceptHeader(self, node: Statement) -> None: # noqa: N802
|
1220
|
+
self._analyze_statement_variables(node)
|
1221
|
+
self._analyze_option_token_variables(node)
|
1222
|
+
|
1223
|
+
variable_token = node.get_token(Token.VARIABLE)
|
1224
|
+
|
1225
|
+
if variable_token is not None and is_scalar_assign(variable_token.value):
|
1226
|
+
try:
|
1227
|
+
if variable_token is not None:
|
1228
|
+
matcher = VariableMatcher(variable_token.value)
|
1229
|
+
if (
|
1230
|
+
next(
|
1231
|
+
(
|
1232
|
+
k
|
1233
|
+
for k, v in self._variables.items()
|
1234
|
+
if k == matcher
|
1235
|
+
and v.type in [VariableDefinitionType.ARGUMENT, VariableDefinitionType.LOCAL_VARIABLE]
|
1236
|
+
),
|
1237
|
+
None,
|
1238
|
+
)
|
1239
|
+
is None
|
1240
|
+
):
|
1241
|
+
self._variables[matcher] = LocalVariableDefinition(
|
1242
|
+
name=variable_token.value,
|
1243
|
+
name_token=strip_variable_token(variable_token),
|
1244
|
+
line_no=variable_token.lineno,
|
1245
|
+
col_offset=variable_token.col_offset,
|
1246
|
+
end_line_no=variable_token.lineno,
|
1247
|
+
end_col_offset=variable_token.end_col_offset,
|
1248
|
+
source=self._namespace.source,
|
1249
|
+
)
|
1250
|
+
|
1251
|
+
except VariableError:
|
1252
|
+
pass
|
1020
1253
|
|
1021
1254
|
def _format_template(self, template: str, arguments: Tuple[str, ...]) -> Tuple[str, Tuple[str, ...]]:
|
1022
1255
|
if get_robot_version() < (7, 0):
|
@@ -1041,14 +1274,16 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1041
1274
|
return "".join(temp), ()
|
1042
1275
|
|
1043
1276
|
def visit_TemplateArguments(self, node: TemplateArguments) -> None: # noqa: N802
|
1044
|
-
|
1277
|
+
self._analyze_statement_variables(node)
|
1278
|
+
|
1279
|
+
template = self._template or self._test_template
|
1045
1280
|
if template is not None and template.value is not None and template.value.upper() not in ("", "NONE"):
|
1046
1281
|
argument_tokens = node.get_tokens(Token.ARGUMENT)
|
1047
1282
|
args = tuple(t.value for t in argument_tokens)
|
1048
1283
|
keyword = template.value
|
1049
1284
|
keyword, args = self._format_template(keyword, args)
|
1050
1285
|
|
1051
|
-
result = self.
|
1286
|
+
result = self._finder.find_keyword(keyword)
|
1052
1287
|
if result is not None:
|
1053
1288
|
try:
|
1054
1289
|
if result.arguments_spec is not None:
|
@@ -1061,15 +1296,15 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1061
1296
|
except (SystemExit, KeyboardInterrupt):
|
1062
1297
|
raise
|
1063
1298
|
except BaseException as e:
|
1064
|
-
self.
|
1299
|
+
self._append_diagnostics(
|
1065
1300
|
range=range_from_node(node, skip_non_data=True),
|
1066
1301
|
message=str(e),
|
1067
1302
|
severity=DiagnosticSeverity.ERROR,
|
1068
1303
|
code=type(e).__qualname__,
|
1069
1304
|
)
|
1070
1305
|
|
1071
|
-
for d in self.
|
1072
|
-
self.
|
1306
|
+
for d in self._finder.diagnostics:
|
1307
|
+
self._append_diagnostics(
|
1073
1308
|
range=range_from_node(node, skip_non_data=True),
|
1074
1309
|
message=d.message,
|
1075
1310
|
severity=d.severity,
|
@@ -1078,11 +1313,16 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1078
1313
|
|
1079
1314
|
self.generic_visit(node)
|
1080
1315
|
|
1316
|
+
def visit_DefaultTags(self, node: Statement) -> None: # noqa: N802
|
1317
|
+
self._analyze_statement_variables(node, DiagnosticSeverity.HINT)
|
1318
|
+
|
1081
1319
|
def visit_ForceTags(self, node: Statement) -> None: # noqa: N802
|
1320
|
+
self._analyze_statement_variables(node, DiagnosticSeverity.HINT)
|
1321
|
+
|
1082
1322
|
if get_robot_version() >= (6, 0):
|
1083
1323
|
tag = node.get_token(Token.FORCE_TAGS)
|
1084
1324
|
if tag is not None and tag.value.upper() == "FORCE TAGS":
|
1085
|
-
self.
|
1325
|
+
self._append_diagnostics(
|
1086
1326
|
range=range_from_node_or_token(node, tag),
|
1087
1327
|
message="`Force Tags` is deprecated in favour of new `Test Tags` setting.",
|
1088
1328
|
severity=DiagnosticSeverity.INFORMATION,
|
@@ -1091,10 +1331,12 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1091
1331
|
)
|
1092
1332
|
|
1093
1333
|
def visit_TestTags(self, node: Statement) -> None: # noqa: N802
|
1334
|
+
self._analyze_statement_variables(node, DiagnosticSeverity.HINT)
|
1335
|
+
|
1094
1336
|
if get_robot_version() >= (6, 0):
|
1095
1337
|
tag = node.get_token(Token.FORCE_TAGS)
|
1096
1338
|
if tag is not None and tag.value.upper() == "FORCE TAGS":
|
1097
|
-
self.
|
1339
|
+
self._append_diagnostics(
|
1098
1340
|
range=range_from_node_or_token(node, tag),
|
1099
1341
|
message="`Force Tags` is deprecated in favour of new `Test Tags` setting.",
|
1100
1342
|
severity=DiagnosticSeverity.INFORMATION,
|
@@ -1102,11 +1344,28 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1102
1344
|
code=Error.DEPRECATED_FORCE_TAG,
|
1103
1345
|
)
|
1104
1346
|
|
1347
|
+
def visit_Arguments(self, node: Statement) -> None: # noqa: N802
|
1348
|
+
pass
|
1349
|
+
|
1350
|
+
def visit_DocumentationOrMetadata(self, node: Statement) -> None: # noqa: N802
|
1351
|
+
self._visit_settings_statement(node, DiagnosticSeverity.HINT)
|
1352
|
+
|
1353
|
+
def visit_Timeout(self, node: Statement) -> None: # noqa: N802
|
1354
|
+
self._analyze_statement_variables(node, DiagnosticSeverity.HINT)
|
1355
|
+
|
1356
|
+
def visit_SingleValue(self, node: Statement) -> None: # noqa: N802
|
1357
|
+
self._visit_settings_statement(node, DiagnosticSeverity.HINT)
|
1358
|
+
|
1359
|
+
def visit_MultiValue(self, node: Statement) -> None: # noqa: N802
|
1360
|
+
self._visit_settings_statement(node, DiagnosticSeverity.HINT)
|
1361
|
+
|
1105
1362
|
def visit_Tags(self, node: Statement) -> None: # noqa: N802
|
1363
|
+
self._visit_settings_statement(node, DiagnosticSeverity.HINT)
|
1364
|
+
|
1106
1365
|
if (6, 0) < get_robot_version() < (7, 0):
|
1107
1366
|
for tag in node.get_tokens(Token.ARGUMENT):
|
1108
1367
|
if tag.value and tag.value.startswith("-"):
|
1109
|
-
self.
|
1368
|
+
self._append_diagnostics(
|
1110
1369
|
range=range_from_node_or_token(node, tag),
|
1111
1370
|
message=f"Settings tags starting with a hyphen using the '[Tags]' setting "
|
1112
1371
|
f"is deprecated. In Robot Framework 7.0 this syntax will be used "
|
@@ -1118,19 +1377,21 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1118
1377
|
)
|
1119
1378
|
|
1120
1379
|
def visit_SectionHeader(self, node: Statement) -> None: # noqa: N802
|
1380
|
+
self._analyze_statement_variables(node)
|
1381
|
+
|
1121
1382
|
if get_robot_version() >= (7, 0):
|
1122
1383
|
token = node.get_token(*Token.HEADER_TOKENS)
|
1123
1384
|
if not token.error:
|
1124
1385
|
return
|
1125
1386
|
if token.type == Token.INVALID_HEADER:
|
1126
|
-
self.
|
1387
|
+
self._append_diagnostics(
|
1127
1388
|
range=range_from_node_or_token(node, token),
|
1128
1389
|
message=token.error,
|
1129
1390
|
severity=DiagnosticSeverity.ERROR,
|
1130
1391
|
code=Error.INVALID_HEADER,
|
1131
1392
|
)
|
1132
1393
|
else:
|
1133
|
-
self.
|
1394
|
+
self._append_diagnostics(
|
1134
1395
|
range=range_from_node_or_token(node, token),
|
1135
1396
|
message=token.error,
|
1136
1397
|
severity=DiagnosticSeverity.WARNING,
|
@@ -1139,10 +1400,12 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1139
1400
|
)
|
1140
1401
|
|
1141
1402
|
def visit_ReturnSetting(self, node: Statement) -> None: # noqa: N802
|
1403
|
+
self._analyze_statement_variables(node)
|
1404
|
+
|
1142
1405
|
if get_robot_version() >= (7, 0):
|
1143
1406
|
token = node.get_token(Token.RETURN_SETTING)
|
1144
1407
|
if token is not None and token.error:
|
1145
|
-
self.
|
1408
|
+
self._append_diagnostics(
|
1146
1409
|
range=range_from_node_or_token(node, token),
|
1147
1410
|
message=token.error,
|
1148
1411
|
severity=DiagnosticSeverity.WARNING,
|
@@ -1152,7 +1415,7 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1152
1415
|
|
1153
1416
|
def _check_import_name(self, value: Optional[str], node: ast.AST, type: str) -> None:
|
1154
1417
|
if not value:
|
1155
|
-
self.
|
1418
|
+
self._append_diagnostics(
|
1156
1419
|
range=range_from_node(node),
|
1157
1420
|
message=f"{type} setting requires value.",
|
1158
1421
|
severity=DiagnosticSeverity.ERROR,
|
@@ -1167,15 +1430,18 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1167
1430
|
if name_token is None:
|
1168
1431
|
return
|
1169
1432
|
|
1433
|
+
self._analyze_token_variables(name_token)
|
1434
|
+
self._analyze_statement_variables(node)
|
1435
|
+
|
1170
1436
|
found = False
|
1171
|
-
entries = self.
|
1172
|
-
if entries and self.
|
1437
|
+
entries = self._namespace.get_import_entries()
|
1438
|
+
if entries and self._namespace.document:
|
1173
1439
|
for v in entries.values():
|
1174
|
-
if v.import_source == self.
|
1440
|
+
if v.import_source == self._namespace.source and v.import_range == range_from_token(name_token):
|
1175
1441
|
for k in self._namespace_references:
|
1176
1442
|
if type(k) is type(v) and k.library_doc.source_or_origin == v.library_doc.source_or_origin:
|
1177
1443
|
self._namespace_references[k].add(
|
1178
|
-
Location(self.
|
1444
|
+
Location(self._namespace.document.document_uri, v.import_range)
|
1179
1445
|
)
|
1180
1446
|
found = True
|
1181
1447
|
break
|
@@ -1185,6 +1451,7 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1185
1451
|
break
|
1186
1452
|
|
1187
1453
|
def visit_ResourceImport(self, node: ResourceImport) -> None: # noqa: N802
|
1454
|
+
|
1188
1455
|
if get_robot_version() >= (6, 1):
|
1189
1456
|
self._check_import_name(node.name, node, "Resource")
|
1190
1457
|
|
@@ -1192,15 +1459,18 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1192
1459
|
if name_token is None:
|
1193
1460
|
return
|
1194
1461
|
|
1462
|
+
self._analyze_token_variables(name_token)
|
1463
|
+
self._analyze_statement_variables(node)
|
1464
|
+
|
1195
1465
|
found = False
|
1196
|
-
entries = self.
|
1197
|
-
if entries and self.
|
1466
|
+
entries = self._namespace.get_import_entries()
|
1467
|
+
if entries and self._namespace.document:
|
1198
1468
|
for v in entries.values():
|
1199
|
-
if v.import_source == self.
|
1469
|
+
if v.import_source == self._namespace.source and v.import_range == range_from_token(name_token):
|
1200
1470
|
for k in self._namespace_references:
|
1201
1471
|
if type(k) is type(v) and k.library_doc.source_or_origin == v.library_doc.source_or_origin:
|
1202
1472
|
self._namespace_references[k].add(
|
1203
|
-
Location(self.
|
1473
|
+
Location(self._namespace.document.document_uri, v.import_range)
|
1204
1474
|
)
|
1205
1475
|
found = True
|
1206
1476
|
break
|
@@ -1217,15 +1487,18 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1217
1487
|
if name_token is None:
|
1218
1488
|
return
|
1219
1489
|
|
1490
|
+
self._analyze_token_variables(name_token)
|
1491
|
+
self._analyze_statement_variables(node)
|
1492
|
+
|
1220
1493
|
found = False
|
1221
|
-
entries = self.
|
1222
|
-
if entries and self.
|
1494
|
+
entries = self._namespace.get_import_entries()
|
1495
|
+
if entries and self._namespace.document:
|
1223
1496
|
for v in entries.values():
|
1224
|
-
if v.import_source == self.
|
1497
|
+
if v.import_source == self._namespace.source and v.import_range == range_from_token(name_token):
|
1225
1498
|
for k in self._namespace_references:
|
1226
1499
|
if type(k) is type(v) and k.library_doc.source_or_origin == v.library_doc.source_or_origin:
|
1227
1500
|
self._namespace_references[k].add(
|
1228
|
-
Location(self.
|
1501
|
+
Location(self._namespace.document.document_uri, v.import_range)
|
1229
1502
|
)
|
1230
1503
|
found = True
|
1231
1504
|
break
|
@@ -1233,3 +1506,220 @@ class NamespaceAnalyzer(Visitor, ModelHelper):
|
|
1233
1506
|
if v not in self._namespace_references:
|
1234
1507
|
self._namespace_references[v] = set()
|
1235
1508
|
break
|
1509
|
+
|
1510
|
+
def visit_WhileHeader(self, node: Statement) -> None: # noqa: N802
|
1511
|
+
self._analyze_statement_expression_variables(node)
|
1512
|
+
|
1513
|
+
self._analyze_option_token_variables(node)
|
1514
|
+
|
1515
|
+
def _analyze_option_token_variables(self, node: Statement) -> None:
|
1516
|
+
for token in node.get_tokens(Token.OPTION):
|
1517
|
+
if token.value and "=" in token.value:
|
1518
|
+
name, value = token.value.split("=", 1)
|
1519
|
+
|
1520
|
+
value_token = Token(token.type, value, token.lineno, token.col_offset + len(name) + 1)
|
1521
|
+
self._analyze_token_variables(value_token)
|
1522
|
+
|
1523
|
+
def visit_IfHeader(self, node: Statement) -> None: # noqa: N802
|
1524
|
+
self._analyze_statement_expression_variables(node)
|
1525
|
+
|
1526
|
+
def visit_IfElseHeader(self, node: Statement) -> None: # noqa: N802
|
1527
|
+
self._analyze_statement_expression_variables(node)
|
1528
|
+
|
1529
|
+
def _find_variable(self, name: str) -> Optional[VariableDefinition]:
|
1530
|
+
if name[:2] == "%{" and name[-1] == "}":
|
1531
|
+
var_name, _, default_value = name[2:-1].partition("=")
|
1532
|
+
return EnvironmentVariableDefinition(
|
1533
|
+
0,
|
1534
|
+
0,
|
1535
|
+
0,
|
1536
|
+
0,
|
1537
|
+
"",
|
1538
|
+
f"%{{{var_name}}}",
|
1539
|
+
None,
|
1540
|
+
default_value=default_value or None,
|
1541
|
+
)
|
1542
|
+
|
1543
|
+
vars = self._suite_variables if self._in_setting else self._variables
|
1544
|
+
|
1545
|
+
matcher = VariableMatcher(name)
|
1546
|
+
|
1547
|
+
return vars.get(matcher, None)
|
1548
|
+
|
1549
|
+
def _is_number(self, name: str) -> bool:
|
1550
|
+
if name.startswith("$"):
|
1551
|
+
finder = NumberFinder()
|
1552
|
+
return bool(finder.find(name) != NOT_FOUND)
|
1553
|
+
return False
|
1554
|
+
|
1555
|
+
def _iter_variables_token(
|
1556
|
+
self,
|
1557
|
+
to: Token,
|
1558
|
+
) -> Iterator[Tuple[Token, Optional[VariableDefinition]]]:
|
1559
|
+
|
1560
|
+
def exception_handler(e: BaseException, t: Token) -> None:
|
1561
|
+
self._append_diagnostics(
|
1562
|
+
range_from_token(t),
|
1563
|
+
str(e),
|
1564
|
+
severity=DiagnosticSeverity.ERROR,
|
1565
|
+
code=Error.TOKEN_ERROR,
|
1566
|
+
)
|
1567
|
+
|
1568
|
+
for sub_token in ModelHelper.tokenize_variables(to, ignore_errors=True, exception_handler=exception_handler):
|
1569
|
+
if sub_token.type == Token.VARIABLE:
|
1570
|
+
base = sub_token.value[2:-1]
|
1571
|
+
if base and not (base[0] == "{" and base[-1] == "}"):
|
1572
|
+
yield sub_token, None
|
1573
|
+
elif base:
|
1574
|
+
for v in self._iter_expression_variables_from_token(
|
1575
|
+
Token(
|
1576
|
+
sub_token.type,
|
1577
|
+
base[1:-1],
|
1578
|
+
sub_token.lineno,
|
1579
|
+
sub_token.col_offset + 3,
|
1580
|
+
sub_token.error,
|
1581
|
+
)
|
1582
|
+
):
|
1583
|
+
yield v
|
1584
|
+
elif base == "":
|
1585
|
+
yield (
|
1586
|
+
sub_token,
|
1587
|
+
VariableNotFoundDefinition(
|
1588
|
+
sub_token.lineno,
|
1589
|
+
sub_token.col_offset,
|
1590
|
+
sub_token.lineno,
|
1591
|
+
sub_token.end_col_offset,
|
1592
|
+
self._namespace.source,
|
1593
|
+
sub_token.value,
|
1594
|
+
strip_variable_token(sub_token),
|
1595
|
+
),
|
1596
|
+
)
|
1597
|
+
continue
|
1598
|
+
|
1599
|
+
if contains_variable(base, "$@&%"):
|
1600
|
+
for sub_token_or_var, var_def in self._iter_variables_token(
|
1601
|
+
Token(to.type, base, sub_token.lineno, sub_token.col_offset + 2)
|
1602
|
+
):
|
1603
|
+
if var_def is None:
|
1604
|
+
if sub_token_or_var.type == Token.VARIABLE:
|
1605
|
+
yield sub_token_or_var, var_def
|
1606
|
+
else:
|
1607
|
+
yield sub_token_or_var, var_def
|
1608
|
+
|
1609
|
+
def _iter_variables_from_token(self, token: Token) -> Iterator[Tuple[Token, VariableDefinition]]:
|
1610
|
+
|
1611
|
+
if token.type == Token.VARIABLE and token.value.endswith("="):
|
1612
|
+
match = search_variable(token.value, ignore_errors=True)
|
1613
|
+
if not match.is_assign(allow_assign_mark=True):
|
1614
|
+
return
|
1615
|
+
|
1616
|
+
token = Token(
|
1617
|
+
token.type,
|
1618
|
+
token.value[:-1].strip(),
|
1619
|
+
token.lineno,
|
1620
|
+
token.col_offset,
|
1621
|
+
token.error,
|
1622
|
+
)
|
1623
|
+
|
1624
|
+
for var_token, var_def in self._iter_variables_token(token):
|
1625
|
+
if var_def is None:
|
1626
|
+
name = var_token.value
|
1627
|
+
var = self._find_variable(name)
|
1628
|
+
if var is not None:
|
1629
|
+
yield strip_variable_token(var_token), var
|
1630
|
+
continue
|
1631
|
+
|
1632
|
+
if self._is_number(var_token.value):
|
1633
|
+
continue
|
1634
|
+
|
1635
|
+
if (
|
1636
|
+
var_token.type == Token.VARIABLE
|
1637
|
+
and var_token.value[:1] in "$@&%"
|
1638
|
+
and var_token.value[1:2] == "{"
|
1639
|
+
and var_token.value[-1:] == "}"
|
1640
|
+
):
|
1641
|
+
match = ModelHelper.match_extended.match(name[2:-1])
|
1642
|
+
if match is not None:
|
1643
|
+
base_name, _ = match.groups()
|
1644
|
+
name = f"{name[0]}{{{base_name.strip()}}}"
|
1645
|
+
var = self._find_variable(name)
|
1646
|
+
sub_sub_token = Token(
|
1647
|
+
var_token.type,
|
1648
|
+
name,
|
1649
|
+
var_token.lineno,
|
1650
|
+
var_token.col_offset,
|
1651
|
+
)
|
1652
|
+
if var is not None:
|
1653
|
+
yield strip_variable_token(sub_sub_token), var
|
1654
|
+
continue
|
1655
|
+
if self._is_number(name):
|
1656
|
+
continue
|
1657
|
+
else:
|
1658
|
+
if contains_variable(var_token.value[2:-1]):
|
1659
|
+
continue
|
1660
|
+
else:
|
1661
|
+
yield (
|
1662
|
+
strip_variable_token(sub_sub_token),
|
1663
|
+
VariableNotFoundDefinition(
|
1664
|
+
sub_sub_token.lineno,
|
1665
|
+
sub_sub_token.col_offset,
|
1666
|
+
sub_sub_token.lineno,
|
1667
|
+
sub_sub_token.end_col_offset,
|
1668
|
+
self._namespace.source,
|
1669
|
+
name,
|
1670
|
+
sub_sub_token,
|
1671
|
+
),
|
1672
|
+
)
|
1673
|
+
|
1674
|
+
yield (
|
1675
|
+
strip_variable_token(var_token),
|
1676
|
+
VariableNotFoundDefinition(
|
1677
|
+
var_token.lineno,
|
1678
|
+
var_token.col_offset,
|
1679
|
+
var_token.lineno,
|
1680
|
+
var_token.end_col_offset,
|
1681
|
+
self._namespace.source,
|
1682
|
+
var_token.value,
|
1683
|
+
var_token,
|
1684
|
+
),
|
1685
|
+
)
|
1686
|
+
else:
|
1687
|
+
yield var_token, var_def
|
1688
|
+
|
1689
|
+
def _iter_expression_variables_from_token(
|
1690
|
+
self,
|
1691
|
+
expression: Token,
|
1692
|
+
) -> Iterator[Tuple[Token, VariableDefinition]]:
|
1693
|
+
variable_started = False
|
1694
|
+
try:
|
1695
|
+
for toknum, tokval, (_, tokcol), _, _ in generate_tokens(StringIO(expression.value).readline):
|
1696
|
+
if variable_started:
|
1697
|
+
if toknum == python_token.NAME:
|
1698
|
+
var = self._find_variable(f"${{{tokval}}}")
|
1699
|
+
sub_token = Token(
|
1700
|
+
expression.type,
|
1701
|
+
tokval,
|
1702
|
+
expression.lineno,
|
1703
|
+
expression.col_offset + tokcol,
|
1704
|
+
expression.error,
|
1705
|
+
)
|
1706
|
+
if var is not None:
|
1707
|
+
yield sub_token, var
|
1708
|
+
else:
|
1709
|
+
yield (
|
1710
|
+
sub_token,
|
1711
|
+
VariableNotFoundDefinition(
|
1712
|
+
sub_token.lineno,
|
1713
|
+
sub_token.col_offset,
|
1714
|
+
sub_token.lineno,
|
1715
|
+
sub_token.end_col_offset,
|
1716
|
+
self._namespace.source,
|
1717
|
+
f"${{{tokval}}}",
|
1718
|
+
sub_token,
|
1719
|
+
),
|
1720
|
+
)
|
1721
|
+
variable_started = False
|
1722
|
+
if tokval == "$":
|
1723
|
+
variable_started = True
|
1724
|
+
except TokenError:
|
1725
|
+
pass
|