Skip to content

Commit af303fb

Browse files
committed
Support for variable files variable analysis for IF/WHILE expressions and specific keywords
1 parent 886ec80 commit af303fb

21 files changed

+748
-560
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ All notable changes to the "robotcode" extension will be documented in this file
44

55
## [Unreleased]
66

7-
- Improve handling of completion of argument definitions
7+
- Variable analysis, finds undefined variables
8+
- in variables, also inner variables like ${a+${b}}
9+
- in inline python expression like ${{$a+$b}}
10+
- in expression arguments of IF/WHILE statements like $a<$b
11+
- in BuiltIn keywords which contains an expression or condition argument, like `Evaluate`, `Should Be True`, `Skip If`, ...
12+
- Improve handling of completion for argument definitions
13+
- Support for variable files
14+
- there is a new setting `robotcode.robot.variableFiles` and corresponding `variableFiles` launch configuration setting
15+
- this corresponds to the `--variablefile` option from robot
816

917
## 0.9.5
1018

robotcode/language_server/robotframework/configuration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class RobotConfig(ConfigBase):
1919
python_path: List[str] = field(default_factory=list)
2020
env: Dict[str, str] = field(default_factory=dict)
2121
variables: Dict[str, Any] = field(default_factory=dict)
22+
variable_files: List[str] = field(default_factory=list)
2223
paths: List[str] = field(default_factory=list)
2324
output_dir: Optional[str] = None
2425
output_file: Optional[str] = None

robotcode/language_server/robotframework/diagnostics/analyzer.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from ..parts.model_helper import ModelHelperMixin
2121
from ..utils.ast import (
2222
HasTokens,
23+
Statement,
2324
Token,
2425
is_not_variable_token,
2526
range_from_node_or_token,
@@ -54,12 +55,13 @@ async def run(self) -> List[Diagnostic]:
5455

5556
async def visit(self, node: ast.AST) -> None:
5657
from robot.variables.search import contains_variable
58+
from robot.parsing.lexer.tokens import Token as RobotToken
59+
from robot.parsing.model.statements import KeywordCall
5760

5861
self.node_stack.append(node)
5962
try:
6063
if isinstance(node, HasTokens):
6164
for token in (t for t in node.tokens if contains_variable(t.value, "$@&%")):
62-
6365
async for var_token, var in self.iter_variables_from_token(
6466
token,
6567
self.namespace,
@@ -75,6 +77,58 @@ async def visit(self, node: ast.AST) -> None:
7577
severity=DiagnosticSeverity.ERROR,
7678
source=DIAGNOSTICS_SOURCE_NAME,
7779
)
80+
if (
81+
isinstance(node, Statement)
82+
and isinstance(node, self.get_expression_statement_types())
83+
and (token := node.get_token(RobotToken.ARGUMENT)) is not None
84+
):
85+
async for var_token, var in self.iter_expression_variables_from_token(
86+
token,
87+
self.namespace,
88+
self.node_stack,
89+
range_from_token(token).start,
90+
skip_commandline_variables=False,
91+
return_not_found=True,
92+
):
93+
if isinstance(var, VariableNotFoundDefinition):
94+
await self.append_diagnostics(
95+
range=range_from_token(var_token),
96+
message=f"Variable '{var.name}' not found",
97+
severity=DiagnosticSeverity.ERROR,
98+
source=DIAGNOSTICS_SOURCE_NAME,
99+
)
100+
elif isinstance(node, Statement) and isinstance(node, KeywordCall) and node.keyword:
101+
kw_doc = await self.namespace.find_keyword(node.keyword)
102+
if kw_doc is not None and kw_doc.longname in [
103+
"BuiltIn.Evaluate",
104+
"BuiltIn.Should Be True",
105+
"BuiltIn.Should Not Be True",
106+
"BuiltIn.Skip If",
107+
"BuiltIn.Continue For Loop If",
108+
"BuiltIn.Exit For Loop If",
109+
"BuiltIn.Return From Keyword If",
110+
"BuiltIn.Run Keyword And Return If",
111+
"BuiltIn.Pass Execution If",
112+
"BuiltIn.Run Keyword If",
113+
"BuiltIn.Run Keyword Unless",
114+
]:
115+
tokens = node.get_tokens(RobotToken.ARGUMENT)
116+
if tokens and (token := tokens[0]):
117+
async for var_token, var in self.iter_expression_variables_from_token(
118+
token,
119+
self.namespace,
120+
self.node_stack,
121+
range_from_token(token).start,
122+
skip_commandline_variables=False,
123+
return_not_found=True,
124+
):
125+
if isinstance(var, VariableNotFoundDefinition):
126+
await self.append_diagnostics(
127+
range=range_from_token(var_token),
128+
message=f"Variable '{var.name}' not found",
129+
severity=DiagnosticSeverity.ERROR,
130+
source=DIAGNOSTICS_SOURCE_NAME,
131+
)
78132

79133
await super().visit(node)
80134
finally:

robotcode/language_server/robotframework/diagnostics/entities.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,52 @@ def __hash__(self) -> int:
7474
)
7575

7676

