From bcbf98b494e28e23e20521a54accb097c7295854 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Tue, 5 Apr 2022 10:04:56 -0300 Subject: [PATCH] Resolve document variables for library arguments. Fixes #634 --- .../.vscode/robocode-vscode.code-workspace | 3 +- robotframework-ls/docs/changelog.md | 1 + .../src/robotframework_ls/impl/ast_utils.py | 2 +- .../impl/auto_import_completions.py | 11 +- .../robotframework_ls/impl/code_analysis.py | 3 +- .../impl/collect_keywords.py | 5 +- .../impl/completion_context.py | 105 ++++++++- .../completion_context_workspace_caches.py | 2 +- .../impl/filesystem_section_completions.py | 3 +- .../robotframework_ls/impl/find_definition.py | 2 +- .../robotframework_ls/impl/libspec_manager.py | 39 ++-- .../src/robotframework_ls/impl/protocols.py | 23 +- .../robotframework_ls/impl/signature_help.py | 2 +- .../robotframework_ls/impl/text_utilities.py | 2 +- .../impl/variable_completions.py | 207 ++++++++--------- .../impl/variable_resolve.py | 214 +++++++++++------- .../case_params_on_lib/.vscode/settings.json | 3 + .../case_params_on_lib/LibWithParams3.py | 6 + .../case_params_on_lib/LibWithParams4.py | 6 + .../case_params_on_lib3.robot | 10 + .../case_params_on_lib4.robot | 8 + .../case_params_on_lib/vars_resource.resource | 2 + .../completions/test_auto_import.py | 45 ++-- .../completions/test_keyword_completions.py | 13 +- .../test_code_analysis.py | 48 ++++ .../test_resolve_vars_in_libdoc_init.yml | 5 + .../test_libspec_manager.py | 46 ++-- .../test_workspace_symbol.py | 7 +- 28 files changed, 534 insertions(+), 289 deletions(-) create mode 100644 robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/.vscode/settings.json create mode 100644 robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/LibWithParams3.py create mode 100644 robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/LibWithParams4.py create mode 100644 robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/case_params_on_lib3.robot create mode 100644 robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/case_params_on_lib4.robot create mode 100644 robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/vars_resource.resource create mode 100644 robotframework-ls/tests/robotframework_ls_tests/test_find_definition/test_resolve_vars_in_libdoc_init.yml diff --git a/robocorp-code/.vscode/robocode-vscode.code-workspace b/robocorp-code/.vscode/robocode-vscode.code-workspace index e3fde7fe42..53add1c42b 100644 --- a/robocorp-code/.vscode/robocode-vscode.code-workspace +++ b/robocorp-code/.vscode/robocode-vscode.code-workspace @@ -49,6 +49,7 @@ }, "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "python.terminal.activateEnvironment": false } } diff --git a/robotframework-ls/docs/changelog.md b/robotframework-ls/docs/changelog.md index 738c048ccd..ff5e2a1bd7 100644 --- a/robotframework-ls/docs/changelog.md +++ b/robotframework-ls/docs/changelog.md @@ -1,6 +1,7 @@ NEXT ----------------------------- +- Variables in document are considered in Libdoc arguments. [#634](https://github.com/robocorp/robotframework-lsp/issues/634) - Fixed issue finding variables in python files with annotated assignments (i.e.: `value: int = 10`). [#629](https://github.com/robocorp/robotframework-lsp/issues/629) - The debugger no longer stops in `Run Keyword And Return Status` by default. [#625](https://github.com/robocorp/robotframework-lsp/issues/625) - Code-lenses (Run/Debug/Interactive console) are shown by default again. diff --git a/robotframework-ls/src/robotframework_ls/impl/ast_utils.py b/robotframework-ls/src/robotframework_ls/impl/ast_utils.py index 6884bbf410..0c22fdf1c6 100644 --- a/robotframework-ls/src/robotframework_ls/impl/ast_utils.py +++ b/robotframework-ls/src/robotframework_ls/impl/ast_utils.py @@ -575,7 +575,7 @@ def tokenize_variables_from_name(name): return tokenize_variables(create_token(name)) # May throw error if it's not OK. -def tokenize_variables(token: IRobotToken): +def tokenize_variables(token: IRobotToken) -> Iterator[IRobotToken]: return token.tokenize_variables() # May throw error if it's not OK. diff --git a/robotframework-ls/src/robotframework_ls/impl/auto_import_completions.py b/robotframework-ls/src/robotframework_ls/impl/auto_import_completions.py index 70f2df7943..d9531e68b4 100644 --- a/robotframework-ls/src/robotframework_ls/impl/auto_import_completions.py +++ b/robotframework-ls/src/robotframework_ls/impl/auto_import_completions.py @@ -276,6 +276,7 @@ def get_resource_import_line(self) -> int: def _obtain_import_location_info(completion_context) -> _ImportLocationInfo: from robotframework_ls.impl import ast_utils from robotframework_ls.impl.libspec_manager import LibspecManager + from robot.api import Token import_location_info = _ImportLocationInfo() @@ -294,12 +295,14 @@ def _obtain_import_location_info(completion_context) -> _ImportLocationInfo: if ast_utils.is_library_node_info(node_info): import_location_info.library_node_info = node_info - library_name = node_info.node.name - if library_name: + library_name_token = node_info.node.get_token(Token.NAME) + if library_name_token is not None: library_doc_or_error = libspec_manager.get_library_doc_or_error( - completion_context.token_value_resolving_variables(library_name), + completion_context.token_value_resolving_variables( + library_name_token + ), create=True, - current_doc_uri=completion_context.doc.uri, + completion_context=completion_context, args=ast_utils.get_library_arguments_serialized(node_info.node), ) library_doc = library_doc_or_error.library_doc diff --git a/robotframework-ls/src/robotframework_ls/impl/code_analysis.py b/robotframework-ls/src/robotframework_ls/impl/code_analysis.py index a4cda5e0c8..2a4c24cac8 100644 --- a/robotframework-ls/src/robotframework_ls/impl/code_analysis.py +++ b/robotframework-ls/src/robotframework_ls/impl/code_analysis.py @@ -11,6 +11,7 @@ ILibraryDoc, INode, IVariableFound, + AbstractVariablesCollector, ) from robocorp_ls_core.lsp import DiagnosticSeverity, DiagnosticTag from robotframework_ls.impl.robot_lsp_constants import ( @@ -54,7 +55,7 @@ def get_keyword(self, normalized_keyword_name: str) -> Optional[IKeywordFound]: return None -class _VariablesCollector(object): +class _VariablesCollector(AbstractVariablesCollector): def __init__(self, on_unresolved_variable_import): self._variables_collected = set() self.on_unresolved_variable_import = on_unresolved_variable_import diff --git a/robotframework-ls/src/robotframework_ls/impl/collect_keywords.py b/robotframework-ls/src/robotframework_ls/impl/collect_keywords.py index 0d30a11c52..2e83bc0ee3 100644 --- a/robotframework-ls/src/robotframework_ls/impl/collect_keywords.py +++ b/robotframework-ls/src/robotframework_ls/impl/collect_keywords.py @@ -318,7 +318,6 @@ def _collect_current_doc_keywords( def _collect_libraries_keywords( completion_context: ICompletionContext, - current_doc_uri: str, library_infos: Iterator[LibraryDependencyInfo], collector: IKeywordCollector, ): @@ -337,7 +336,7 @@ def _collect_libraries_keywords( libspec_manager.get_library_doc_or_error( library_info.name, create=True, - current_doc_uri=current_doc_uri, + completion_context=completion_context, builtin=library_info.builtin, args=library_info.args, ) @@ -431,7 +430,6 @@ def _collect_from_context( completion_context.check_cancelled() _collect_libraries_keywords( completion_context, - completion_context.doc.uri, dependency_graph.iter_libraries(completion_context.doc.uri), collector, ) @@ -494,7 +492,6 @@ def _collect_from_context( _collect_current_doc_keywords(new_ctx, collector) _collect_libraries_keywords( new_ctx, - resource_doc.uri, dependency_graph.iter_libraries(resource_doc.uri), collector, ) diff --git a/robotframework-ls/src/robotframework_ls/impl/completion_context.py b/robotframework-ls/src/robotframework_ls/impl/completion_context.py index ed4c7a9b70..f28d20779a 100644 --- a/robotframework-ls/src/robotframework_ls/impl/completion_context.py +++ b/robotframework-ls/src/robotframework_ls/impl/completion_context.py @@ -8,7 +8,6 @@ Set, Callable, Dict, - Union, Iterator, Sequence, ) @@ -40,15 +39,14 @@ IVariableImportNode, VarTokenInfo, IVariablesFromArgumentsFileLoader, + IVariableFound, + NodeInfo, ) from robotframework_ls.impl.robot_workspace import RobotDocument from robocorp_ls_core import uris import itertools from functools import partial import typing -from robotframework_ls.impl.variables_from_arguments_file import ( - VariablesFromArgumentsFileLoader, -) log = get_logger(__name__) @@ -336,12 +334,101 @@ def get_current_token(self) -> Optional[TokenInfo]: return None return ast_utils.find_token(section, self.sel.line, self.sel.col) - def get_all_variables(self): + def get_all_variables(self) -> Tuple[NodeInfo, ...]: from robotframework_ls.impl import ast_utils ast = self.get_ast() return tuple(ast_utils.iter_variables(ast)) + @instance_cache + def get_doc_normalized_var_name_to_var_found(self) -> Dict[str, IVariableFound]: + from robotframework_ls.impl import ast_utils + from robotframework_ls.impl.variable_resolve import robot_search_variable + from robot.api import Token + from robotframework_ls.impl.variable_types import VariableFoundFromToken + from robotframework_ls.impl.text_utilities import normalize_robot_name + + ret: Dict[str, IVariableFound] = {} + for variable_node_info in self.get_all_variables(): + variable_node = variable_node_info.node + token = variable_node.get_token(Token.VARIABLE) + if token is None: + continue + + variable_match = robot_search_variable(token.value) + # Filter out empty base + if variable_match is None or not variable_match.base: + continue + + base_token = ast_utils.convert_variable_match_base_to_token( + token, variable_match + ) + ret[normalize_robot_name(variable_match.base)] = VariableFoundFromToken( + self, + base_token, + variable_node.value, + variable_name=variable_match.base, + ) + + return ret + + @instance_cache + def get_settings_normalized_var_name_to_var_found( + self, + ) -> Dict[str, IVariableFound]: + from robotframework_ls.impl.text_utilities import normalize_robot_name + from robotframework_ls.impl.variable_types import VariableFoundFromSettings + + ret: Dict[str, IVariableFound] = {} + + from robotframework_ls.impl.robot_lsp_constants import OPTION_ROBOT_VARIABLES + + config = self.config + if config is not None: + robot_variables = config.get_setting(OPTION_ROBOT_VARIABLES, dict, {}) + for key, val in robot_variables.items(): + ret[normalize_robot_name(key)] = VariableFoundFromSettings(key, val) + + return ret + + @instance_cache + def get_builtins_normalized_var_name_to_var_found( + self, resolved + ) -> Dict[str, IVariableFound]: + from robotframework_ls.impl.text_utilities import normalize_robot_name + from robotframework_ls.impl.variable_types import VariableFoundFromBuiltins + from robotframework_ls.impl.robot_constants import BUILTIN_VARIABLES_RESOLVED + + ret: Dict[str, IVariableFound] = {} + + from robotframework_ls.impl.robot_constants import get_builtin_variables + + for key, val in get_builtin_variables(): + ret[normalize_robot_name(key)] = VariableFoundFromBuiltins(key, val) + + if resolved: + for key, val in BUILTIN_VARIABLES_RESOLVED.items(): + # Provide a resolved value for the ones we can resolve. + ret[normalize_robot_name(key)] = VariableFoundFromBuiltins(key, val) + + return ret + + def get_arguments_files_normalized_var_name_to_var_found( + self, + ) -> Dict[str, IVariableFound]: + from robotframework_ls.impl.text_utilities import normalize_robot_name + + ret: Dict[str, IVariableFound] = {} + + if not self.variables_from_arguments_files_loader: + return ret + + for c in self.variables_from_arguments_files_loader: + for variable in c.get_variables(): + ret[normalize_robot_name(variable.variable_name)] = variable + + return ret + @instance_cache def get_current_variable(self, section=None) -> Optional[VarTokenInfo]: """ @@ -389,12 +476,10 @@ def get_variable_imports(self) -> Tuple[INode, ...]: ret.append(resource.node) return tuple(ret) - def token_value_resolving_variables(self, token: Union[str, IRobotToken]) -> str: + def token_value_resolving_variables(self, token: IRobotToken) -> str: from robotframework_ls.impl.variable_resolve import ResolveVariablesContext - return ResolveVariablesContext( - self.config, self._doc.path - ).token_value_resolving_variables(token) + return ResolveVariablesContext(self).token_value_resolving_variables(token) def token_value_and_unresolved_resolving_variables( self, token: IRobotToken @@ -402,7 +487,7 @@ def token_value_and_unresolved_resolving_variables( from robotframework_ls.impl.variable_resolve import ResolveVariablesContext return ResolveVariablesContext( - self.config, self._doc.path + self ).token_value_and_unresolved_resolving_variables(token) @instance_cache diff --git a/robotframework-ls/src/robotframework_ls/impl/completion_context_workspace_caches.py b/robotframework-ls/src/robotframework_ls/impl/completion_context_workspace_caches.py index 04a4509eea..01bb485489 100644 --- a/robotframework-ls/src/robotframework_ls/impl/completion_context_workspace_caches.py +++ b/robotframework-ls/src/robotframework_ls/impl/completion_context_workspace_caches.py @@ -86,7 +86,7 @@ def __init__( self._lock = threading.Lock() # Small cache because invalidation could become slow in a big cache # (and it should be enough to hold what we're currently working with). - self._cached: _LRU[ICompletionContextDependencyGraph] = _LRU(4) + self._cached: _LRU[ICompletionContextDependencyGraph] = _LRU(5) self.cache_hits = 0 self.invalidations = 0 diff --git a/robotframework-ls/src/robotframework_ls/impl/filesystem_section_completions.py b/robotframework-ls/src/robotframework_ls/impl/filesystem_section_completions.py index 0fd98e640d..0e79b8530c 100644 --- a/robotframework-ls/src/robotframework_ls/impl/filesystem_section_completions.py +++ b/robotframework-ls/src/robotframework_ls/impl/filesystem_section_completions.py @@ -103,6 +103,7 @@ def _get_completions( from robotframework_ls.impl.string_matcher import RobotStringMatcher from robocorp_ls_core import uris from robotframework_ls.impl.robot_constants import BUILTIN_LIB, RESERVED_LIB + from robotframework_ls.impl import ast_utils ret: List[CompletionItemTypedDict] = [] @@ -112,7 +113,7 @@ def _get_completions( value_to_cursor = value_to_cursor[: -(token.end_col_offset - sel.col)] if "{" in value_to_cursor: value_to_cursor = completion_context.token_value_resolving_variables( - value_to_cursor + ast_utils.create_token(value_to_cursor) ) value_to_cursor_split = os.path.split(value_to_cursor) diff --git a/robotframework-ls/src/robotframework_ls/impl/find_definition.py b/robotframework-ls/src/robotframework_ls/impl/find_definition.py index d10d3fb389..a1b10f4c9d 100644 --- a/robotframework-ls/src/robotframework_ls/impl/find_definition.py +++ b/robotframework-ls/src/robotframework_ls/impl/find_definition.py @@ -367,7 +367,7 @@ def find_definition_extended( library_doc = libspec_manager.get_library_doc_or_error( completion_context.token_value_resolving_variables(token), create=True, - current_doc_uri=completion_context.doc.uri, + completion_context=completion_context, args=ast_utils.get_library_arguments_serialized(token_info.node), ).library_doc if library_doc is not None: diff --git a/robotframework-ls/src/robotframework_ls/impl/libspec_manager.py b/robotframework-ls/src/robotframework_ls/impl/libspec_manager.py index ffa656b2bb..0eebd8766a 100644 --- a/robotframework-ls/src/robotframework_ls/impl/libspec_manager.py +++ b/robotframework-ls/src/robotframework_ls/impl/libspec_manager.py @@ -5,7 +5,11 @@ import threading from typing import Optional, Dict, Set, Iterator, Union, Any from robocorp_ls_core.protocols import Sentinel, IEndPoint -from robotframework_ls.impl.protocols import ILibraryDoc, ILibraryDocOrError +from robotframework_ls.impl.protocols import ( + ILibraryDoc, + ILibraryDocOrError, + ICompletionContext, +) import itertools from robocorp_ls_core.watchdog_wrapper import IFSObserver from robotframework_ls.impl.robot_lsp_constants import ( @@ -1126,7 +1130,7 @@ def get_library_doc_or_error( self, libname: str, create: bool, - current_doc_uri: str, + completion_context: ICompletionContext, builtin: bool = False, args: Optional[str] = None, ) -> ILibraryDocOrError: @@ -1136,12 +1140,12 @@ def get_library_doc_or_error( absolute path to a .py file. """ from robotframework_ls.impl import robot_constants - - assert current_doc_uri is not None + from robotframework_ls.impl import ast_utils libname_lower = libname.lower() target_file: str = "" normalized_target_file: str = "" + pre_error_msg: str = "" config = self.config libraries_libdoc_needs_args_lower: Set[str] @@ -1171,15 +1175,24 @@ def get_library_doc_or_error( ResolveVariablesContext, ) - args = ResolveVariablesContext( - self.config, current_doc_uri - ).token_value_resolving_variables(args) + assert completion_context.config is config + + args, unresolved = ResolveVariablesContext( + completion_context + ).token_value_and_unresolved_resolving_variables( + ast_utils.create_token(args) + ) + + 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),) + ) args = args.replace("\\\\", "\\") if not builtin: found_target_filename = self._get_library_target_filename( - libname, current_doc_uri + libname, completion_context.doc.uri ) if found_target_filename: target_file = found_target_filename @@ -1252,7 +1265,7 @@ def get_library_doc_or_error( return self.get_library_doc_or_error( libname, create=False, - current_doc_uri=current_doc_uri, + completion_context=completion_context, builtin=builtin, args=args, ) @@ -1270,22 +1283,22 @@ def get_library_doc_or_error( return self.get_library_doc_or_error( libname, create=False, - current_doc_uri=current_doc_uri, + completion_context=completion_context, builtin=builtin, args=args, ) - return _LibraryDocOrError(None, error_msg) + return _LibraryDocOrError(None, pre_error_msg + error_msg) error_msg = self._get_cached_error( libname, is_builtin=builtin, target_file=target_file, args=args ) if error_msg: log.debug("Unable to get library named: %s. Reason: %s", libname, error_msg) - return _LibraryDocOrError(None, error_msg) + return _LibraryDocOrError(None, pre_error_msg + error_msg) msg = f"Unable to find library named: {libname}" log.debug(msg) - return _LibraryDocOrError(None, msg) + return _LibraryDocOrError(None, pre_error_msg + msg) class _LibraryDocOrError: diff --git a/robotframework-ls/src/robotframework_ls/impl/protocols.py b/robotframework-ls/src/robotframework_ls/impl/protocols.py index 168c1bf11d..3f7bb7ac46 100644 --- a/robotframework-ls/src/robotframework_ls/impl/protocols.py +++ b/robotframework-ls/src/robotframework_ls/impl/protocols.py @@ -12,6 +12,7 @@ Callable, Union, Hashable, + Dict, ) from robocorp_ls_core.protocols import ( Sentinel, @@ -159,7 +160,7 @@ class INode(Protocol): end_col_offset: int tokens: List[IRobotToken] - def get_token(self, name: str) -> IRobotToken: + def get_token(self, name: str) -> Optional[IRobotToken]: pass def get_tokens(self, name: str) -> List[IRobotToken]: @@ -898,6 +899,24 @@ def get_current_token(self) -> Optional[TokenInfo]: def get_all_variables(self) -> Tuple[NodeInfo, ...]: pass + def get_doc_normalized_var_name_to_var_found(self) -> Dict[str, "IVariableFound"]: + pass + + def get_settings_normalized_var_name_to_var_found( + self, + ) -> Dict[str, "IVariableFound"]: + pass + + def get_builtins_normalized_var_name_to_var_found( + self, resolved + ) -> Dict[str, "IVariableFound"]: + pass + + def get_arguments_files_normalized_var_name_to_var_found( + self, + ) -> Dict[str, "IVariableFound"]: + pass + def get_current_variable(self, section=None) -> Optional[VarTokenInfo]: """ Provides the current variable token. Note that it won't include '{' nor '}'. @@ -936,7 +955,7 @@ def get_variable_imports_as_docs( def get_imported_libraries(self) -> Tuple[ILibraryImportNode, ...]: pass - def token_value_resolving_variables(self, token: Union[str, IRobotToken]) -> str: + def token_value_resolving_variables(self, token: IRobotToken) -> str: pass def token_value_and_unresolved_resolving_variables( diff --git a/robotframework-ls/src/robotframework_ls/impl/signature_help.py b/robotframework-ls/src/robotframework_ls/impl/signature_help.py index f2efa5b33f..a4c43a217d 100644 --- a/robotframework-ls/src/robotframework_ls/impl/signature_help.py +++ b/robotframework-ls/src/robotframework_ls/impl/signature_help.py @@ -33,7 +33,7 @@ def _library_signature_help( library_doc = libspec_manager.get_library_doc_or_error( name, create=True, - current_doc_uri=completion_context.doc.uri, + completion_context=completion_context, builtin=False, args=args, ).library_doc diff --git a/robotframework-ls/src/robotframework_ls/impl/text_utilities.py b/robotframework-ls/src/robotframework_ls/impl/text_utilities.py index 7b3337986b..b9089815bd 100644 --- a/robotframework-ls/src/robotframework_ls/impl/text_utilities.py +++ b/robotframework-ls/src/robotframework_ls/impl/text_utilities.py @@ -33,7 +33,7 @@ def strip(self): # Note: this not only makes it faster, but also makes us use less memory as a # way to reuse the same 'interned" strings. @lru_cache(maxsize=2000) -def normalize_robot_name(text): +def normalize_robot_name(text: str) -> str: return text.lower().replace("_", "").replace(" ", "") diff --git a/robotframework-ls/src/robotframework_ls/impl/variable_completions.py b/robotframework-ls/src/robotframework_ls/impl/variable_completions.py index 31f8928a96..c00a97f0f7 100644 --- a/robotframework-ls/src/robotframework_ls/impl/variable_completions.py +++ b/robotframework-ls/src/robotframework_ls/impl/variable_completions.py @@ -19,8 +19,6 @@ VariableFoundFromToken, VariableFoundFromPythonAst, VariableFoundFromYaml, - VariableFoundFromSettings, - VariableFoundFromBuiltins, ) @@ -93,36 +91,19 @@ def __typecheckself__(self) -> None: _: IVariablesCollector = check_implements(self) -def _collect_completions_from_ast( +def _collect_variables_from_ast( ast, completion_context: ICompletionContext, collector: IVariablesCollector ): from robotframework_ls.impl import ast_utils from robotframework_ls.impl.variable_resolve import robot_search_variable - - completion_context.check_cancelled() from robot.api import Token - for variable_node_info in completion_context.get_all_variables(): - variable_node = variable_node_info.node - token = variable_node.get_token(Token.VARIABLE) - if token is None: - continue - - variable_match = robot_search_variable(token.value) - # Filter out empty base - if variable_match is None or not variable_match.base: - continue + completion_context.check_cancelled() - if collector.accepts(variable_match.base): - base_token = ast_utils.convert_variable_match_base_to_token( - token, variable_match - ) - variable_found = VariableFoundFromToken( - completion_context, - base_token, - variable_node.value, - variable_name=variable_match.base, - ) + for ( + variable_found + ) in completion_context.get_doc_normalized_var_name_to_var_found().values(): + if collector.accepts(variable_found.variable_name): collector.on_variable(variable_found) accept_sets_in = { @@ -178,7 +159,7 @@ def _collect_current_doc_variables( # Get keywords defined in the file itself ast = completion_context.get_ast() - _collect_completions_from_ast(ast, completion_context, collector) + _collect_variables_from_ast(ast, completion_context, collector) def _collect_resource_imports_variables( @@ -189,7 +170,7 @@ def _collect_resource_imports_variables( if resource_doc is None: continue new_ctx = completion_context.create_copy(resource_doc) - _collect_variables_from_document_context(new_ctx, collector) + _collect_global_variables_from_document_context(new_ctx, collector) def _gen_var_from_python_ast(variable_import_doc, collector, node, target): @@ -293,74 +274,83 @@ def _collect_variables_from_variable_import_doc( log.exception() -def _collect_variables_from_document_context( +def collect_global_variables_from_document_dependencies( completion_context: ICompletionContext, collector: IVariablesCollector, - only_current_doc=False, ): - completion_context.check_cancelled() - _collect_current_doc_variables(completion_context, collector) + dependency_graph = completion_context.collect_dependency_graph() - if not only_current_doc: - dependency_graph = completion_context.collect_dependency_graph() - - for resource_doc in completion_context.iter_dependency_and_init_resource_docs( - dependency_graph - ): - new_ctx = completion_context.create_copy(resource_doc) - _collect_current_doc_variables(new_ctx, collector) - - for node, variable_doc in dependency_graph.iter_all_variable_imports_as_docs(): - if variable_doc is None: - # Note that 'None' documents will only be given for the - # initial context (so, it's ok to use `completion_context` - # in this case). - from robot.api import Token - - node_name_tok = node.get_token(Token.NAME) - if node_name_tok is not None: - - ( - _value, - token_errors, - ) = completion_context.token_value_and_unresolved_resolving_variables( - node_name_tok - ) - - if token_errors: - for token_error 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`.", - ) + for resource_doc in completion_context.iter_dependency_and_init_resource_docs( + dependency_graph + ): + new_ctx = completion_context.create_copy(resource_doc) + _collect_current_doc_variables(new_ctx, collector) + + for node, variable_doc in dependency_graph.iter_all_variable_imports_as_docs(): + if variable_doc is None: + # Note that 'None' documents will only be given for the + # initial context (so, it's ok to use `completion_context` + # in this case). + from robot.api import Token + + node_name_tok = node.get_token(Token.NAME) + if node_name_tok is not None: + + ( + _value, + token_errors, + ) = completion_context.token_value_and_unresolved_resolving_variables( + node_name_tok + ) - else: + if token_errors: + for token_error in token_errors: collector.on_unresolved_variable_import( completion_context, node.name, - node_name_tok.lineno, - node_name_tok.lineno, - node_name_tok.col_offset, - node_name_tok.end_col_offset, - None, + 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`.", ) + else: collector.on_unresolved_variable_import( completion_context, node.name, - node.lineno, - node.end_lineno, - node.col_offset, - node.end_col_offset, + node_name_tok.lineno, + node_name_tok.lineno, + node_name_tok.col_offset, + node_name_tok.end_col_offset, None, ) - continue - _collect_variables_from_variable_import_doc(variable_doc, collector) + else: + collector.on_unresolved_variable_import( + completion_context, + node.name, + node.lineno, + node.end_lineno, + node.col_offset, + node.end_col_offset, + None, + ) + continue + _collect_variables_from_variable_import_doc(variable_doc, collector) + + +def _collect_global_variables_from_document_context( + completion_context: ICompletionContext, + collector: IVariablesCollector, + only_current_doc=False, +): + completion_context.check_cancelled() + _collect_current_doc_variables(completion_context, collector) + + if not only_current_doc: + collect_global_variables_from_document_dependencies( + completion_context, collector + ) def _collect_arguments( @@ -385,29 +375,6 @@ def _collect_arguments( collector.on_variable(variable_found) -def _collect_from_settings( - completion_context: ICompletionContext, collector: IVariablesCollector -): - from robotframework_ls.impl.robot_lsp_constants import OPTION_ROBOT_VARIABLES - - config = completion_context.config - if config is not None: - robot_variables = config.get_setting(OPTION_ROBOT_VARIABLES, dict, {}) - for key, val in robot_variables.items(): - if collector.accepts(key): - collector.on_variable(VariableFoundFromSettings(key, val)) - - -def _collect_from_builtins( - completion_context: ICompletionContext, collector: IVariablesCollector -): - from robotframework_ls.impl.robot_constants import get_builtin_variables - - for key, val in get_builtin_variables(): - if collector.accepts(key): - collector.on_variable(VariableFoundFromBuiltins(key, val)) - - def collect_variables( completion_context: ICompletionContext, collector: IVariablesCollector, @@ -420,16 +387,26 @@ def collect_variables( collect_global_variables(completion_context, collector, only_current_doc) -def _collect_from_arguments_files( - completion_context: ICompletionContext, collector: IVariablesCollector -): - if not completion_context.variables_from_arguments_files_loader: - return +def _collect_global_static_variables(completion_context, collector): + for ( + var_found + ) in completion_context.get_settings_normalized_var_name_to_var_found().values(): + if collector.accepts(var_found.variable_name): + collector.on_variable(var_found) + + for var_found in completion_context.get_builtins_normalized_var_name_to_var_found( + False + ).values(): + if collector.accepts(var_found.variable_name): + collector.on_variable(var_found) - for c in completion_context.variables_from_arguments_files_loader: - for variable in c.get_variables(): - if collector.accepts(variable.variable_name): - collector.on_variable(variable) + for ( + var_found + ) in ( + completion_context.get_arguments_files_normalized_var_name_to_var_found().values() + ): + if collector.accepts(var_found.variable_name): + collector.on_variable(var_found) def collect_global_variables( @@ -437,14 +414,12 @@ def collect_global_variables( collector: IVariablesCollector, only_current_doc=False, ): - _collect_variables_from_document_context( + _collect_global_variables_from_document_context( completion_context, collector, only_current_doc=only_current_doc ) if not only_current_doc: - _collect_from_settings(completion_context, collector) - _collect_from_builtins(completion_context, collector) - _collect_from_arguments_files(completion_context, collector) + _collect_global_static_variables(completion_context, collector) def collect_local_variables( diff --git a/robotframework-ls/src/robotframework_ls/impl/variable_resolve.py b/robotframework-ls/src/robotframework_ls/impl/variable_resolve.py index 6c89fa2758..44f4d299a2 100644 --- a/robotframework-ls/src/robotframework_ls/impl/variable_resolve.py +++ b/robotframework-ls/src/robotframework_ls/impl/variable_resolve.py @@ -1,9 +1,16 @@ import os from functools import lru_cache from typing import Optional, Union, Tuple, List, Iterator -from robotframework_ls.impl.protocols import IRobotVariableMatch, IRobotToken -from robocorp_ls_core.protocols import Sentinel, IConfig +from robotframework_ls.impl.protocols import ( + IRobotVariableMatch, + IRobotToken, + ICompletionContext, + AbstractVariablesCollector, + IVariableFound, +) +from robocorp_ls_core.protocols import Sentinel from robocorp_ls_core.robotframework_log import get_logger +import threading log = get_logger(__name__) @@ -115,21 +122,31 @@ def _not_escaping(name): return backslashes % 2 == 0 +class _VariablesCollector(AbstractVariablesCollector): + def __init__(self): + self.var_name_to_var_found = {} + + def accepts(self, variable_name: str) -> bool: + return True + + def on_variable(self, variable_found: IVariableFound): + from robotframework_ls.impl.text_utilities import normalize_robot_name + + self.var_name_to_var_found[ + normalize_robot_name(variable_found.variable_name) + ] = variable_found + + class ResolveVariablesContext: - def __init__(self, config: Optional[IConfig], doc_path: str): - self.config = config - self.doc_path = doc_path + _thread_local = threading.local() - def _resolve_builtin(self, var_name, value_if_not_found, log_info): - from robotframework_ls.impl.robot_constants import BUILTIN_VARIABLES_RESOLVED + def __init__(self, completion_context: ICompletionContext): + self.config = completion_context.config + self.completion_context = completion_context - ret = BUILTIN_VARIABLES_RESOLVED.get(var_name, Sentinel.SENTINEL) - if ret is Sentinel.SENTINEL: - if var_name == "CURDIR": - return os.path.dirname(self.doc_path) - log.info(*log_info) - return value_if_not_found - return ret + @property + def doc_path(self) -> str: + return self.completion_context.doc.path def _resolve_environment_variable(self, var_name, value_if_not_found, log_info): ret = os.environ.get(var_name, Sentinel.SENTINEL) @@ -139,29 +156,79 @@ def _resolve_environment_variable(self, var_name, value_if_not_found, log_info): return ret def _convert_robot_variable(self, var_name, value_if_not_found): - from robotframework_ls.impl.robot_lsp_constants import OPTION_ROBOT_VARIABLES + from robotframework_ls.impl.text_utilities import normalize_robot_name + + normalized = normalize_robot_name(var_name) + + if normalized == "curdir": + return os.path.dirname(self.doc_path) + + completion_context = self.completion_context + found = completion_context.get_doc_normalized_var_name_to_var_found().get( + normalized + ) + + if found is not None: + return found.variable_value + + found = completion_context.get_settings_normalized_var_name_to_var_found().get( + normalized + ) + if found is not None: + return found.variable_value + + found = completion_context.get_arguments_files_normalized_var_name_to_var_found().get( + normalized + ) + if found is not None: + return found.variable_value + + found = completion_context.get_builtins_normalized_var_name_to_var_found( + True + ).get(normalized) + if found is not None: + return found.variable_value + + # At this point we have to do a search in the imports to check whether + # maybe the variable is defined in a dependency. Note that we can get + # into a recursion here as we may need to resolve other variables + # in order to get here. + # It goes something like: + # Resolve variable -> some variable + # collect dependencies (which may need to resolve variables again). + # In this case we have a thread-local variable to know whether this is the + # case and if it is bail out. + try: + resolve_info = self._thread_local.resolve_info + except: + resolve_info = self._thread_local.resolve_info = set() + + if var_name in resolve_info: + log.info("Unable to find robot variable: %s", var_name) + return value_if_not_found - if self.config is None: - value = self._resolve_builtin( - var_name, - value_if_not_found, - ( - "Config not available while trying to convert robot variable: %s", - var_name, - ), - ) else: - robot_variables = self.config.get_setting(OPTION_ROBOT_VARIABLES, dict, {}) - value = robot_variables.get(var_name, Sentinel.SENTINEL) - if value is Sentinel.SENTINEL: - value = self._resolve_builtin( - var_name, - value_if_not_found, - ("Unable to find robot variable: %s", var_name), + try: + resolve_info.add(var_name) + + from robotframework_ls.impl.variable_completions import ( + collect_global_variables_from_document_dependencies, ) - value = str(value) - return value + collector = _VariablesCollector() + + collect_global_variables_from_document_dependencies( + completion_context, collector + ) + + found = collector.var_name_to_var_found.get(normalized) + if found is not None: + return found.variable_value + + log.info("Unable to find robot variable: %s", var_name) + return value_if_not_found + finally: + resolve_info.discard(var_name) def _convert_environment_variable(self, var_name, value_if_not_found): from robotframework_ls.impl.robot_lsp_constants import OPTION_ROBOT_PYTHON_ENV @@ -188,46 +255,12 @@ def _convert_environment_variable(self, var_name, value_if_not_found): value = str(value) return value - def token_value_resolving_variables(self, token: Union[str, IRobotToken]) -> str: - from robotframework_ls.impl import ast_utils - - robot_token: IRobotToken - if isinstance(token, str): - robot_token = ast_utils.create_token(token) - else: - robot_token = token - - try: - tokenized_vars = ast_utils.tokenize_variables(robot_token) - except: - return robot_token.value # Unable to tokenize - - parts = [] - for v in tokenized_vars: - if v.type == v.NAME: - parts.append(str(v)) - - elif v.type == v.VARIABLE: - # Resolve variable from config - initial_v = v = str(v) - if v.startswith("${") and v.endswith("}"): - v = v[2:-1] - parts.append(self._convert_robot_variable(v, initial_v)) - elif v.startswith("%{") and v.endswith("}"): - v = v[2:-1] - parts.append(self._convert_environment_variable(v, initial_v)) - else: - log.info("Cannot resolve variable: %s", v) - parts.append(v) # Leave unresolved. - - joined_parts = "".join(parts) - return joined_parts + 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, ...]]: - unresolved: List[IRobotToken] = [] - from robotframework_ls.impl import ast_utils robot_token: IRobotToken @@ -241,33 +274,38 @@ def token_value_and_unresolved_resolving_variables( except: return robot_token.value, () # Unable to tokenize - parts = [] + unresolved: List[IRobotToken] = [] + + parts: List[str] = [] + tok: IRobotToken + value: str for tok in tokenized_vars: if tok.type == tok.NAME: parts.append(str(tok)) elif tok.type == tok.VARIABLE: - # Resolve variable from config - initial_v = v = str(tok) - if v.startswith("${") and v.endswith("}"): - v = v[2:-1] - converted = self._convert_robot_variable(v, initial_v) - parts.append(converted) - if converted == initial_v: - # Unable to resolve - unresolved.append(tok) + value = str(tok) - elif v.startswith("%{") and v.endswith("}"): - v = v[2:-1] - converted = self._convert_environment_variable(v, initial_v) - parts.append(converted) - if converted == initial_v: - # Unable to resolve - unresolved.append(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. else: - log.info("Cannot resolve variable: %s", v) - parts.append(v) # Leave unresolved. + 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) joined_parts = "".join(parts) return joined_parts, tuple(unresolved) diff --git a/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/.vscode/settings.json b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/.vscode/settings.json new file mode 100644 index 0000000000..677e64ca8b --- /dev/null +++ b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.terminal.activateEnvironment": false +} \ No newline at end of file diff --git a/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/LibWithParams3.py b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/LibWithParams3.py new file mode 100644 index 0000000000..4354746c4d --- /dev/null +++ b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/LibWithParams3.py @@ -0,0 +1,6 @@ +class LibWithParams3: + def __init__(self, resolved_name): + assert resolved_name == "RESOLVED" + + def check(self): + return 1 diff --git a/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/LibWithParams4.py b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/LibWithParams4.py new file mode 100644 index 0000000000..2555577827 --- /dev/null +++ b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/LibWithParams4.py @@ -0,0 +1,6 @@ +class LibWithParams4: + def __init__(self, resolved_name): + assert resolved_name == "RESOLVED" + + def check(self): + return 1 diff --git a/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/case_params_on_lib3.robot b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/case_params_on_lib3.robot new file mode 100644 index 0000000000..5b2dee0fae --- /dev/null +++ b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/case_params_on_lib3.robot @@ -0,0 +1,10 @@ +*** Settings *** +Library LibWithParams3 ${resolve_this} + + +*** Variables *** +${resolve_this}= RESOLVED + +*** Test Case *** +Test name + LibWithParams3.Check \ No newline at end of file diff --git a/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/case_params_on_lib4.robot b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/case_params_on_lib4.robot new file mode 100644 index 0000000000..12dc5b3ac6 --- /dev/null +++ b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/case_params_on_lib4.robot @@ -0,0 +1,8 @@ +*** Settings *** +Resource ./vars_resource.resource +Library LibWithParams4 ${resolve_this} + + +*** Test Case *** +Test name + LibWithParams4.Check \ No newline at end of file diff --git a/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/vars_resource.resource b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/vars_resource.resource new file mode 100644 index 0000000000..e3b64c78d9 --- /dev/null +++ b/robotframework-ls/tests/robotframework_ls_tests/_resources/case_params_on_lib/vars_resource.resource @@ -0,0 +1,2 @@ +*** Variables *** +${resolve_this}= RESOLVED diff --git a/robotframework-ls/tests/robotframework_ls_tests/completions/test_auto_import.py b/robotframework-ls/tests/robotframework_ls_tests/completions/test_auto_import.py index 08a7c6c8f8..bcfcfb2437 100644 --- a/robotframework-ls/tests/robotframework_ls_tests/completions/test_auto_import.py +++ b/robotframework-ls/tests/robotframework_ls_tests/completions/test_auto_import.py @@ -117,16 +117,15 @@ def test_completion_with_auto_import_import_not_duplicated_case1_library( Verify another m""", ) + completion_context = CompletionContext(doc, workspace=workspace.ws) # Needed to pre-generate the information workspace.ws.libspec_manager.get_library_doc_or_error( libname="case1_library", create=True, - current_doc_uri=workspace.get_doc("case1.robot").uri, + completion_context=completion_context, ) - completions = auto_import_completions.complete( - CompletionContext(doc, workspace=workspace.ws), {} - ) + completions = auto_import_completions.complete(completion_context, {}) assert len(completions) == 0 @@ -187,18 +186,16 @@ def test_completion_with_auto_import_case1_library_imported_1( from robotframework_ls.impl.completion_context import CompletionContext doc = setup_case2_doc - + completion_context = CompletionContext(doc, workspace=workspace.ws) # Needed to pre-generate the information workspace.ws.libspec_manager.get_library_doc_or_error( libname="case1_library", create=True, - current_doc_uri=workspace.get_doc("case1.robot").uri, + completion_context=completion_context, ) # Get completions from the user library adding the *** Settings *** - completions = auto_import_completions.complete( - CompletionContext(doc, workspace=workspace.ws), {} - ) + completions = auto_import_completions.complete(completion_context, {}) assert len(completions) == 1 apply_completion(doc, completions[0]) @@ -221,22 +218,23 @@ def test_completion_with_auto_import_case1_library_imported_2( from robotframework_ls.impl.completion_context import CompletionContext doc = setup_case2_doc - # Needed to pre-generate the information - workspace.ws.libspec_manager.get_library_doc_or_error( - libname="case1_library", - create=True, - current_doc_uri=workspace.get_doc("case1.robot").uri, - ) - # Get completions from the user library adding the existing *** Settings *** doc.source = """*** Settings *** *** Test Cases *** User can call library Verify another m""" - completions = auto_import_completions.complete( - CompletionContext(doc, workspace=workspace.ws), {} + completion_context = CompletionContext(doc, workspace=workspace.ws) + + # Needed to pre-generate the information + libdoc_or_error = workspace.ws.libspec_manager.get_library_doc_or_error( + libname="case1_library", + create=True, + completion_context=CompletionContext(workspace.get_doc("case1.robot")), ) + assert not libdoc_or_error.error + + completions = auto_import_completions.complete(completion_context, {}) assert len(completions) == 1 apply_completion(doc, completions[0]) @@ -267,16 +265,17 @@ def test_completion_with_auto_import_case1_library_imported_3( User can call library Verify another m""" + completion_context = CompletionContext(doc, workspace=workspace.ws) + # Needed to pre-generate the information - workspace.ws.libspec_manager.get_library_doc_or_error( + libdoc_or_error = workspace.ws.libspec_manager.get_library_doc_or_error( libname="case1_library", create=True, - current_doc_uri=workspace.get_doc("case1.robot").uri, + completion_context=CompletionContext(workspace.get_doc("case1.robot")), ) + assert not libdoc_or_error.error - completions = auto_import_completions.complete( - CompletionContext(doc, workspace=workspace.ws), {} - ) + completions = auto_import_completions.complete(completion_context, {}) assert len(completions) == 1 apply_completion(doc, completions[0]) diff --git a/robotframework-ls/tests/robotframework_ls_tests/completions/test_keyword_completions.py b/robotframework-ls/tests/robotframework_ls_tests/completions/test_keyword_completions.py index 8454475948..4c94915813 100644 --- a/robotframework-ls/tests/robotframework_ls_tests/completions/test_keyword_completions.py +++ b/robotframework-ls/tests/robotframework_ls_tests/completions/test_keyword_completions.py @@ -571,18 +571,21 @@ def test_typing_not_shown(libspec_manager, workspace, data_regression, workspace with open(os.path.join(workspace_dir_a, "my.libspec"), "w") as stream: stream.write(LIBSPEC_3) libspec_manager.add_workspace_folder(uris.from_fs_path(workspace_dir_a)) + workspace.set_root(workspace_dir, libspec_manager=libspec_manager) + + doc = workspace.ws.put_document(TextDocumentItem("temp_doc.robot", text="")) assert ( libspec_manager.get_library_doc_or_error( "case3_library", False, - uris.from_fs_path(os.path.join(workspace_dir_a, "my.robot")), + CompletionContext( + doc, + workspace=workspace.ws, + ), ).library_doc is not None ) - workspace.set_root(workspace_dir, libspec_manager=libspec_manager) - - doc = workspace.ws.put_document(TextDocumentItem("temp_doc.robot", text="")) doc.source = """*** Settings *** Library case3_library @@ -668,7 +671,7 @@ def test_keyword_completions_lib_with_params_slash(workspace, libspec_manager, c doc = workspace.get_doc("case_params_on_lib2.robot") completions = keyword_completions.complete( - CompletionContext(doc, workspace=workspace.ws) + CompletionContext(doc, workspace=workspace.ws, config=config) ) if sys.platform == "win32": expected = r"My:\foo\bar\echo (LibWithParams2)" diff --git a/robotframework-ls/tests/robotframework_ls_tests/test_code_analysis.py b/robotframework-ls/tests/robotframework_ls_tests/test_code_analysis.py index 2447153403..150e78c0a6 100644 --- a/robotframework-ls/tests/robotframework_ls_tests/test_code_analysis.py +++ b/robotframework-ls/tests/robotframework_ls_tests/test_code_analysis.py @@ -363,6 +363,54 @@ def test_code_analysis_lib_with_params( _collect_errors(workspace, doc, data_regression, basename="no_error", config=config) +def test_code_analysis_lib_with_params_2( + workspace, libspec_manager, cases, data_regression +): + from robotframework_ls.robot_config import RobotConfig + + workspace.set_root("case_params_on_lib", libspec_manager=libspec_manager) + + caseroot = cases.get_path("case_params_on_lib") + config = RobotConfig() + config.update( + { + "robot": { + "pythonpath": [caseroot], + "libraries": {"libdoc": {"needsArgs": ["*"]}}, + } + } + ) + libspec_manager.config = config + + doc = workspace.get_doc("case_params_on_lib3.robot") + + _collect_errors(workspace, doc, data_regression, basename="no_error", config=config) + + +def test_code_analysis_lib_with_params_3( + workspace, libspec_manager, cases, data_regression +): + from robotframework_ls.robot_config import RobotConfig + + workspace.set_root("case_params_on_lib", libspec_manager=libspec_manager) + + caseroot = cases.get_path("case_params_on_lib") + config = RobotConfig() + config.update( + { + "robot": { + "pythonpath": [caseroot], + "libraries": {"libdoc": {"needsArgs": ["*"]}}, + } + } + ) + libspec_manager.config = config + + doc = workspace.get_doc("case_params_on_lib4.robot") + + _collect_errors(workspace, doc, data_regression, basename="no_error", config=config) + + def test_code_analysis_same_lib_multiple_with_alias( workspace, libspec_manager, data_regression ): diff --git a/robotframework-ls/tests/robotframework_ls_tests/test_find_definition/test_resolve_vars_in_libdoc_init.yml b/robotframework-ls/tests/robotframework_ls_tests/test_find_definition/test_resolve_vars_in_libdoc_init.yml new file mode 100644 index 0000000000..dfd675ae6f --- /dev/null +++ b/robotframework-ls/tests/robotframework_ls_tests/test_find_definition/test_resolve_vars_in_libdoc_init.yml @@ -0,0 +1,5 @@ +- col_offset: 0 + end_col_offset: 12 + end_lineno: 1 + lineno: 1 + source: my.resource diff --git a/robotframework-ls/tests/robotframework_ls_tests/test_libspec_manager.py b/robotframework-ls/tests/robotframework_ls_tests/test_libspec_manager.py index 138a0a9ff0..ba5c2b07be 100644 --- a/robotframework-ls/tests/robotframework_ls_tests/test_libspec_manager.py +++ b/robotframework-ls/tests/robotframework_ls_tests/test_libspec_manager.py @@ -7,11 +7,15 @@ def test_libspec_info(libspec_manager, tmpdir): from robotframework_ls.impl.robot_specbuilder import LibraryDoc from robotframework_ls.impl.robot_specbuilder import KeywordDoc + from robotframework_ls.impl.completion_context import CompletionContext + from robotframework_ls.impl.robot_workspace import RobotDocument assert "BuiltIn" in libspec_manager.get_library_names() uri = uris.from_fs_path(str(tmpdir.join("case.robot"))) lib_info = libspec_manager.get_library_doc_or_error( - "BuiltIn", create=False, current_doc_uri=uri + "BuiltIn", + create=False, + completion_context=CompletionContext(RobotDocument(uri, "")), ).library_doc assert isinstance(lib_info, LibraryDoc) assert lib_info.source is not None @@ -49,6 +53,8 @@ def test_libspec(libspec_manager, workspace_dir, data_regression): from robotframework_ls.impl.robot_specbuilder import LibraryDoc from robotframework_ls.impl.robot_specbuilder import KeywordDoc from typing import List + from robotframework_ls.impl.completion_context import CompletionContext + from robotframework_ls.impl.robot_workspace import RobotDocument os.makedirs(workspace_dir) libspec_manager.add_additional_pythonpath_folder(workspace_dir) @@ -79,7 +85,7 @@ def method6(): uri = uris.from_fs_path(os.path.join(workspace_dir, "case.robot")) library_info: Optional[LibraryDoc] = libspec_manager.get_library_doc_or_error( - "check_lib", True, uri + "check_lib", True, CompletionContext(RobotDocument(uri, "")) ).library_doc assert library_info is not None keywords: List[KeywordDoc] = library_info.keywords @@ -93,6 +99,8 @@ def test_libspec_rest(libspec_manager, workspace_dir, data_regression): from robotframework_ls.impl.robot_specbuilder import LibraryDoc from robotframework_ls.impl.robot_specbuilder import KeywordDoc from typing import List + from robotframework_ls.impl.robot_workspace import RobotDocument + from robotframework_ls.impl.completion_context import CompletionContext os.makedirs(workspace_dir) libspec_manager.add_additional_pythonpath_folder(workspace_dir) @@ -118,7 +126,7 @@ def my_method(self) -> None: uri = uris.from_fs_path(os.path.join(workspace_dir, "case.robot")) library_info: Optional[LibraryDoc] = libspec_manager.get_library_doc_or_error( - "CheckLib", True, uri + "CheckLib", True, CompletionContext(RobotDocument(uri, "")) ).library_doc assert library_info is not None @@ -132,6 +140,8 @@ def test_libspec_cache_no_lib(libspec_manager, workspace_dir): import time from robocorp_ls_core.basic import wait_for_condition import sys + from robotframework_ls.impl.completion_context import CompletionContext + from robotframework_ls.impl.robot_workspace import RobotDocument os.makedirs(workspace_dir) libspec_manager.add_additional_pythonpath_folder(workspace_dir) @@ -141,7 +151,7 @@ def disallow_cached_create_libspec(*args, **kwargs): uri = uris.from_fs_path(os.path.join(workspace_dir, "case.robot")) library_info: Optional[LibraryDoc] = libspec_manager.get_library_doc_or_error( - "check_lib", True, uri + "check_lib", True, CompletionContext(RobotDocument(uri, "")) ).library_doc assert library_info is None @@ -149,7 +159,7 @@ def disallow_cached_create_libspec(*args, **kwargs): original_cached_create_libspec = libspec_manager._cached_create_libspec libspec_manager._cached_create_libspec = disallow_cached_create_libspec library_info: Optional[LibraryDoc] = libspec_manager.get_library_doc_or_error( - "check_lib", True, uri + "check_lib", True, CompletionContext(RobotDocument(uri, "")) ).library_doc assert library_info is None libspec_manager._cached_create_libspec = original_cached_create_libspec @@ -170,7 +180,7 @@ def method2(a:int): # Check that the cache invalidation is in place! wait_for_condition( lambda: libspec_manager.get_library_doc_or_error( - "check_lib", True, uri + "check_lib", True, CompletionContext(RobotDocument(uri, "")) ).library_doc is not None, msg="Did not recreate library in the available timeout.", @@ -180,6 +190,8 @@ def method2(a:int): def test_libspec_no_rest(libspec_manager, workspace_dir): from robotframework_ls.impl.robot_specbuilder import LibraryDoc + from robotframework_ls.impl.robot_workspace import RobotDocument + from robotframework_ls.impl.completion_context import CompletionContext os.makedirs(workspace_dir) libspec_manager.add_additional_pythonpath_folder(workspace_dir) @@ -233,7 +245,7 @@ def raise_error(cmdline, *args, **kwargs): uri = uris.from_fs_path(os.path.join(workspace_dir, "case.robot")) library_info: Optional[LibraryDoc] = libspec_manager.get_library_doc_or_error( - "check_lib", True, uri + "check_lib", True, CompletionContext(RobotDocument(uri, "")) ).library_doc assert library_info is not None @@ -245,6 +257,8 @@ def test_libspec_manager_caches(libspec_manager, workspace_dir): from robotframework_ls_tests.fixtures import LIBSPEC_2_A import time from robocorp_ls_core.unittest_tools.fixtures import wait_for_test_condition + from robotframework_ls.impl.completion_context import CompletionContext + from robotframework_ls.impl.robot_workspace import RobotDocument workspace_dir_a = os.path.join(workspace_dir, "workspace_dir_a") os.makedirs(workspace_dir_a) @@ -254,14 +268,16 @@ def test_libspec_manager_caches(libspec_manager, workspace_dir): uri = uris.from_fs_path(os.path.join(workspace_dir, "case.robot")) assert ( libspec_manager.get_library_doc_or_error( - "case1_library", create=False, current_doc_uri=uri + "case1_library", + create=False, + completion_context=CompletionContext(RobotDocument(uri, "")), ).library_doc is not None ) libspec_manager.remove_workspace_folder(uris.from_fs_path(workspace_dir_a)) library_info = libspec_manager.get_library_doc_or_error( - "case1_library", False, uri + "case1_library", False, CompletionContext(RobotDocument(uri, "")) ).library_doc if library_info is not None: raise AssertionError( @@ -272,7 +288,7 @@ def test_libspec_manager_caches(libspec_manager, workspace_dir): libspec_manager.add_workspace_folder(uris.from_fs_path(workspace_dir_a)) assert ( libspec_manager.get_library_doc_or_error( - "case1_library", False, uri + "case1_library", False, CompletionContext(RobotDocument(uri, "")) ).library_doc is not None ) @@ -285,7 +301,7 @@ def test_libspec_manager_caches(libspec_manager, workspace_dir): def check_spec_found(): library_info = libspec_manager.get_library_doc_or_error( - "case2_library", False, uri + "case2_library", False, CompletionContext(RobotDocument(uri, "")) ).library_doc return library_info is not None @@ -293,7 +309,7 @@ def check_spec_found(): wait_for_test_condition(check_spec_found, sleep=1 / 5.0) library_info = libspec_manager.get_library_doc_or_error( - "case2_library", False, uri + "case2_library", False, CompletionContext(RobotDocument(uri, "")) ).library_doc assert set(x.name for x in library_info.keywords) == set( ["Case 2 Verify Another Model", "Case 2 Verify Model"] @@ -307,7 +323,7 @@ def check_spec_found(): def check_spec_2_a(): library_info = libspec_manager.get_library_doc_or_error( - "case2_library", False, uri + "case2_library", False, CompletionContext(RobotDocument(uri, "")) ).library_doc if library_info: return set(x.name for x in library_info.keywords) == set( @@ -326,7 +342,9 @@ def test_libspec_manager_basic(workspace, libspec_manager): doc = workspace.get_doc("case1.robot") def get_library_doc_or_error(*args, **kwargs): - kwargs["current_doc_uri"] = doc.uri + from robotframework_ls.impl.completion_context import CompletionContext + + kwargs["completion_context"] = CompletionContext(doc) if "create" not in kwargs: kwargs["create"] = True return libspec_manager.get_library_doc_or_error(*args, **kwargs) diff --git a/robotframework-ls/tests/robotframework_ls_tests/test_workspace_symbol.py b/robotframework-ls/tests/robotframework_ls_tests/test_workspace_symbol.py index 5e8e6ab90b..c0e230310e 100644 --- a/robotframework-ls/tests/robotframework_ls_tests/test_workspace_symbol.py +++ b/robotframework-ls/tests/robotframework_ls_tests/test_workspace_symbol.py @@ -47,18 +47,21 @@ def test_workspace_symbols_same_basename(workspace, libspec_manager): from robotframework_ls.impl.completion_context import BaseContext from robocorp_ls_core.constants import NULL from robocorp_ls_core.config import Config + from robotframework_ls.impl.completion_context import CompletionContext workspace.set_root("case_same_basename", libspec_manager=libspec_manager) # Needed to pre-generate the information libspec_manager.get_library_doc_or_error( libname="my_library", create=True, - current_doc_uri=workspace.get_doc("tasks1.robot").uri, + completion_context=CompletionContext(workspace.get_doc("tasks1.robot")), ) libspec_manager.get_library_doc_or_error( libname="my_library", create=True, - current_doc_uri=workspace.get_doc("directory/tasks2.robot").uri, + completion_context=CompletionContext( + workspace.get_doc("directory/tasks2.robot") + ), ) config = Config()