Skip to content

Commit 886ec80

Browse files
committed
implement analyse variables, partially #36
1 parent 2aa6843 commit 886ec80

File tree

4 files changed

+116
-98
lines changed

4 files changed

+116
-98
lines changed

robotcode/language_server/robotframework/diagnostics/analyzer.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
from ...common.text_document import TextDocument
2020
from ..parts.model_helper import ModelHelperMixin
2121
from ..utils.ast import (
22+
HasTokens,
2223
Token,
2324
is_not_variable_token,
2425
range_from_node_or_token,
2526
range_from_token,
2627
)
2728
from ..utils.async_ast import AsyncVisitor
29+
from .entities import VariableNotFoundDefinition
2830
from .library_doc import KeywordDoc, is_embedded_keyword
2931
from .namespace import DIAGNOSTICS_SOURCE_NAME, KeywordFinder, Namespace
3032

@@ -37,11 +39,12 @@ def __init__(self, model: ast.AST, namespace: Namespace) -> None:
3739
from robot.parsing.model.statements import Template, TestTemplate
3840

3941
self.model = model
40-
self._namespace = namespace
42+
self.namespace = namespace
4143
self.current_testcase_or_keyword_name: Optional[str] = None
42-
self.finder = KeywordFinder(self._namespace)
44+
self.finder = KeywordFinder(self.namespace)
4345
self.test_template: Optional[TestTemplate] = None
4446
self.template: Optional[Template] = None
47+
self.node_stack: List[ast.AST] = []
4548

4649
async def run(self) -> List[Diagnostic]:
4750
self._results: List[Diagnostic] = []
@@ -50,7 +53,32 @@ async def run(self) -> List[Diagnostic]:
5053
return self._results
5154

5255
async def visit(self, node: ast.AST) -> None:
53-
await super().visit(node)
56+
from robot.variables.search import contains_variable
57+
58+
self.node_stack.append(node)
59+
try:
60+
if isinstance(node, HasTokens):
61+
for token in (t for t in node.tokens if contains_variable(t.value, "$@&%")):
62+
63+
async for var_token, var in self.iter_variables_from_token(
64+
token,
65+
self.namespace,
66+
self.node_stack,
67+
range_from_token(token).start,
68+
skip_commandline_variables=False,
69+
return_not_found=True,
70+
):
71+
if isinstance(var, VariableNotFoundDefinition):
72+
await self.append_diagnostics(
73+
range=range_from_token(var_token),
74+
message=f"Variable '{var.name}' not found",
75+
severity=DiagnosticSeverity.ERROR,
76+
source=DIAGNOSTICS_SOURCE_NAME,
77+
)
78+
79+
await super().visit(node)
80+
finally:
81+
self.node_stack = self.node_stack[:-1]
5482

5583
@staticmethod
5684
async def should_ignore(document: Optional[TextDocument], range: Range) -> bool:
@@ -83,7 +111,7 @@ async def append_diagnostics(
83111
data: Optional[Any] = None,
84112
) -> None:
85113

86-
if await self.should_ignore(self._namespace.document, range):
114+
if await self.should_ignore(self.namespace.document, range):
87115
return
88116

89117
self._results.append(

robotcode/language_server/robotframework/diagnostics/entities.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class VariableDefinitionType(Enum):
8282
BUILTIN_VARIABLE = "builtin variable"
8383
IMPORTED_VARIABLE = "imported variable"
8484
ENVIRONMENT_VARIABLE = "environment variable"
85+
VARIABLE_NOT_FOUND = "variable not found"
8586

8687

8788
@dataclass
@@ -160,3 +161,12 @@ class EnvironmentVariableDefinition(VariableDefinition):
160161

161162
def __hash__(self) -> int:
162163
return hash((type(self), self.name, self.type))
164+
165+
166+
@dataclass
167+
class VariableNotFoundDefinition(VariableDefinition):
168+
type: VariableDefinitionType = VariableDefinitionType.VARIABLE_NOT_FOUND
169+
resolvable: bool = False
170+
171+
def __hash__(self) -> int:
172+
return hash((type(self), self.name, self.type))

robotcode/language_server/robotframework/parts/model_helper.py

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
)
1919