77+
class InvalidVariableError(Exception):
78+
pass
79+
80+
81+
class VariableMatcher:
82+
def __init__(self, name: str) -> None:
83+
from robot.utils.normalizing import normalize
84+
from robot.variables.search import VariableSearcher
85+
86+
self.name = name
87+
88+
searcher = VariableSearcher("$@&%", ignore_errors=True)
89+
match = searcher.search(name)
90+
91+
if match.base is None:
92+
raise InvalidVariableError(f"Invalid variable '{name}'")
93+
94+
self.base = match.base
95+
96+
self.normalized_name = str(normalize(self.base, "_"))
97+
98+
def __eq__(self, o: object) -> bool:
99+
from robot.utils.normalizing import normalize
100+
from robot.variables.search import VariableSearcher
101+
102+
if isinstance(o, VariableMatcher):
103+
return o.normalized_name == self.normalized_name
104+
elif isinstance(o, str):
105+
searcher = VariableSearcher("$@&%", ignore_errors=True)
106+
match = searcher.search(o)
107+
base = match.base
108+
normalized = str(normalize(base, "_"))
109+
return self.normalized_name == normalized
110+
else:
111+
return False
112+
113+
def __hash__(self) -> int:
114+
return hash(self.name)
115+
116+
def __str__(self) -> str:
117+
return self.name
118+
119+
def __repr__(self) -> str:
120+
return f"{type(self).__name__}(name={repr(self.name)})"
121+
122+
77123
class VariableDefinitionType(Enum):
78124
VARIABLE = "variable"
79125
LOCAL_VARIABLE = "local variable"
@@ -96,6 +142,14 @@ class VariableDefinition(SourceEntity):
96142

97143
value: Any = None
98144

145+
__matcher: Optional[VariableMatcher] = None
146+
147+
@property
148+
def matcher(self) -> VariableMatcher:
149+
if self.__matcher is None:
150+
self.__matcher = VariableMatcher(self.name)
151+
return self.__matcher
152+
99153
def __hash__(self) -> int:
100154
return hash((type(self), self.name, self.type, self.range, self.source, self.name_token))
101155

robotcode/language_server/robotframework/diagnostics/imports_manager.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,9 @@ def python_path(self) -> List[str]:
507507
return self._python_path or []
508508

509509
@_logger.call
510-
def get_command_line_variables(self) -> List[VariableDefinition]:
510+
async def get_command_line_variables(self) -> List[VariableDefinition]:
511+
from robot.utils.text import split_args_from_name_or_path
512+
511513
if self._command_line_variables is None:
512514
if self.config is None:
513515
self._command_line_variables = []
@@ -516,6 +518,19 @@ def get_command_line_variables(self) -> List[VariableDefinition]:
516518
CommandLineVariableDefinition(0, 0, 0, 0, "", f"${{{k}}}", None, has_value=True, value=(v,))
517519
for k, v in self.config.variables.items()
518520
]
521+
for variable_file in self.config.variable_files:
522+
name, args = split_args_from_name_or_path(variable_file)
523+
try:
524+
lib_doc = await self.get_libdoc_for_variables_import(
525+
name, tuple(args), str(self.folder.to_path()), self
526+
)
527+
if lib_doc is not None:
528+
self._command_line_variables += lib_doc.variables
529+
530+
except (SystemExit, KeyboardInterrupt, asyncio.CancelledError):
531+
raise
532+
except BaseException as e:
533+
self._logger.exception(e)
519534

520535
return self._command_line_variables
521536

robotcode/language_server/robotframework/diagnostics/library_doc.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -165,52 +165,6 @@ def __repr__(self) -> str:
165165
ALL_RUN_KEYWORDS_MATCHERS = [KeywordMatcher(e) for e in ALL_RUN_KEYWORDS]
166166

167167

168-
class InvalidVariableError(Exception):
169-
pass
170-
171-
172-
class VariableMatcher:
173-
def __init__(self, name: str) -> None:
174-
from robot.utils.normalizing import normalize
175-
from robot.variables.search import VariableSearcher
176-
177-
self.name = name
178-
179-
searcher = VariableSearcher("$@&%", ignore_errors=True)
180-
match = searcher.search(name)
181-
182-
if match.base is None:
183-
raise InvalidVariableError(f"Invalid variable '{name}'")
184-
185-
self.base = match.base
186-
187-
self.normalized_name = str(normalize(self.base, "_"))
188-
189-
def __eq__(self, o: object) -> bool:
190-
from robot.utils.normalizing import normalize
191-
from robot.variables.search import VariableSearcher
192-
193-
if isinstance(o, VariableMatcher):
194-
return o.normalized_name == self.normalized_name
195-
elif isinstance(o, str):
196-
searcher = VariableSearcher("$@&%", ignore_errors=True)
197-
match = searcher.search(o)
198-
base = match.base
199-
normalized = str(normalize(base, "_"))
200-
return self.normalized_name == normalized
201-
else:
202-
return False
203-
204-
def __hash__(self) -> int:
205-
return hash(self.name)
206-
207-
def __str__(self) -> str:
208-
return self.name
209-
210-
def __repr__(self) -> str:
211-
return f"{type(self).__name__}(name={repr(self.name)})"
212-
213-
214168
@dataclass
215169
class Error:
216170
message: str

0 commit comments

Comments
 (0)