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.
@@ -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 typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Type, Union
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
- import robot.parsing.model.statements
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.search import contains_variable, search_variable
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 .library_doc import (
67
- KeywordDoc,
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
- from .namespace import KeywordFinder, Namespace
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
- class NamespaceAnalyzer(Visitor, ModelHelper):
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.model = model
124
- self.namespace = namespace
125
- self.finder = finder
105
+ self._model = model
106
+ self._namespace = namespace
107
+ self._finder = finder
126
108
 
127
- self.current_testcase_or_keyword_name: Optional[str] = None
128
- self.test_template: Optional[TestTemplate] = None
129
- self.template: Optional[Template] = None
130
- self.node_stack: List[ast.AST] = []
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
- self.visit(self.model)
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 yield_argument_name_and_rest(self, node: ast.AST, token: Token) -> Iterator[Token]:
152
- if isinstance(node, Arguments) and token.type == Token.ARGUMENT:
153
- argument = next(
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 visit_Variable(self, node: Variable) -> None: # noqa: N802
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
- r = range_from_token(
200
- strip_variable_token(
201
- Token(
202
- name_token.type,
203
- name,
204
- name_token.lineno,
205
- name_token.col_offset,
206
- name_token.error,
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 = next(
212
- (
213
- v
214
- for v in self.namespace.get_own_variables()
215
- if v.name_token is not None and range_from_token(v.name_token) == r
216
- ),
217
- None,
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
- if var_def is None:
221
- return
212
+ add_to_references = True
213
+ first_overidden_reference: Optional[VariableDefinition] = None
214
+ if existing_var is not None:
222
215
 
223
- cmd_line_var = self.namespace.find_variable(
224
- name,
225
- skip_commandline_variables=False,
226
- position=r.start,
227
- ignore_error=True,
228
- )
229
- if isinstance(cmd_line_var, CommandLineVariableDefinition):
230
- if self.namespace.document is not None:
231
- self._variable_references[cmd_line_var].add(Location(self.namespace.document.document_uri, r))
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
- if var_def not in self._variable_references:
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
- def visit_Var(self, node: Statement) -> None: # noqa: N802
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
- name = name_token.value
304
+ def visit_Var(self, node: Statement) -> None: # noqa: N802
305
+ self._analyze_statement_variables(node)
242
306
 
243
- if name is not None:
244
- match = search_variable(name, ignore_errors=True)
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
- if name.endswith("="):
249
- name = name[:-1].rstrip()
311
+ try:
312
+ var_name = variable.value
313
+ if var_name.endswith("="):
314
+ var_name = var_name[:-1].rstrip()
250
315
 
251
- r = range_from_token(
252
- strip_variable_token(
253
- Token(
254
- name_token.type,
255
- name,
256
- name_token.lineno,
257
- name_token.col_offset,
258
- name_token.error,
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
- var_def = self.namespace.find_variable(
264
- name,
265
- skip_commandline_variables=False,
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
- if self.namespace.document is not None:
276
- self._variable_references[var_def] = set()
346
+ existing_var = self._variables[var.matcher]
277
347
 
278
- def generic_visit(self, node: ast.AST) -> None:
279
- check_current_task_canceled()
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
- super().generic_visit(node)
353
+ except VariableError:
354
+ pass
282
355
 
283
- if get_robot_version() < (7, 0):
284
- variable_statements: Tuple[Type[Any], ...] = (Variable,)
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 visit(self, node: ast.AST) -> None:
289
- check_current_task_canceled()
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
- self.node_stack.append(node)
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
- in_setting = isinstance(node, IN_SETTING_TYPES)
378
+ self._analyze_statement_variables(node, severity)
379
+ finally:
380
+ self._in_setting = False
294
381
 
295
- severity = (
296
- DiagnosticSeverity.HINT if isinstance(node, VARIABLE_NOT_FOUND_HINT_TYPES) else DiagnosticSeverity.ERROR
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
- if isinstance(node, KeywordCall) and node.keyword:
300
- kw_doc = self.finder.find_keyword(node.keyword, raise_keyword_error=False)
301
- if kw_doc is not None and kw_doc.longname == "BuiltIn.Comment":
302
- severity = DiagnosticSeverity.HINT
303
-
304
- if isinstance(node, Statement) and not isinstance(node, (TestTemplate, Template)):
305
- for token1 in (
306
- t
307
- for t in node.tokens
308
- if not (isinstance(node, self.variable_statements) and t.type == Token.VARIABLE)
309
- and t.error is None
310
- and contains_variable(t.value, "$@&%")
311
- ):
312
- for token in self.yield_argument_name_and_rest(node, token1):
313
- if isinstance(node, Arguments) and token.value == "@{}":
314
- continue
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
- for var_token, var in self.iter_variables_from_token(
317
- token,
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
- if self.namespace.document is not None:
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
- if (
398
- isinstance(node, Statement)
399
- and isinstance(node, self.get_expression_statement_types())
400
- and (token := node.get_token(Token.ARGUMENT)) is not None
401
- ):
402
- for var_token, var in self.iter_expression_variables_from_token(
403
- token,
404
- self.namespace,
405
- self.node_stack,
406
- range_from_token(token).start,
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
- source=DIAGNOSTICS_SOURCE_NAME,
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.node_stack = self.node_stack[:-1]
448
+ self._node_stack.pop()
448
449
 
449
- def append_diagnostics(
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
- analyse_run_keywords: bool = True,
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
- if (
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
- kw_range = range_from_token(keyword_token)
557
+ if result is None:
558
+ keyword_token = ModelHelper.strip_bdd_prefix(self._namespace, keyword_token)
503
559
 
504
- lib_entry = None
505
- lib_range = None
506
- kw_namespace = None
560
+ result = self._finder.find_keyword(keyword, raise_keyword_error=False)
507
561
 
508
- result = self.finder.find_keyword(keyword, raise_keyword_error=False)
562
+ kw_range = range_from_token(keyword_token)
509
563
 
510
- if keyword is not None:
564
+ if keyword:
511
565
  (
512
566
  lib_entry,
513
567
  kw_namespace,
514
- ) = self.get_namespace_info_from_keyword_token(self.namespace, keyword_token)
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
- if self.namespace.document is not None:
535
- entries = [lib_entry]
536
- if self.finder.multiple_keywords_result is not None:
537
- entries = next(
538
- (v for k, v in (self.namespace.get_namespaces()).items() if k == kw_namespace),
539
- entries,
540
- )
541
- for entry in entries:
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.finder.diagnostics:
546
- self.append_diagnostics(
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.namespace.document is not None and self.finder.multiple_keywords_result is not None:
555
- for d in self.finder.multiple_keywords_result:
556
- self._keyword_references[d].add(Location(self.namespace.document.document_uri, kw_range))
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
- if self.namespace.document is not None:
559
- self._keyword_references[result].add(Location(self.namespace.document.document_uri, kw_range))
611
+
612
+ self._keyword_references[result].add(Location(self._namespace.document_uri, kw_range))
560
613
 
561
614
  if result.errors:
562
- self.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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.namespace.source != result.source:
619
- self.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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 self.namespace.document is not None and result is not None:
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
- for (
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.namespace.document.document_uri,
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 analyse_run_keywords:
733
- self._analyse_run_keyword(result, node, argument_tokens)
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 _analyse_run_keyword(
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.append_diagnostics(
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.finder.find_keyword(argument_tokens[1].value)
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._analyse_run_keyword(result, node, argument_tokens)
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
- analyse_run_keywords=False,
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
- analyse_run_keywords=False,
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._analyse_run_keyword(result, node, argument_tokens)
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
- analyse_run_keywords=False,
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._analyse_run_keyword(result, node, argument_tokens)
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 is not None
890
- and keyword_token.value is not None
891
- and keyword_token.value.upper() not in ("", "NONE")
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
- analyse_run_keywords=False,
920
+ analyze_run_keywords=False,
917
921
  allow_variables=True,
918
922
  )
919
923
 
920
- self.test_template = node
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
- analyse_run_keywords=False,
937
+ analyze_run_keywords=False,
936
938
  allow_variables=True,
937
939
  )
938
- self.template = node
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.append_diagnostics(
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
- else:
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
- if not self.current_testcase_or_keyword_name:
960
- self.append_diagnostics(
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.generic_visit(node)
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.append_diagnostics(
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.current_testcase_or_keyword_name = node.name
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.current_testcase_or_keyword_name = None
985
- self.template = None
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
- kw_doc = self.get_keyword_definition_at_token(self.namespace.get_library_doc(), name_token)
1002
+ self._current_keyword_doc = ModelHelper.get_keyword_definition_at_token(
1003
+ self._namespace.get_library_doc(), name_token
1004
+ )
991
1005
 
992
- if kw_doc is not None and kw_doc not in self._keyword_references:
993
- self._keyword_references[kw_doc] = set()
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.append_diagnostics(
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.append_diagnostics(
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.current_testcase_or_keyword_name = node.name
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.current_testcase_or_keyword_name = None
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
- template = self.template or self.test_template
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.finder.find_keyword(keyword)
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.append_diagnostics(
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.finder.diagnostics:
1072
- self.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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.append_diagnostics(
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.namespace.get_import_entries()
1172
- if entries and self.namespace.document:
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.namespace.source and v.import_range == range_from_token(name_token):
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.namespace.document.document_uri, v.import_range)
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.namespace.get_import_entries()
1197
- if entries and self.namespace.document:
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.namespace.source and v.import_range == range_from_token(name_token):
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.namespace.document.document_uri, v.import_range)
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.namespace.get_import_entries()
1222
- if entries and self.namespace.document:
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.namespace.source and v.import_range == range_from_token(name_token):
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.namespace.document.document_uri, v.import_range)
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