2020
from ...common.lsp_types import Position
21-
from ..diagnostics.entities import VariableDefinition
21+
from ..diagnostics.entities import VariableDefinition, VariableNotFoundDefinition
2222
from ..diagnostics.library_doc import KeywordDoc, KeywordError
2323
from ..diagnostics.namespace import LibraryEntry, Namespace
2424
from ..utils.ast import (
@@ -239,6 +239,7 @@ async def iter_expression_variables_from_token(
239239
nodes: Optional[List[ast.AST]],
240240
position: Optional[Position] = None,
241241
skip_commandline_variables: bool = False,
242+
return_not_found: bool = False,
242243
) -> AsyncGenerator[Tuple[Token, VariableDefinition], Any]:
243244
from robot.api.parsing import Token as RobotToken
244245

@@ -250,14 +251,25 @@ async def iter_expression_variables_from_token(
250251
var = await namespace.find_variable(
251252
f"${{{tokval}}}", nodes, position, skip_commandline_variables=skip_commandline_variables
252253
)
254+
sub_token = RobotToken(
255+
expression.type,
256+
tokval,
257+
expression.lineno,
258+
expression.col_offset + tokcol,
259+
expression.error,
260+
)
253261
if var is not None:
254-
yield RobotToken(
255-
expression.type,
262+
yield sub_token, var
263+
elif return_not_found:
264+
yield sub_token, VariableNotFoundDefinition(
265+
sub_token.lineno,
266+
sub_token.col_offset,
267+
sub_token.lineno,
268+
sub_token.end_col_offset,
269+
namespace.source,
256270
tokval,
257-
expression.lineno,
258-
expression.col_offset + tokcol,
259-
expression.error,
260-
), var
271+
sub_token,
272+
)
261273
variable_started = False
262274
if toknum == python_token.ERRORTOKEN and tokval == "$":
263275
variable_started = True
@@ -331,10 +343,19 @@ async def iter_variables_from_token(
331343
nodes: Optional[List[ast.AST]],
332344
position: Optional[Position] = None,
333345
skip_commandline_variables: bool = False,
346+
return_not_found: bool = False,
334347
) -> AsyncGenerator[Tuple[Token, VariableDefinition], Any]:
335348
from robot.api.parsing import Token as RobotToken
336349
from robot.variables.search import contains_variable, search_variable
337350

351+
def is_number(name: str) -> bool:
352+
from robot.variables.finders import NOT_FOUND, NumberFinder
353+
354+
if name.startswith("$"):
355+
finder = NumberFinder()
356+
return bool(finder.find(name) != NOT_FOUND)
357+
return False
358+
338359
async def iter_token(
339360
to: Token, ignore_errors: bool = False
340361
) -> AsyncGenerator[Union[Token, Tuple[Token, VariableDefinition]], Any]:
@@ -352,6 +373,7 @@ async def iter_token(
352373
nodes,
353374
position,
354375
skip_commandline_variables=skip_commandline_variables,
376+
return_not_found=return_not_found,
355377
):
356378
yield v
357379

@@ -384,6 +406,9 @@ async def iter_token(
384406
yield strip_variable_token(sub_token), var
385407
continue
386408

409+
if is_number(sub_token.value):
410+
continue
411+
387412
if (
388413
sub_token.type == RobotToken.VARIABLE
389414
and sub_token.value[:1] in "$@&%"
@@ -397,10 +422,33 @@ async def iter_token(
397422
var = await namespace.find_variable(
398423
name, nodes, position, skip_commandline_variables=skip_commandline_variables
399424
)
425+
sub_sub_token = RobotToken(sub_token.type, name, sub_token.lineno, sub_token.col_offset)
400426
if var is not None:
401-
yield strip_variable_token(
402-
RobotToken(sub_token.type, name, sub_token.lineno, sub_token.col_offset)
403-
), var
427+
yield strip_variable_token(sub_sub_token), var
428+
continue
429+
if is_number(name):
430+
continue
431+
elif return_not_found:
432+
yield strip_variable_token(sub_sub_token), VariableNotFoundDefinition(
433+
sub_sub_token.lineno,
434+
sub_sub_token.col_offset,
435+
sub_sub_token.lineno,
436+
sub_sub_token.end_col_offset,
437+
namespace.source,
438+
name,
439+
sub_sub_token,
440+
)
441+
continue
442+
if return_not_found:
443+
yield strip_variable_token(sub_token), VariableNotFoundDefinition(
444+
sub_token.lineno,
445+
sub_token.col_offset,
446+
sub_token.lineno,
447+
sub_token.end_col_offset,
448+
namespace.source,
449+
sub_token.value,
450+
sub_token,
451+
)
404452
else:
405453
yield token_or_var
406454

Lines changed: 16 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,26 @@
1-
*** Settings ***
2-
Variables testvars.yml
3-
Resource firstresource.resource
4-
Test Setup kw 1
5-
61
*** Variables ***
7-
${A} ${1}
8-
${B} ${2}
9-
${C} 1
10-
${A VAR} 123
11-
&{A DICT VAR} first=hei second=no
12-
@{A LISTVAR} 1 2 3 4 5
13-
${CMD_VAR_LONG} 1
2+
${2} ${0x112.3432}
3+
${a} ${2}
144

15-
*** Test Cases ***
16-
first *1*
17-
[Documentation] A test suite for valid login.
18-
...
19-
... \${Execdir}: ${Execdir}
20-
...
21-
... \${LOG_FILE}: ${LOG_FILE}
225

6+
*** Test Cases ***
7+
first
238
Log ${{$a+$b}}
24-
Log ${CMD_VAR_LONG}
25-
Log ${A}[1]
26-
27-
28-
first ?11?
29-
log hi
30-
Log ${A VAR}
31-
Log ${A VAR}[${A}]
32-
Log ${{$a+1}}
33-
Log @${{[1,2,3]}}
34-
Log ${A VAR}[\[]
35-
Log ${A VAR}[${{$c+["\["][0]}}]
36-
37-
first *11*
38-
Log hi
39-
#${A} Evaluate ${{$b+$b}}
40-
#${A} Evaluate $b+$b
41-
# Evaluate $a==1
42-
# Run Keyword If $A a ELSE IF $a==34
43-
# Run Keyword And Return If $a==2 a
44-
# Run Keyword Unless $a==2 a
45-
46-
do something in a resource
47-
48-
IF $A_Var=='124'
49-
Log Hello
50-
END
51-
IF $a==2
52-
kw 55
53-
ELSE IF $a==1
54-
Log ho
55-
ELSE
56-
Log huch
57-
END
589

59-
FOR ${i} IN arg
60-
Log ${i}
61-
END
10+
Log %{APPDATA}
11+
Log ${2}
12+
first kw 1 2 3
13+
Log ${a+@{c}+${d}}
14+
Log ${{$a+$b+$c}}
15+
Log ${c}
16+
${v} IF 1 Evaluate 2 ELSE Evaluate 4
17+
Log ${v}
6218

63-
${k} Evaluate ${b}+$b
64-
Log ${k}
6519

66-
WHILE ${b}+$b
67-
Log ${B}
68-
${B} Evaluate $b-1
69-
70-
END
71-
72-
73-
Log ende
7420

7521
*** Keywords ***
76-
kw
77-
[Arguments] ${a}
78-
79-
kw1
80-
IF ${a}==1
81-
Log yeah
82-
ELSE IF $a==1
83-
Log no yeah
84-
ELSE
85-
Log hoho
86-
END
87-
88-
89-
kw1
90-
[Arguments] ${c}=99
91-
${B} Evaluate 1+2
92-
Log hello
93-
22+
first kw
23+
[Arguments] ${a} ${b} ${a}
24+
Log ${a}
25+
Log ${1asda + ${c} + 2}
9426

0 commit comments

Comments
 (0)