diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 1b036c3f..98fd1649 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -16,8 +16,14 @@ jobs: with: python-version: "${{ matrix.python-version }}" - # Include the appropriate package manager steps based on the selection - + # Add Go bin directory to PATH for this workflow + # GITHUB_PATH is a special file that GitHub Actions uses to modify PATH + # Writing to this file adds the directory to the PATH for subsequent steps + - name: Install gopls + run: | + go install golang.org/x/tools/gopls@latest + echo "$HOME/go/bin" >> $GITHUB_PATH + - name: Install uv run: curl -LsSf https://astral.sh/uv/install.sh | sh - name: Create virtual environment diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f32d63a..258e3b5c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,7 @@ "pixi", "sensai", "vibing" - ] + ], + "basedpyright.disableLanguageServices": true, + "python.languageServer": "None" } \ No newline at end of file diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py index 3845ba84..d797d132 100644 --- a/src/multilspy/language_server.py +++ b/src/multilspy/language_server.py @@ -32,6 +32,7 @@ from .lsp_protocol_handler import lsp_types as LSPTypes from .lsp_protocol_handler.lsp_constants import LSPConstants from .lsp_protocol_handler.lsp_types import SymbolKind from .lsp_protocol_handler.server import ( + Error, LanguageServerHandler, ProcessLaunchInfo, ) @@ -614,7 +615,6 @@ class LanguageServer: ) except Exception as e: # Catch LSP internal error (-32603) and raise a more informative exception - from multilspy.lsp_protocol_handler.server import Error if isinstance(e, Error) and getattr(e, 'code', None) == -32603: raise RuntimeError( f"LSP internal error (-32603) when requesting references for {relative_file_path}:{line}:{column}. " diff --git a/src/multilspy/language_servers/gopls/gopls.py b/src/multilspy/language_servers/gopls/gopls.py index 6dbe88cd..f17ffcf8 100644 --- a/src/multilspy/language_servers/gopls/gopls.py +++ b/src/multilspy/language_servers/gopls/gopls.py @@ -5,13 +5,15 @@ import os import pathlib import subprocess from contextlib import asynccontextmanager -from typing import AsyncIterator +from typing import AsyncIterator, List from overrides import override +from multilspy import multilspy_types +from multilspy.multilspy_exceptions import MultilspyException from multilspy.multilspy_logger import MultilspyLogger from multilspy.language_server import LanguageServer -from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo +from multilspy.lsp_protocol_handler.server import Error, ProcessLaunchInfo from multilspy.lsp_protocol_handler.lsp_types import InitializeParams from multilspy.multilspy_config import MultilspyConfig @@ -57,28 +59,21 @@ class Gopls(LanguageServer): Check if required Go runtime dependencies are available. Raises RuntimeError with helpful message if dependencies are missing. """ - missing_deps = [] - - # Check for Go installation go_version = cls._get_go_version() if not go_version: - missing_deps.append(("Go", "https://golang.org/doc/install")) + raise RuntimeError("Go is not installed. Please install Go from https://golang.org/doc/install and make sure it is added to your PATH.") - # Check for gopls gopls_version = cls._get_gopls_version() if not gopls_version: - missing_deps.append(("gopls", "https://pkg.go.dev/golang.org/x/tools/gopls#section-readme")) - - if missing_deps: - error_msg = "Missing required dependencies:\n" - for dep, install_url in missing_deps: - error_msg += f"- {dep}: Please install from {install_url}\n" - raise RuntimeError(error_msg) + raise RuntimeError( + "Found a Go version but gopls is not installed.\n" + "Please install gopls as described in https://pkg.go.dev/golang.org/x/tools/gopls#section-readme\n\n" + "After installation, make sure it is added to your PATH (it might be installed in a different location than Go)." + ) return True def __init__(self, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str): - # Check runtime dependencies before initializing self.setup_runtime_dependency() super().__init__( @@ -159,3 +154,20 @@ class Gopls(LanguageServer): await self.server.shutdown() await self.server.stop() + + @override + async def request_references( + self, relative_file_path: str, line: int, column: int + ) -> List[multilspy_types.Location]: + NUM_COLS_TO_TRY = 10 + """selectionRange in Gopls always return the wrong column, + the one at the beginnning of the declaration instead of the symbol. We do a dirty hack here. + See https://github.com/golang/go/issues/73521 + """ + actual_column = column + for actual_column in range(column, column+NUM_COLS_TO_TRY): + try: + return await super().request_references(relative_file_path, line, actual_column) + except (Error, RuntimeError, MultilspyException) as e: + self.logger.log(f"Cannot find symbol at {line=}, column={actual_column}, trying to bump column by 1", logging.INFO) + raise RuntimeError(f"Failed to find references for symbol at {line=}, {column=}") diff --git a/src/multilspy/lsp_protocol_handler/server.py b/src/multilspy/lsp_protocol_handler/server.py index b9dd0de8..81a9dc4a 100644 --- a/src/multilspy/lsp_protocol_handler/server.py +++ b/src/multilspy/lsp_protocol_handler/server.py @@ -38,6 +38,7 @@ from typing import Any, Dict, List, Optional, Union from .lsp_requests import LspNotification, LspRequest from .lsp_types import ErrorCodes +from ..multilspy_exceptions import MultilspyException StringDict = Dict[str, Any] PayloadLike = Union[List[StringDict], StringDict, None] @@ -481,7 +482,7 @@ class LanguageServerHandler: await self._send_payload(make_request(method, request_id, params)) await request.cv.wait() if isinstance(request.error, Error): - raise request.error + raise MultilspyException(f"Could not process request {method} with params:\n{params}.\n Language server error: {request.error}") from request.error return request.result def _send_payload_sync(self, payload: StringDict) -> None: diff --git a/src/multilspy/multilspy_utils.py b/src/multilspy/multilspy_utils.py index c87f6e65..75f023a8 100644 --- a/src/multilspy/multilspy_utils.py +++ b/src/multilspy/multilspy_utils.py @@ -17,6 +17,7 @@ from enum import Enum from multilspy.multilspy_exceptions import MultilspyException from pathlib import PurePath, Path from multilspy.multilspy_logger import MultilspyLogger +from multilspy.multilspy_types import UnifiedSymbolInformation class TextUtils: @@ -322,3 +323,13 @@ class PlatformUtils: except (FileNotFoundError, subprocess.CalledProcessError): raise MultilspyException("dotnet or mono not found on the system") + +class SymbolUtils: + @staticmethod + def symbol_tree_contains_name(roots: list[UnifiedSymbolInformation], name: str) -> bool: + for symbol in roots: + if symbol["name"] == name: + return True + if SymbolUtils.symbol_tree_contains_name(symbol["children"], name): + return True + return False diff --git a/test/conftest.py b/test/conftest.py index 0c6321da..4e403dfe 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -59,7 +59,7 @@ def language_server(request: LanguageParamRequest): Example: ``` @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_python_server(language_server): + def test_python_server(language_server: SyncLanguageServer) -> None: # Use the Python language server pass ``` @@ -67,7 +67,7 @@ def language_server(request: LanguageParamRequest): You can also test multiple languages in a single test: ``` @pytest.mark.parametrize("language_server", [Language.PYTHON, Language.TYPESCRIPT], indirect=True) - def test_multiple_languages(language_server): + def test_multiple_languages(language_server: SyncLanguageServer) -> None: # This test will run once for each language pass ``` diff --git a/test/multilspy/go/test_go_basic.py b/test/multilspy/go/test_go_basic.py new file mode 100644 index 00000000..a87e9fe1 --- /dev/null +++ b/test/multilspy/go/test_go_basic.py @@ -0,0 +1,32 @@ +import os + +import pytest + +from multilspy import SyncLanguageServer +from multilspy.multilspy_config import Language +from multilspy.multilspy_utils import SymbolUtils + + +class TestGoLanguageServer: + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) + def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Helper"), "Helper function not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoStruct"), "DemoStruct not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.GO], indirect=True) + def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + file_path = os.path.join("main.go") + symbols = language_server.request_document_symbols(file_path) + helper_symbol = None + for sym in symbols[0]: + if sym.get("name") == "Helper": + helper_symbol = sym + break + assert helper_symbol is not None, "Could not find 'Helper' function symbol in main.go" + sel_start = helper_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "main.go" in ref.get("relativePath", "") for ref in refs + ), "main.go should reference Helper (tried all positions in selectionRange)" diff --git a/test/multilspy/java/test_java_basic.py b/test/multilspy/java/test_java_basic.py index d66753f1..8dcf4efd 100644 --- a/test/multilspy/java/test_java_basic.py +++ b/test/multilspy/java/test_java_basic.py @@ -2,29 +2,21 @@ import os import pytest +from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language - - -def find_symbol_recursive(symbols, name): - for symbol in symbols: - if symbol.get("name") == name: - return True - if symbol.get("children"): - if find_symbol_recursive(symbol["children"], name): - return True - return False +from multilspy.multilspy_utils import SymbolUtils class TestJavaLanguageServer: @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_find_symbol(self, language_server): + def test_find_symbol(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert find_symbol_recursive(symbols, "Main"), "Main class not found in symbol tree" - assert find_symbol_recursive(symbols, "Utils"), "Utils class not found in symbol tree" - assert find_symbol_recursive(symbols, "Model"), "Model class not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils class not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model class not found in symbol tree" @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_find_referencing_symbols(self, language_server): + def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: # Use correct Maven/Java file paths file_path = os.path.join("src", "main", "java", "test_repo", "Utils.java") refs = language_server.request_references(file_path, 4, 20) @@ -42,26 +34,16 @@ class TestJavaLanguageServer: # Use selectionRange if present, otherwise fall back to range if "selectionRange" in model_symbol: sel_start = model_symbol["selectionRange"]["start"] - sel_end = model_symbol["selectionRange"]["end"] else: sel_start = model_symbol["range"]["start"] - sel_end = model_symbol["range"]["end"] - found = False - if sel_start["line"] == sel_end["line"]: - for col in range(sel_start["character"], sel_end["character"] + 1): - refs = language_server.request_references(file_path, sel_start["line"], col) - if any("Main.java" in ref.get("relativePath", "") for ref in refs): - found = True - else: - refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) - refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) - if any("Main.java" in ref.get("relativePath", "") for ref in refs_start + refs_end): - found = True - assert found, "Main should reference Model (tried all positions in selectionRange)" + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "Main.java" in ref.get("relativePath", "") for ref in refs + ), "Main should reference Model (tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.JAVA], indirect=True) - def test_overview_methods(self, language_server): + def test_overview_methods(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert find_symbol_recursive(symbols, "Main"), "Main missing from overview" - assert find_symbol_recursive(symbols, "Utils"), "Utils missing from overview" - assert find_symbol_recursive(symbols, "Model"), "Model missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "Model"), "Model missing from overview" diff --git a/test/multilspy/python/test_python_basic.py b/test/multilspy/python/test_python_basic.py index ddf50369..635b19fe 100644 --- a/test/multilspy/python/test_python_basic.py +++ b/test/multilspy/python/test_python_basic.py @@ -18,7 +18,7 @@ class TestLanguageServerBasics: """Test basic functionality of the language server.""" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_user_class(self, language_server: SyncLanguageServer): + def test_request_references_user_class(self, language_server: SyncLanguageServer) -> None: """Test request_references on the User class.""" # Get references to the User class in models.py file_path = os.path.join("test_repo", "models.py") @@ -33,7 +33,7 @@ class TestLanguageServerBasics: assert len(references) > 1, "User class should be referenced in multiple files (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_item_class(self, language_server: SyncLanguageServer): + def test_request_references_item_class(self, language_server: SyncLanguageServer) -> None: """Test request_references on the Item class.""" # Get references to the Item class in models.py file_path = os.path.join("test_repo", "models.py") @@ -49,7 +49,7 @@ class TestLanguageServerBasics: assert len(services_references) > 0, "At least one reference should be in services.py (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_function_parameter(self, language_server: SyncLanguageServer): + def test_request_references_function_parameter(self, language_server: SyncLanguageServer) -> None: """Test request_references on a function parameter.""" # Get references to the id parameter in get_user method file_path = os.path.join("test_repo", "services.py") @@ -64,7 +64,7 @@ class TestLanguageServerBasics: assert len(references) > 0, "id parameter should be referenced within the method (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_references_create_user_method(self, language_server: SyncLanguageServer): + def test_request_references_create_user_method(self, language_server: SyncLanguageServer) -> None: # Get references to the create_user method in UserService file_path = os.path.join("test_repo", "services.py") # Line 15 contains the create_user method definition @@ -78,7 +78,7 @@ class TestLanguageServerBasics: assert len(references) > 1, "Should get valid references for create_user (using selectionRange if present)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_retrieve_content_around_line(self, language_server: SyncLanguageServer): + def test_retrieve_content_around_line(self, language_server: SyncLanguageServer) -> None: """Test retrieve_content_around_line functionality with various scenarios.""" file_path = os.path.join("test_repo", "models.py") @@ -185,7 +185,7 @@ class TestLanguageServerBasics: assert line.match_type == LineType.AFTER_MATCH @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_search_files_for_pattern(self, language_server: SyncLanguageServer): + def test_search_files_for_pattern(self, language_server: SyncLanguageServer) -> None: """Test search_files_for_pattern with various patterns and glob filters.""" # Test 1: Search for class definitions across all files class_pattern = r"class\s+\w+\s*(?:\([^{]*\)|:)" diff --git a/test/multilspy/python/test_symbol_retrieval.py b/test/multilspy/python/test_symbol_retrieval.py index 700f7e6a..6f7b22cd 100644 --- a/test/multilspy/python/test_symbol_retrieval.py +++ b/test/multilspy/python/test_symbol_retrieval.py @@ -19,7 +19,7 @@ class TestLanguageServerSymbols: """Test the language server's symbol-related functionality.""" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_function(self, language_server: SyncLanguageServer): + def test_request_containing_symbol_function(self, language_server: SyncLanguageServer) -> None: """Test request_containing_symbol for a function.""" # Test for a position inside the create_user method file_path = os.path.join("test_repo", "services.py") @@ -34,7 +34,7 @@ class TestLanguageServerSymbols: assert containing_symbol["body"].strip().startswith("def create_user(self") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_references_to_variables(self, language_server: SyncLanguageServer): + def test_references_to_variables(self, language_server: SyncLanguageServer) -> None: """Test request_referencing_symbols for a variable.""" file_path = os.path.join("test_repo", "variables.py") # Line 75 contains the field status that is later modified @@ -49,7 +49,7 @@ class TestLanguageServerSymbols: assert "second_dataclass" in ref_names @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_class(self, language_server: SyncLanguageServer): + def test_request_containing_symbol_class(self, language_server: SyncLanguageServer) -> None: """Test request_containing_symbol for a class.""" # Test for a position inside the UserService class but outside any method file_path = os.path.join("test_repo", "services.py") @@ -62,7 +62,7 @@ class TestLanguageServerSymbols: assert containing_symbol["kind"] == SymbolKind.Class @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer): + def test_request_containing_symbol_nested(self, language_server: SyncLanguageServer) -> None: """Test request_containing_symbol with nested scopes.""" # Test for a position inside a method which is inside a class file_path = os.path.join("test_repo", "services.py") @@ -88,7 +88,7 @@ class TestLanguageServerSymbols: assert parent_symbol["kind"] == SymbolKind.Class @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_none(self, language_server: SyncLanguageServer): + def test_request_containing_symbol_none(self, language_server: SyncLanguageServer) -> None: """Test request_containing_symbol for a position with no containing symbol.""" # Test for a position outside any function/class (e.g., in imports) file_path = os.path.join("test_repo", "services.py") @@ -99,7 +99,7 @@ class TestLanguageServerSymbols: assert containing_symbol is None or containing_symbol == {} @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer): + def test_request_referencing_symbols_function(self, language_server: SyncLanguageServer) -> None: """Test request_referencing_symbols for a function.""" # Test referencing symbols for create_user function file_path = os.path.join("test_repo", "services.py") @@ -121,7 +121,7 @@ class TestLanguageServerSymbols: assert "end" in symbol["location"]["range"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer): + def test_request_referencing_symbols_class(self, language_server: SyncLanguageServer) -> None: """Test request_referencing_symbols for a class.""" # Test referencing symbols for User class file_path = os.path.join("test_repo", "models.py") @@ -140,7 +140,7 @@ class TestLanguageServerSymbols: assert len(services_references) > 0, "No referencing symbols from services.py for User (selectionRange)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer): + def test_request_referencing_symbols_parameter(self, language_server: SyncLanguageServer) -> None: """Test request_referencing_symbols for a function parameter.""" # Test referencing symbols for id parameter in get_user file_path = os.path.join("test_repo", "services.py") @@ -154,14 +154,12 @@ class TestLanguageServerSymbols: method_refs = [ symbol for symbol in ref_symbols - if "location" in symbol - and "range" in symbol["location"] - and symbol["location"]["range"]["start"]["line"] > sel_start["line"] + if "location" in symbol and "range" in symbol["location"] and symbol["location"]["range"]["start"]["line"] > sel_start["line"] ] assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer): + def test_request_referencing_symbols_none(self, language_server: SyncLanguageServer) -> None: """Test request_referencing_symbols for a position with no symbol.""" # For positions with no symbol, the method might throw an error or return None/empty list # We'll modify our test to handle this by using a try-except block @@ -179,7 +177,7 @@ class TestLanguageServerSymbols: # Tests for request_defining_symbol @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer): + def test_request_defining_symbol_variable(self, language_server: SyncLanguageServer) -> None: """Test request_defining_symbol for a variable usage.""" # Test finding the definition of a symbol in the create_user method file_path = os.path.join("test_repo", "services.py") @@ -198,7 +196,7 @@ class TestLanguageServerSymbols: assert "services.py" in defining_symbol["location"]["uri"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer): + def test_request_defining_symbol_imported_class(self, language_server: SyncLanguageServer) -> None: """Test request_defining_symbol for an imported class.""" # Test finding the definition of the 'User' class used in the UserService.create_user method file_path = os.path.join("test_repo", "services.py") @@ -210,7 +208,7 @@ class TestLanguageServerSymbols: assert defining_symbol.get("name") == "User" @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer): + def test_request_defining_symbol_method_call(self, language_server: SyncLanguageServer) -> None: """Test request_defining_symbol for a method call.""" # Create an example file path for a file that calls UserService.create_user examples_file_path = os.path.join("examples", "user_management.py") @@ -235,7 +233,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify method call definition - file structure may differ from expected") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_none(self, language_server: SyncLanguageServer): + def test_request_defining_symbol_none(self, language_server: SyncLanguageServer) -> None: """Test request_defining_symbol for a position with no symbol.""" # Test for a position with no symbol (e.g., whitespace or comment) file_path = os.path.join("test_repo", "services.py") @@ -246,7 +244,7 @@ class TestLanguageServerSymbols: assert defining_symbol is None @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer): + def test_request_containing_symbol_variable(self, language_server: SyncLanguageServer) -> None: """Test request_containing_symbol where the symbol is a variable.""" # Test for a position inside a variable definition file_path = os.path.join("test_repo", "services.py") @@ -259,7 +257,7 @@ class TestLanguageServerSymbols: assert containing_symbol["kind"] == SymbolKind.Variable @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer): + def test_request_defining_symbol_nested_function(self, language_server: SyncLanguageServer) -> None: """Test request_defining_symbol for a nested function or closure.""" # Use the existing nested.py file which contains nested classes and methods file_path = os.path.join("test_repo", "nested.py") @@ -310,7 +308,7 @@ class TestLanguageServerSymbols: assert defining_symbol.get("kind") == SymbolKind.Function.value @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_methods_integration(self, language_server: SyncLanguageServer): + def test_symbol_methods_integration(self, language_server: SyncLanguageServer) -> None: """Test the integration between different symbol-related methods.""" # This test demonstrates using the various symbol methods together # by finding a symbol and then checking its definition @@ -359,7 +357,7 @@ class TestLanguageServerSymbols: warnings.warn("Could not verify container hierarchy - implementation detail") @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_tree_structure(self, language_server: SyncLanguageServer): + def test_symbol_tree_structure(self, language_server: SyncLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file repo_structure = language_server.request_full_symbol_tree() @@ -387,7 +385,7 @@ class TestLanguageServerSymbols: assert user_management_roots == user_management_node["children"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer): + def test_symbol_tree_structure_subdir(self, language_server: SyncLanguageServer) -> None: """Test that the symbol tree structure is correctly built.""" # Get all symbols in the test file examples_package_roots = language_server.request_full_symbol_tree(within_relative_path="examples") @@ -408,7 +406,7 @@ class TestLanguageServerSymbols: assert user_management_roots == user_management_node["children"] @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_dir_overview(self, language_server: SyncLanguageServer): + def test_request_dir_overview(self, language_server: SyncLanguageServer) -> None: """Test that request_dir_overview returns correct symbol information for files in a directory.""" # Get overview of the examples directory overview = language_server.request_dir_overview("test_repo") @@ -433,7 +431,7 @@ class TestLanguageServerSymbols: assert symbol in services_symbols @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_request_document_overview(self, language_server: SyncLanguageServer): + def test_request_document_overview(self, language_server: SyncLanguageServer) -> None: """Test that request_document_overview returns correct symbol information for a file.""" # Get overview of the user_management.py file overview = language_server.request_document_overview(os.path.join("examples", "user_management.py")) @@ -443,7 +441,7 @@ class TestLanguageServerSymbols: assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names) @pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True) - def test_containing_symbol_of_var_is_file(self, language_server: SyncLanguageServer): + def test_containing_symbol_of_var_is_file(self, language_server: SyncLanguageServer) -> None: """Test that the containing symbol of a variable is the file itself.""" # Get the containing symbol of a variable in a file file_path = os.path.join("test_repo", "services.py") diff --git a/test/multilspy/rust/test_rust_basic.py b/test/multilspy/rust/test_rust_basic.py index 8b1d1a57..aeaf85a4 100644 --- a/test/multilspy/rust/test_rust_basic.py +++ b/test/multilspy/rust/test_rust_basic.py @@ -2,22 +2,14 @@ import os import pytest +from multilspy import SyncLanguageServer from multilspy.multilspy_config import Language - - -def find_symbol_recursive(symbols, name): - for symbol in symbols: - if symbol.get("name") == name: - return True - if symbol.get("children"): - if find_symbol_recursive(symbol["children"], name): - return True - return False +from multilspy.multilspy_utils import SymbolUtils class TestRustLanguageServer: @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_references_raw(self, language_server): + def test_find_references_raw(self, language_server: SyncLanguageServer) -> None: # Directly test the request_references method for the add function file_path = os.path.join("src", "lib.rs") symbols = language_server.request_document_symbols(file_path) @@ -28,32 +20,20 @@ class TestRustLanguageServer: break assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs" sel_start = add_symbol["selectionRange"]["start"] - sel_end = add_symbol["selectionRange"]["end"] - found = False - if sel_start["line"] == sel_end["line"]: - for col in range(sel_start["character"], sel_end["character"] + 1): - refs = language_server.request_references(file_path, sel_start["line"], col) - # f"Raw request_references for add in lib.rs at col {col}: {refs}") - if any("main.rs" in ref.get("relativePath", "") for ref in refs): - found = True - else: - refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) - refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) - # f"Raw request_references for add in lib.rs at selectionRange start: {refs_start}") - # f"Raw request_references for add in lib.rs at selectionRange end: {refs_end}") - if any("main.rs" in ref.get("relativePath", "") for ref in refs_start + refs_end): - found = True - assert found, "main.rs should reference add (raw, tried all positions in selectionRange)" + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "main.rs" in ref.get("relativePath", "") for ref in refs + ), "main.rs should reference add (raw, tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_symbol(self, language_server): + def test_find_symbol(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert find_symbol_recursive(symbols, "main"), "main function not found in symbol tree" - assert find_symbol_recursive(symbols, "add"), "add function not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add function not found in symbol tree" # Add more as needed based on test_repo @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_find_referencing_symbols(self, language_server): + def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: # Find references to 'add' defined in lib.rs, should be referenced from main.rs file_path = os.path.join("src", "lib.rs") symbols = language_server.request_document_symbols(file_path) @@ -64,28 +44,13 @@ class TestRustLanguageServer: break assert add_symbol is not None, "Could not find 'add' function symbol in lib.rs" sel_start = add_symbol["selectionRange"]["start"] - sel_end = add_symbol["selectionRange"]["end"] - # f"add symbol selectionRange: start={sel_start}, end={sel_end}") - found = False - # Try all character positions in the selectionRange (same line, from start to end character) - if sel_start["line"] == sel_end["line"]: - for col in range(sel_start["character"], sel_end["character"] + 1): - refs = language_server.request_references(file_path, sel_start["line"], col) - # f"References for add in lib.rs at col {col} (selectionRange): {refs}") - if any("main.rs" in ref.get("relativePath", "") for ref in refs): - found = True - else: - # If the function name spans multiple lines, try start and end - refs_start = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) - refs_end = language_server.request_references(file_path, sel_end["line"], sel_end["character"]) - # f"References for add in lib.rs at selectionRange start: {refs_start}") - # f"References for add in lib.rs at selectionRange end: {refs_end}") - if any("main.rs" in ref.get("relativePath", "") for ref in refs_start + refs_end): - found = True - assert found, "main.rs should reference add (tried all positions in selectionRange)" + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "main.rs" in ref.get("relativePath", "") for ref in refs + ), "main.rs should reference add (tried all positions in selectionRange)" @pytest.mark.parametrize("language_server", [Language.RUST], indirect=True) - def test_overview_methods(self, language_server): + def test_overview_methods(self, language_server: SyncLanguageServer) -> None: symbols = language_server.request_full_symbol_tree() - assert find_symbol_recursive(symbols, "main"), "main missing from overview" - assert find_symbol_recursive(symbols, "add"), "add missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main missing from overview" + assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add missing from overview" diff --git a/test/multilspy/typescript/test_typescript_basic.py b/test/multilspy/typescript/test_typescript_basic.py new file mode 100644 index 00000000..0b8243d9 --- /dev/null +++ b/test/multilspy/typescript/test_typescript_basic.py @@ -0,0 +1,32 @@ +import os + +import pytest + +from multilspy import SyncLanguageServer +from multilspy.multilspy_config import Language +from multilspy.multilspy_utils import SymbolUtils + + +class TestTypescriptLanguageServer: + @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + def test_find_symbol(self, language_server: SyncLanguageServer) -> None: + symbols = language_server.request_full_symbol_tree() + assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoClass"), "DemoClass not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "helperFunction"), "helperFunction not found in symbol tree" + assert SymbolUtils.symbol_tree_contains_name(symbols, "printValue"), "printValue method not found in symbol tree" + + @pytest.mark.parametrize("language_server", [Language.TYPESCRIPT], indirect=True) + def test_find_referencing_symbols(self, language_server: SyncLanguageServer) -> None: + file_path = os.path.join("index.ts") + symbols = language_server.request_document_symbols(file_path) + helper_symbol = None + for sym in symbols[0]: + if sym.get("name") == "helperFunction": + helper_symbol = sym + break + assert helper_symbol is not None, "Could not find 'helperFunction' symbol in index.ts" + sel_start = helper_symbol["selectionRange"]["start"] + refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert any( + "index.ts" in ref.get("relativePath", "") for ref in refs + ), "index.ts should reference helperFunction (tried all positions in selectionRange)" diff --git a/test/resources/repos/go/test_repo/main.go b/test/resources/repos/go/test_repo/main.go new file mode 100644 index 00000000..b38c31ff --- /dev/null +++ b/test/resources/repos/go/test_repo/main.go @@ -0,0 +1,20 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello, Go!") + Helper() +} + +func Helper() { + fmt.Println("Helper function called") +} + +type DemoStruct struct { + Field int +} + +func UsingHelper() { + Helper() +} diff --git a/test/resources/repos/typescript/test_repo/index.ts b/test/resources/repos/typescript/test_repo/index.ts new file mode 100644 index 00000000..7b8bd1da --- /dev/null +++ b/test/resources/repos/typescript/test_repo/index.ts @@ -0,0 +1,16 @@ +export class DemoClass { + value: number; + constructor(value: number) { + this.value = value; + } + printValue() { + console.log(this.value); + } +} + +export function helperFunction() { + const demo = new DemoClass(42); + demo.printValue(); +} + +helperFunction();