Skip to content

Commit

Permalink
Recursively resolve variables and provide better error messages on fa…
Browse files Browse the repository at this point in the history
…ilure conditions.
  • Loading branch information
fabioz committed Aug 2, 2022
1 parent 5e5cbce commit 6c24dcb
Show file tree
Hide file tree
Showing 14 changed files with 268 additions and 63 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests-robotframework-lsp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
working-directory: ./robotframework-ls/tests
env:
PYTHONPATH: .
RUN_TESTS_TIMEOUT: 300
RUN_TESTS_TIMEOUT: 500
run: python -u ../../robocorp-python-ls-core/tests/run_tests.py -rfE -otests_output -vv .
- uses: actions/upload-artifact@v1
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,15 +386,15 @@ def _collect_libraries_keywords(
node_name_tok
)
if token_errors:
for token_error in token_errors:
for token_error, error_msg in token_errors:
collector.on_unresolved_library(
completion_context,
node.name,
token_error.lineno,
token_error.lineno,
token_error.col_offset,
token_error.end_col_offset,
f"\nUnable to statically resolve variable: {token_error.value}.\nPlease set the `{token_error.value[2:-1]}` value in `robot.variables`.",
error_msg,
value,
)
else:
Expand Down Expand Up @@ -457,15 +457,15 @@ def _collect_from_context(
)

if token_errors:
for token_error in token_errors:
for token_error, error_msg in token_errors:
collector.on_unresolved_resource(
completion_context,
node.name,
token_error.lineno,
token_error.lineno,
token_error.col_offset,
token_error.end_col_offset,
f"\nUnable to statically resolve variable: {token_error.value}.\nPlease set the `{token_error.value[2:-1]}` value in `robot.variables`.",
error_msg,
value,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def token_value_resolving_variables(self, token: IRobotToken) -> str:

def token_value_and_unresolved_resolving_variables(
self, token: IRobotToken
) -> Tuple[str, Tuple[IRobotToken, ...]]:
) -> Tuple[str, Tuple[Tuple[IRobotToken, str], ...]]:
from robotframework_ls.impl.variable_resolve import ResolveVariablesContext

return ResolveVariablesContext(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1185,7 +1185,7 @@ def get_library_doc_or_error(

pre_error_msg = (
"It was not possible to statically resolve the following variables:\n%s\nFollow-up error:\n"
% (", ".join(str(x) for x in unresolved),)
% (", ".join(str(x[0]) for x in unresolved),)
)

args = args.replace("\\\\", "\\")
Expand Down
2 changes: 1 addition & 1 deletion robotframework-ls/src/robotframework_ls/impl/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ def token_value_resolving_variables(self, token: IRobotToken) -> str:

def token_value_and_unresolved_resolving_variables(
self, token: IRobotToken
) -> Tuple[str, Tuple[IRobotToken, ...]]:
) -> Tuple[str, Tuple[Tuple[IRobotToken, str], ...]]:
pass

def get_current_keyword_definition_and_usage_info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,15 @@ def collect_global_variables_from_document_dependencies(
)

if token_errors:
for token_error in token_errors:
for token_error, error_msg in token_errors:
collector.on_unresolved_variable_import(
completion_context,
node.name,
token_error.lineno,
token_error.lineno,
token_error.col_offset,
token_error.end_col_offset,
f"\nUnable to statically resolve variable: {token_error.value}.\nPlease set the `{token_error.value[2:-1]}` value in `robot.variables`.",
error_msg,
value,
)

Expand Down
127 changes: 101 additions & 26 deletions robotframework-ls/src/robotframework_ls/impl/variable_resolve.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
from functools import lru_cache
from typing import Optional, Tuple, List, Iterator
from typing import Optional, Tuple, List, Iterator, Union
from robotframework_ls.impl.protocols import (
IRobotVariableMatch,
IRobotToken,
Expand Down Expand Up @@ -294,7 +294,9 @@ def _convert_robot_variable(self, var_name, value_if_not_found):
finally:
resolve_info.discard(var_name)

def _convert_environment_variable(self, var_name, value_if_not_found):
def _convert_environment_variable(
self, var_name, value_if_not_found
) -> Optional[str]:
from robotframework_ls.impl.robot_lsp_constants import OPTION_ROBOT_PYTHON_ENV

if self.config is None:
Expand All @@ -316,15 +318,29 @@ def _convert_environment_variable(self, var_name, value_if_not_found):
("Unable to find environment variable: %s", var_name),
)

value = str(value)
return value
if value is None:
return None
return str(value)

def token_value_resolving_variables(self, token: IRobotToken) -> str:
return self.token_value_and_unresolved_resolving_variables(token)[0]

def token_value_and_unresolved_resolving_variables(
self, token: IRobotToken
) -> Tuple[str, Tuple[IRobotToken, ...]]:
self, token: Union[IRobotToken, str], count=0
) -> Tuple[str, Tuple[Tuple[IRobotToken, str], ...]]:
"""
:return: The new value of the token (with resolved variables) and if
some variable was unresolved, a tuple of tuples with the token of the
unresolved variable as well as the related error message.
i.e.:
(
'resolved/var',
(
(unresolved_token, error_msg), (unresolved_token, error_msg)
)
)
"""
from robotframework_ls.impl import ast_utils

