mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-06 20:16:14 +00:00
Black dependency created a dependabot warning and couldn't be bumped since it collided with pathspec dependencies. Since ruff can also format, black is not needed. But we now have these small format changes, mostly in test files
259 lines
15 KiB
Python
259 lines
15 KiB
Python
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from solidlsp import SolidLanguageServer
|
|
from solidlsp.ls_config import Language
|
|
from test.conftest import is_ci, is_windows, language_tests_enabled
|
|
|
|
_php_servers: list[Language] = [Language.PHP]
|
|
if language_tests_enabled(Language.PHP_PHPACTOR):
|
|
if not (is_windows and is_ci): # TODO: Phpactor tests are flaky in Windows CI and can even cause hangs #1040
|
|
_php_servers.append(Language.PHP_PHPACTOR)
|
|
|
|
|
|
@pytest.mark.php
|
|
class TestPhpLanguageServers:
|
|
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
|
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
|
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
|
"""Test that the language server starts and stops successfully."""
|
|
assert language_server.is_running()
|
|
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
|
|
|
|
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
|
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
|
def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
|
# In index.php:
|
|
# Line 9 (1-indexed): $greeting = greet($userName);
|
|
# Line 11 (1-indexed): echo $greeting;
|
|
# We want to find the definition of $greeting (defined on line 9)
|
|
# from its usage in echo $greeting; on line 11.
|
|
# LSP is 0-indexed: definition on line 8, usage on line 10.
|
|
# $greeting in echo $greeting; (e c h o $ g r e e t i n g)
|
|
# ^ char 5
|
|
# Intelephense uses line 10 (0-indexed), Phpactor uses line 11 (0-indexed)
|
|
if language_server.language_server.language == Language.PHP_PHPACTOR:
|
|
definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 11, 6)
|
|
else:
|
|
definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 10, 6)
|
|
|
|
assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
|
|
if language_server.language_server.language == Language.PHP_PHPACTOR:
|
|
assert len(definition_location_list) >= 1
|
|
else:
|
|
assert len(definition_location_list) == 1
|
|
definition_location = definition_location_list[0]
|
|
assert definition_location["uri"].endswith("index.php")
|
|
# Definition of $greeting is on line 10 (1-indexed) / line 9 (0-indexed), char 0
|
|
assert definition_location["range"]["start"]["line"] == 9
|
|
if language_server.language_server.language != Language.PHP_PHPACTOR:
|
|
assert definition_location["range"]["start"]["character"] == 0
|
|
|
|
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
|
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
|
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
|
# Intelephense uses line 12 (0-indexed), Phpactor uses line 13 (0-indexed)
|
|
if language_server.language_server.language == Language.PHP_PHPACTOR:
|
|
definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 13, 5)
|
|
else:
|
|
definition_location_list = language_server.request_definition(str(repo_path / "index.php"), 12, 5)
|
|
|
|
assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
|
|
if language_server.language_server.language == Language.PHP_PHPACTOR:
|
|
assert len(definition_location_list) >= 1
|
|
else:
|
|
assert len(definition_location_list) == 1
|
|
definition_location = definition_location_list[0]
|
|
assert definition_location["uri"].endswith("helper.php")
|
|
assert definition_location["range"]["start"]["line"] == 2
|
|
if language_server.language_server.language != Language.PHP_PHPACTOR:
|
|
assert definition_location["range"]["start"]["character"] == 0
|
|
|
|
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
|
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
|
def test_find_definition_simple_variable(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
|
file_path = str(repo_path / "simple_var.php")
|
|
|
|
# In simple_var.php:
|
|
# Line 2 (1-indexed): $localVar = "test";
|
|
# Line 3 (1-indexed): echo $localVar;
|
|
# LSP is 0-indexed: definition on line 1, usage on line 2
|
|
# Find definition of $localVar (char 5 on line 3 / 0-indexed: line 2, char 5)
|
|
# $localVar in echo $localVar; (e c h o $ l o c a l V a r)
|
|
# ^ char 5
|
|
definition_location_list = language_server.request_definition(file_path, 2, 6) # cursor on 'l' in $localVar
|
|
|
|
assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
|
|
if language_server.language_server.language == Language.PHP_PHPACTOR:
|
|
assert len(definition_location_list) >= 1
|
|
else:
|
|
assert len(definition_location_list) == 1
|
|
definition_location = definition_location_list[0]
|
|
assert definition_location["uri"].endswith("simple_var.php")
|
|
assert definition_location["range"]["start"]["line"] == 1 # Definition of $localVar (0-indexed)
|
|
if language_server.language_server.language != Language.PHP_PHPACTOR:
|
|
assert definition_location["range"]["start"]["character"] == 0 # $localVar (0-indexed)
|
|
|
|
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
|
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
|
def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
|
index_php_path = str(repo_path / "index.php")
|
|
|
|
# In index.php (0-indexed lines):
|
|
# Line 9: $greeting = greet($userName); // Definition of $greeting
|
|
# Line 11: echo $greeting; // Usage of $greeting
|
|
# Find references for $greeting from its usage in "echo $greeting;" (line 11, char 6 for 'g')
|
|
references = language_server.request_references(index_php_path, 11, 6)
|
|
|
|
assert references, f"Expected non-empty references for $greeting but got {references=}"
|
|
|
|
if language_server.language_server.language == Language.PHP_PHPACTOR:
|
|
actual_locations = [
|
|
{
|
|
"uri_suffix": loc["uri"].split("/")[-1],
|
|
"line": loc["range"]["start"]["line"],
|
|
}
|
|
for loc in references
|
|
]
|
|
|
|
# Check that at least one reference points to $greeting usage in index.php
|
|
matching = [loc for loc in actual_locations if loc["uri_suffix"] == "index.php" and loc["line"] == 11]
|
|
assert matching, f"Expected reference to $greeting on line 11 of index.php, got {actual_locations}"
|
|
else:
|
|
# Intelephense, when asked for references from usage, seems to only return the usage itself.
|
|
assert len(references) == 1, "Expected to find 1 reference for $greeting (the usage itself)"
|
|
|
|
expected_locations = [{"uri_suffix": "index.php", "line": 11, "character": 5}] # Usage: echo $greeting (points to $)
|
|
|
|
# Convert actual references to a comparable format and sort
|
|
actual_locations = sorted(
|
|
[
|
|
{
|
|
"uri_suffix": loc["uri"].split("/")[-1],
|
|
"line": loc["range"]["start"]["line"],
|
|
"character": loc["range"]["start"]["character"],
|
|
}
|
|
for loc in references
|
|
],
|
|
key=lambda x: (x["uri_suffix"], x["line"], x["character"]),
|
|
)
|
|
|
|
expected_locations = sorted(expected_locations, key=lambda x: (x["uri_suffix"], x["line"], x["character"]))
|
|
|
|
assert actual_locations == expected_locations
|
|
|
|
@pytest.mark.parametrize("language_server", _php_servers, indirect=True)
|
|
@pytest.mark.parametrize("repo_path", [Language.PHP], indirect=True)
|
|
def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
|
|
helper_php_path = str(repo_path / "helper.php")
|
|
# In index.php (0-indexed lines):
|
|
# Line 13: helperFunction(); // Usage of helperFunction
|
|
# Find references for helperFunction from its definition
|
|
references = language_server.request_references(helper_php_path, 2, len("function "))
|
|
|
|
assert references, f"Expected non-empty references for helperFunction but got {references=}"
|
|
|
|
if language_server.language_server.language == Language.PHP_PHPACTOR:
|
|
actual_locations_comparable = []
|
|
for loc in references:
|
|
actual_locations_comparable.append(
|
|
{
|
|
"uri_suffix": loc["uri"].split("/")[-1],
|
|
"line": loc["range"]["start"]["line"],
|
|
}
|
|
)
|
|
|
|
# Check that helperFunction usage in index.php line 13 is found
|
|
matching = [loc for loc in actual_locations_comparable if loc["uri_suffix"] == "index.php" and loc["line"] == 13]
|
|
assert matching, f"Usage of helperFunction in index.php (line 13) not found in {actual_locations_comparable}"
|
|
else:
|
|
# Intelephense might return 1 (usage) or 2 (usage + definition) references.
|
|
# Let's check for at least the usage in index.php
|
|
# Definition is in helper.php, line 2, char 0 (based on previous findings)
|
|
# Usage is in index.php, line 13, char 0
|
|
actual_locations_comparable = []
|
|
for loc in references:
|
|
actual_locations_comparable.append(
|
|
{
|
|
"uri_suffix": loc["uri"].split("/")[-1],
|
|
"line": loc["range"]["start"]["line"],
|
|
"character": loc["range"]["start"]["character"],
|
|
}
|
|
)
|
|
|
|
usage_in_index_php = {"uri_suffix": "index.php", "line": 13, "character": 0}
|
|
assert usage_in_index_php in actual_locations_comparable, "Usage of helperFunction in index.php not found"
|
|
|
|
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
|
|
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test that document symbols are properly retrieved after Intelephense capability fix."""
|
|
from solidlsp.ls_utils import SymbolUtils
|
|
|
|
symbols = language_server.request_full_symbol_tree()
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "helperFunction"), "helperFunction not found in symbol tree"
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet function not found in symbol tree"
|
|
|
|
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
|
|
def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
|
"""Test that document symbols are properly retrieved for a specific file."""
|
|
doc_symbols = language_server.request_document_symbols("helper.php")
|
|
all_symbols = doc_symbols.get_all_symbols_and_roots()
|
|
symbol_names = [sym.get("name") for sym in all_symbols[0] if sym.get("name")]
|
|
assert "helperFunction" in symbol_names, f"helperFunction not found in document symbols. Found: {symbol_names}"
|
|
|
|
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
|
|
def test_document_symbols_hierarchical_structure(self, language_server: SolidLanguageServer) -> None:
|
|
"""Verify Intelephense returns hierarchical DocumentSymbol format.
|
|
|
|
When hierarchicalDocumentSymbolSupport is declared in client capabilities,
|
|
Intelephense returns DocumentSymbol[] where class methods appear as children
|
|
of their parent class. Without this declaration, it falls back to a flat
|
|
SymbolInformation[] list where all symbols appear at root level with no
|
|
parent-child relationships.
|
|
"""
|
|
all_symbols, root_symbols = language_server.request_document_symbols("sample.php").get_all_symbols_and_roots()
|
|
|
|
root_names = [s.get("name") for s in root_symbols]
|
|
assert "Animal" in root_names, f"Animal class not found at root level. Roots: {root_names}"
|
|
assert "Dog" in root_names, f"Dog class not found at root level. Roots: {root_names}"
|
|
assert "Cat" in root_names, f"Cat class not found at root level. Roots: {root_names}"
|
|
|
|
# Verify Dog has method children — this is the key assertion for hierarchical support.
|
|
# With a flat response, Dog would have no children and all methods would be at root level.
|
|
dog_symbol = next((s for s in root_symbols if s.get("name") == "Dog"), None)
|
|
assert dog_symbol is not None, "Dog class not found in root symbols"
|
|
dog_children = dog_symbol.get("children", [])
|
|
dog_child_names = [c.get("name") for c in dog_children]
|
|
assert len(dog_child_names) > 0, (
|
|
f"Dog class has no children — hierarchicalDocumentSymbolSupport is not working. All root symbols: {root_names}"
|
|
)
|
|
expected_methods = {"greet", "fetch", "getBreed", "describe"}
|
|
missing = expected_methods - set(dog_child_names)
|
|
assert not missing, f"Dog class missing expected methods: {missing}. Children found: {dog_child_names}"
|
|
|
|
# Methods must NOT appear at root level (that would indicate the flat fallback format).
|
|
assert "greet" not in root_names, f"greet should be a child of Dog, not at root level. Roots: {root_names}"
|
|
assert "fetch" not in root_names, f"fetch should be a child of Dog, not at root level. Roots: {root_names}"
|
|
|
|
@pytest.mark.parametrize("language_server", [Language.PHP], indirect=True)
|
|
def test_full_symbol_tree_within_file(self, language_server: SolidLanguageServer) -> None:
|
|
"""Verify request_full_symbol_tree scoped to a PHP file returns correct symbols.
|
|
|
|
This validates that Intelephense responds correctly when symbols are requested
|
|
for a single file, including class/method hierarchy in sample.php.
|
|
"""
|
|
from solidlsp.ls_utils import SymbolUtils
|
|
|
|
symbols = language_server.request_full_symbol_tree(within_relative_path="sample.php")
|
|
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "Dog"), "Dog not found in sample.php symbol tree"
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "Animal"), "Animal not found in sample.php symbol tree"
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet method not found in sample.php symbol tree"
|
|
assert SymbolUtils.symbol_tree_contains_name(symbols, "fetch"), "fetch method not found in sample.php symbol tree"
|
|
|
|
# Methods must appear as children of Dog, not as root-level symbols
|
|
dog_root = next((s for s in symbols if s.get("name") == "Dog"), None)
|
|
if dog_root is not None:
|
|
assert SymbolUtils.symbol_tree_contains_name([dog_root], "greet"), "greet should be nested under Dog in symbol tree"
|