Fix symbol name resolution in selected LS (#1247)

Adds tests for name resolution for each language and fixes the name resolution where tests have failed.

Also:

Extends python tests to cover ty
Extends skipping conditions for several LS (but only when not in CI)
Better test output in CI
Removed a wrong python test (started failing for ty and should have failed for pyright too)
This commit is contained in:
Michael Panchenko authored and GitHub committed 2026-04-01 16:29:00 +02:00
1 parent 7377a84337
commit d30cdf8474
59 files changed
+1015 -97

No files matched your search

+20 -7
View File
@@ -12,14 +12,14 @@ import pytest
from serena.project import Project
from serena.util.text_utils import LineType
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES, format_symbol_for_assert, has_malformed_name, request_all_symbols
@pytest.mark.python
class TestPythonLanguageServerBasics:
"""Test basic functionality of the language server."""
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_references_user_class(self, language_server: SolidLanguageServer) -> None:
"""Test request_references on the User class."""
# Get references to the User class in models.py
@@ -34,7 +34,7 @@ class TestPythonLanguageServerBasics:
references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
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)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_references_item_class(self, language_server: SolidLanguageServer) -> None:
"""Test request_references on the Item class."""
# Get references to the Item class in models.py
@@ -50,7 +50,7 @@ class TestPythonLanguageServerBasics:
services_references = [ref for ref in references if "services.py" in ref["uri"]]
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)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_references_function_parameter(self, language_server: SolidLanguageServer) -> None:
"""Test request_references on a function parameter."""
# Get references to the id parameter in get_user method
@@ -65,7 +65,7 @@ class TestPythonLanguageServerBasics:
references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
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)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_references_create_user_method(self, language_server: SolidLanguageServer) -> None:
# Get references to the create_user method in UserService
file_path = os.path.join("test_repo", "services.py")
@@ -81,7 +81,7 @@ class TestPythonLanguageServerBasics:
class TestProjectBasics:
@pytest.mark.parametrize("project", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("project", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_retrieve_content_around_line(self, project: Project) -> None:
"""Test retrieve_content_around_line functionality with various scenarios."""
file_path = os.path.join("test_repo", "models.py")
@@ -188,7 +188,7 @@ class TestProjectBasics:
else:
assert line.match_type == LineType.AFTER_MATCH
@pytest.mark.parametrize("project", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("project", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_search_files_for_pattern(self, project: Project) -> None:
"""Test search_files_for_pattern with various patterns and glob filters."""
# Test 1: Search for class definitions across all files
@@ -233,3 +233,16 @@ class TestProjectBasics:
no_match_pattern = r"def\s+this_method_does_not_exist\s*\([^)]*\):"
matches = project.search_source_files_for_pattern(no_match_pattern)
assert len(matches) == 0
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
for s in all_symbols:
if has_malformed_name(s):
malformed_symbols.append(s)
if malformed_symbols:
pytest.fail(
f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}",
pytrace=False,
)
@@ -6,6 +6,7 @@ import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import start_ls_context
from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES
# This mark will be applied to all tests in this module
pytestmark = pytest.mark.python
@@ -19,7 +20,7 @@ def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
yield ls
@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("ls_with_ignored_dirs", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
"""Tests that request_full_symbol_tree ignores the configured directory."""
root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
@@ -28,7 +29,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
assert children_names == {"test_repo", "examples"}
@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("ls_with_ignored_dirs", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
"""Tests that find_references ignores the configured directory."""
# Location of Item, which is referenced in scripts
@@ -42,7 +43,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
assert not any("scripts" in ref["relativePath"] for ref in references)
@pytest.mark.parametrize("repo_path", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("repo_path", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
"""Tests that refs and symbols with glob patterns are ignored."""
ignored_paths = ["*ipts", "custom_t*"]
+24 -53
View File
@@ -12,8 +12,8 @@ import pytest
from serena.symbol import LanguageServerSymbol
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES
pytestmark = pytest.mark.python
@@ -21,7 +21,7 @@ pytestmark = pytest.mark.python
class TestLanguageServerSymbols:
"""Test the language server's symbol-related functionality."""
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a function."""
# Test for a position inside the create_user method
@@ -36,7 +36,7 @@ class TestLanguageServerSymbols:
if "body" in containing_symbol:
assert containing_symbol["body"].get_text().strip().startswith("def create_user(self")
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_references_to_variables(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a variable."""
file_path = os.path.join("test_repo", "variables.py")
@@ -51,7 +51,7 @@ class TestLanguageServerSymbols:
assert "dataclass_instance" in ref_names
assert "second_dataclass" in ref_names
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a class."""
# Test for a position inside the UserService class but outside any method
@@ -64,7 +64,7 @@ class TestLanguageServerSymbols:
assert containing_symbol["name"] == "UserService"
assert containing_symbol["kind"] == SymbolKind.Class
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol with nested scopes."""
# Test for a position inside a method which is inside a class
@@ -90,7 +90,7 @@ class TestLanguageServerSymbols:
assert parent_symbol["name"] == "UserService"
assert parent_symbol["kind"] == SymbolKind.Class
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a position with no containing symbol."""
# Test for a position outside any function/class (e.g., in imports)
@@ -101,7 +101,7 @@ class TestLanguageServerSymbols:
# Should return None or an empty dictionary
assert containing_symbol is None or containing_symbol == {}
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a function."""
# Test referencing symbols for create_user function
@@ -125,7 +125,7 @@ class TestLanguageServerSymbols:
assert "start" in symbol["location"]["range"]
assert "end" in symbol["location"]["range"]
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a class."""
# Test referencing symbols for User class
@@ -146,7 +146,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)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a function parameter."""
# Test referencing symbols for id parameter in get_user
@@ -167,7 +167,7 @@ class TestLanguageServerSymbols:
]
assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)"
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> 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
@@ -185,7 +185,7 @@ class TestLanguageServerSymbols:
pass
# Tests for request_defining_symbol
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a variable usage."""
# Test finding the definition of a symbol in the create_user method
@@ -204,7 +204,7 @@ class TestLanguageServerSymbols:
if "location" in defining_symbol and "uri" in defining_symbol["location"]:
assert "services.py" in defining_symbol["location"]["uri"]
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for an imported class."""
# Test finding the definition of the 'User' class used in the UserService.create_user method
@@ -216,7 +216,7 @@ class TestLanguageServerSymbols:
assert defining_symbol is not None
assert defining_symbol.get("name") == "User"
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a method call."""
# Create an example file path for a file that calls UserService.create_user
@@ -241,7 +241,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)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a position with no symbol."""
# Test for a position with no symbol (e.g., whitespace or comment)
@@ -252,7 +252,7 @@ class TestLanguageServerSymbols:
# Should return None for positions with no symbol
assert defining_symbol is None
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol where the symbol is a variable."""
# Test for a position inside a variable definition
@@ -265,7 +265,7 @@ class TestLanguageServerSymbols:
assert containing_symbol["name"] == "user_var_str"
assert containing_symbol["kind"] == SymbolKind.Variable
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a nested function or closure."""
# Use the existing nested.py file which contains nested classes and methods
@@ -290,15 +290,8 @@ class TestLanguageServerSymbols:
# Test 3: Find definition of a method-local function
defining_symbol = language_server.request_defining_symbol(file_path, 9, 15) # Position inside func_within_func
# This is challenging for many language servers and may fail
try:
assert defining_symbol is not None
assert defining_symbol.get("name") == "func_within_func"
except (AssertionError, TypeError, KeyError):
# This is expected to potentially fail in many implementations
import warnings
warnings.warn("Could not resolve nested class method definition - implementation limitation")
assert defining_symbol is not None
assert defining_symbol.get("name") == "func_within_func"
# Test 2: Find definition of the nested class
defining_symbol = language_server.request_defining_symbol(file_path, 15, 18) # Position of NestedClass
@@ -314,9 +307,9 @@ class TestLanguageServerSymbols:
# This is challenging for many language servers and may fail
assert defining_symbol is not None
assert defining_symbol.get("name") == "func_within_func"
assert defining_symbol.get("kind") == SymbolKind.Function.value
assert defining_symbol.get("kind") in [SymbolKind.Function.value, SymbolKind.Method.value]
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
"""Test the integration between different symbol-related methods."""
# This test demonstrates using the various symbol methods together
@@ -365,7 +358,7 @@ class TestLanguageServerSymbols:
warnings.warn("Could not verify container hierarchy - implementation detail")
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
"""Test that the symbol tree structure is correctly built."""
# Get all symbols in the test file
@@ -395,7 +388,7 @@ class TestLanguageServerSymbols:
).get_all_symbols_and_roots()
assert user_management_roots == user_management_node["children"]
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None:
"""Test that the symbol tree structure is correctly built."""
# Get all symbols in the test file
@@ -418,7 +411,7 @@ class TestLanguageServerSymbols:
).get_all_symbols_and_roots()
assert user_management_roots == user_management_node["children"]
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
"""Test that request_dir_overview returns correct symbol information for files in a directory."""
# Get overview of the examples directory
@@ -442,7 +435,7 @@ class TestLanguageServerSymbols:
retrieved_symbols = {symbol["name"] for symbol in services_symbols if "name" in symbol}
assert expected_symbols.issubset(retrieved_symbols)
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
"""Test that request_document_overview returns correct symbol information for a file."""
# Get overview of the user_management.py file
@@ -451,25 +444,3 @@ class TestLanguageServerSymbols:
# Verify that we have entries for both files
symbol_names = {LanguageServerSymbol(s_info).name for s_info in overview}
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: SolidLanguageServer) -> 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")
# import of typing
references_to_typing = [
ref.symbol
for ref in language_server.request_referencing_symbols(file_path, 4, 6, include_imports=False, include_file_symbols=True)
]
assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
# now include bodies
references_to_typing = [
ref.symbol
for ref in language_server.request_referencing_symbols(
file_path, 4, 6, include_imports=False, include_file_symbols=True, include_body=True
)
]
assert {ref["kind"] for ref in references_to_typing} == {SymbolKind.File}
assert references_to_typing[0]["body"]