robotcode-robot 0.93.1__py3-none-any.whl → 0.94.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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