robotcode-robot 0.93.0__py3-none-any.whl → 0.94.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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 +79 -37
- robotcode/robot/diagnostics/keyword_finder.py +458 -0
- robotcode/robot/diagnostics/library_doc.py +37 -19
- 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.0.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.0.dist-info/RECORD +0 -30
- {robotcode_robot-0.93.0.dist-info → robotcode_robot-0.94.0.dist-info}/WHEEL +0 -0
- {robotcode_robot-0.93.0.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
|