robot_token: IRobotToken
Expand All @@ -338,7 +354,7 @@ def token_value_and_unresolved_resolving_variables(
except:
return robot_token.value, () # Unable to tokenize

unresolved: List[IRobotToken] = []
unresolved: List[Tuple[IRobotToken, str]] = []

parts: List[str] = []
tok: IRobotToken
Expand All @@ -348,28 +364,87 @@ def token_value_and_unresolved_resolving_variables(
parts.append(str(tok))

elif tok.type == tok.VARIABLE:
if count > 6:
unresolved.append(
(
tok,
f"\nRecursion detected when resolving variable: {tok.value}.",
)
)
return str(tok), tuple(unresolved)
value = str(tok)

convert_with = None
if value.startswith("${") and value.endswith("}"):
convert_with = self._convert_robot_variable

elif value.startswith("%{") and value.endswith("}"):
convert_with = self._convert_environment_variable

if convert_with is None:
log.info("Cannot resolve variable: %s", value)
unresolved.append(tok)
parts.append(value) # Leave unresolved.
resolved, new_value = self._resolve_tok_var(value)
if new_value is None:
new_value = value
if not resolved:
unresolved.append(
(
tok,
f"\nUnable to statically resolve variable: {tok.value}.\nPlease set the `{tok.value[2:-1]}` value in `robot.variables`.",
)
)
else:
value = value[2:-1]
converted = convert_with(value, None)
if converted is None:
log.info("Cannot resolve variable: %s", value)
unresolved.append(tok)
parts.append(value)
else:
parts.append(converted)
# Ok, it was resolved, but we need to check if we need
# to resolve new values from that value (and do it
# recursively if needed).
for _ in range(6):
if "{" in new_value:
(
v2,
unresolved_tokens,
) = self.token_value_and_unresolved_resolving_variables(
new_value, count + 1
)
if unresolved_tokens:
if len(unresolved_tokens) == 1:
t = next(iter(unresolved_tokens))[0]
unresolved.append(
(
tok,
f"\nUnable to statically resolve variable: {tok.value} because dependent variable: {t.value} was not resolved.",
)
)
else:
lst = []
for t, _error_msg in unresolved_tokens:
lst.append(t.value)

unresolved.append(
(
tok,
f"\nUnable to statically resolve variable: {tok.value} because dependent variables: {', '.join(lst)} were not resolved.",
)
)
break

if v2 == new_value:
break
new_value = v2
parts.append(new_value)

joined_parts = "".join(parts)
return joined_parts, tuple(unresolved)

def _resolve_tok_var(self, value: str) -> Tuple[bool, str]:
"""
:return: whether the variable was resolved and its value (same as input if unresolved).
"""
convert_with = None
if value.startswith("${") and value.endswith("}"):
convert_with = self._convert_robot_variable

elif value.startswith("%{") and value.endswith("}"):
convert_with = self._convert_environment_variable

if convert_with is None:
log.info("Cannot resolve variable: %s", value)
return False, value # Leave unresolved.
else:
inner_value = value[2:-1]
converted = convert_with(inner_value, None)
if converted is None:
log.info("Cannot resolve variable: %s", value)
return False, value
else:
return True, converted
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
*** Settings ***
Test Template Variable should not exist
Resource ${IMPORT 1}.robot
#! ^^^^^^^^^^^^^^^^^ Unresolved resource: ${IMPORT 1}.robot
#! ^^^^^^^^^^^^^^^^^ Note: resolved name: ${IMPORT 2}.robot
#! ^^^^^^^^^^^ Unable to statically resolve variable: ${IMPORT 1} because dependent variable: ${IMPORT 2} was not resolved.
Library ${IMPORT 2}.py
#! ^^^^^^^^^^^^^^ Unresolved library: ${IMPORT 2}.py.
#! ^^^^^^^^^^^^^^ Error generating libspec:
#! ^^^^^^^^^^^^^^ Importing library '${IMPORT 1}' failed: ModuleNotFoundError: No module named '${IMPORT 1}'
#! ^^^^^^^^^^^^^^ Note: resolved name: ${IMPORT 1}.py
#! ^^^^^^^^^^^^^^ Consider adding the needed paths to the "robot.pythonpath" setting
#! ^^^^^^^^^^^^^^ and calling the "Robot Framework: Clear caches and restart" action.
#! ^^^^^^^^^^^ Unresolved library: ${IMPORT 2}.py.
#! ^^^^^^^^^^^ Unable to statically resolve variable: ${IMPORT 2} because dependent variable: ${IMPORT 1} was not resolved.

*** Variables ***
${DIRECT} ${DIRECT}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,7 @@ ${NONEX 3} This ${NON EXISTING VARIABLE} is used in imports.

*** Settings ***
Resource ${NONEX 3}
#! ^^^^^^^^^^ Unresolved resource: ${NONEX 3}
#! ^^^^^^^^^^ Note: resolved name: This ${NON EXISTING VARIABLE} is used in imports.
#! ^^^^^^^^^^ Unable to statically resolve variable: ${NONEX 3} because dependent variable: ${NON EXISTING VARIABLE} was not resolved.
Library ${NONEX 3}
#! ^^^^^^^^^^ Unresolved library: ${NONEX 3}.
#! ^^^^^^^^^^ Error generating libspec:
#! ^^^^^^^^^^ Importing library 'This ${NON EXISTING VARIABLE} is used in imports.' failed: ModuleNotFoundError: No module named 'This ${NON EXISTING VARIABLE} is used in imports'
#! ^^^^^^^^^^ Note: resolved name: This ${NON EXISTING VARIABLE} is used in imports.
#! ^^^^^^^^^^ Consider adding the needed paths to the "robot.pythonpath" setting
#! ^^^^^^^^^^ and calling the "Robot Framework: Clear caches and restart" action.
#! ^^^^^^^^^^ Unable to statically resolve variable: ${NONEX 3} because dependent variable: ${NON EXISTING VARIABLE} was not resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,10 @@ ${NONEX 3} This ${NON EXISTING VARIABLE} is used in imports.
*** Settings ***
Resource ${NONEX 3}
#! ^^^^^^^^^^ Unresolved resource: ${NONEX 3}
#! ^^^^^^^^^^ Note: resolved name: This ${NON EXISTING VARIABLE} is used in imports.
#! ^^^^^^^^^^ Unable to statically resolve variable: ${NONEX 3} because dependent variable: ${NON EXISTING VARIABLE} was not resolved.
Library ${NONEX 3}
#! ^^^^^^^^^^ Unresolved library: ${NONEX 3}.
#! ^^^^^^^^^^ Error generating libspec:
#! ^^^^^^^^^^ Importing library 'This ${NON EXISTING VARIABLE} is used in imports.' failed: ModuleNotFoundError: No module named 'This ${NON EXISTING VARIABLE} is used in imports'
#! ^^^^^^^^^^ Note: resolved name: This ${NON EXISTING VARIABLE} is used in imports.
#! ^^^^^^^^^^ Consider adding the needed paths to the "robot.pythonpath" setting
#! ^^^^^^^^^^ and calling the "Robot Framework: Clear caches and restart" action.
#! ^^^^^^^^^^ Unable to statically resolve variable: ${NONEX 3} because dependent variable: ${NON EXISTING VARIABLE} was not resolved.
*** Test Case ***
Scalar String
Expand Down
Loading

0 comments on commit 6c24dcb

Please sign in to comment.