Formatting, removed unused vars and imports

This commit is contained in:
Michael Panchenko authored and Michael Panchenko committed 2025-07-06 22:23:17 +02:00
1 parent 5102a38fbe
commit a24dcea427
9 files changed
+166 -177

No files matched your search

+6 -1
View File
@@ -429,7 +429,12 @@ class Symbol(ToStringMixin):
return result
def to_dict(
self, kind: bool = False, location: bool = False, depth: int = 0, include_body: bool = False, include_children_body: bool = False,
self,
kind: bool = False,
location: bool = False,
depth: int = 0,
include_body: bool = False,
include_children_body: bool = False,
include_relative_path=True,
) -> dict[str, Any]:
"""
@@ -1 +1 @@
@@ -35,24 +35,27 @@ class ElixirTools(SolidLanguageServer):
def _is_next_ls_internal_file(self, abs_path: str) -> bool:
"""Check if an absolute path is a Next LS internal file that should be ignored."""
return any(pattern in abs_path for pattern in [
".burrito", # Next LS runtime directory
"next_ls_erts-", # Next LS Erlang runtime
"_next_ls_private_", # Next LS private files
"/priv/monkey/", # Next LS monkey patching directory
])
return any(
pattern in abs_path
for pattern in [
".burrito", # Next LS runtime directory
"next_ls_erts-", # Next LS Erlang runtime
"_next_ls_private_", # Next LS private files
"/priv/monkey/", # Next LS monkey patching directory
]
)
@override
def _send_references_request(self, relative_file_path: str, line: int, column: int):
"""Override to filter out Next LS internal files from references."""
from solidlsp.ls_utils import PathUtils
# Get the raw response from the parent implementation
raw_response = super()._send_references_request(relative_file_path, line, column)
if raw_response is None:
return None
# Filter out Next LS internal files
filtered_response = []
for item in raw_response:
@@ -62,7 +65,7 @@ class ElixirTools(SolidLanguageServer):
self.logger.log(f"Filtering out Next LS internal file: {abs_path}", logging.DEBUG)
continue
filtered_response.append(item)
return filtered_response
@staticmethod
@@ -87,7 +90,7 @@ class ElixirTools(SolidLanguageServer):
raise RuntimeError(
"Elixir is not installed. Please install Elixir from https://elixir-lang.org/install.html and make sure it is added to your PATH."
)
logger.log(f"Found Elixir: {elixir_version}", logging.INFO)
platformId = PlatformUtils.get_platform_id()
@@ -101,17 +104,17 @@ class ElixirTools(SolidLanguageServer):
# Map platform IDs to runtime dependency keys
platform_mapping = {
"linux-x64": "linux-x64",
"osx-x64": "darwin-x64",
"osx-x64": "darwin-x64",
"osx-arm64": "darwin-arm64",
"darwin-x64": "darwin-x64",
"darwin-arm64": "darwin-arm64",
"win-x64": "win-x64"
"darwin-arm64": "darwin-arm64",
"win-x64": "win-x64",
}
platform_key = platform_mapping.get(platformId.value)
if not platform_key:
raise RuntimeError(f"Unsupported platform for Next LS: {platformId.value}")
# Check for Windows and provide a helpful error message
if platformId.value.startswith("win"):
raise RuntimeError(
@@ -122,18 +125,18 @@ class ElixirTools(SolidLanguageServer):
dependency = runtimeDependencies["next_ls"][platform_key]
next_ls_dir = str(PurePath(os.path.abspath(os.path.dirname(__file__)), "static", dependency["relative_extraction_path"]))
os.makedirs(next_ls_dir, exist_ok=True)
executable_path = str(PurePath(next_ls_dir, dependency["executable_name"]))
binary_path = str(PurePath(next_ls_dir, dependency["binary_name"]))
if not os.path.exists(executable_path):
logger.log(f"Downloading Next LS binary from {dependency['url']}", logging.INFO)
FileUtils.download_file(logger, dependency["url"], binary_path)
# Make the binary executable on Unix-like systems
if not platformId.value.startswith("win"):
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
# Create a symlink or copy with the expected name
if binary_path != executable_path:
if os.path.exists(executable_path):
@@ -141,13 +144,14 @@ class ElixirTools(SolidLanguageServer):
if platformId.value.startswith("win"):
# On Windows, copy the file
import shutil
shutil.copy2(binary_path, executable_path)
else:
# On Unix-like systems, create a symlink
os.symlink(os.path.basename(binary_path), executable_path)
assert os.path.exists(executable_path), f"Next LS executable not found at {executable_path}"
logger.log(f"Next LS binary ready at: {executable_path}", logging.INFO)
return executable_path
@@ -163,7 +167,7 @@ class ElixirTools(SolidLanguageServer):
)
self.server_ready = threading.Event()
self.request_id = 0
# Set generous timeout for Next LS which can be slow to initialize and respond
self.set_request_timeout(180.0) # 60 seconds for all environments
@@ -201,7 +205,7 @@ class ElixirTools(SolidLanguageServer):
"""Handle window/logMessage notifications from Next LS"""
message_text = msg.get("message", "")
self.logger.log(f"LSP: window/logMessage: {message_text}", logging.INFO)
# Check for the specific Next LS readiness signal
# Based on Next LS source: "Runtime for folder #{name} is ready..."
if "Runtime for folder" in message_text and "is ready..." in message_text:
@@ -214,11 +218,11 @@ class ElixirTools(SolidLanguageServer):
def check_server_ready(params):
"""
Handle $/progress notifications from Next LS.
Keep as fallback for error detection, but primary readiness detection
Keep as fallback for error detection, but primary readiness detection
is now done via window/logMessage handler.
"""
value = params.get("value", {})
# Check for initialization completion progress (fallback signal)
if value.get("kind") == "end":
message = value.get("message", "")
@@ -255,10 +259,10 @@ class ElixirTools(SolidLanguageServer):
# Verify server capabilities - be more lenient with Next LS
self.logger.log(f"Next LS capabilities: {list(init_response['capabilities'].keys())}", logging.INFO)
# Next LS may not provide all capabilities immediately, so we check for basic ones
assert "textDocumentSync" in init_response["capabilities"], f"Missing textDocumentSync in {init_response['capabilities']}"
# Some capabilities might be optional or provided later
if "completionProvider" not in init_response["capabilities"]:
self.logger.log("Warning: completionProvider not available in initial capabilities", logging.WARNING)
@@ -272,10 +276,10 @@ class ElixirTools(SolidLanguageServer):
# This is the authoritative signal that Next LS is truly ready for requests
ready_timeout = 180.0
self.logger.log(f"Waiting up to {ready_timeout} seconds for Next LS runtime readiness...", logging.INFO)
if self.server_ready.wait(timeout=ready_timeout):
self.logger.log("Next LS is ready and available for requests", logging.INFO)
# Add a small settling period to ensure background indexing is complete
# Next LS often continues compilation/indexing in background after ready signal
settling_time = 120.0
@@ -285,4 +289,4 @@ class ElixirTools(SolidLanguageServer):
else:
error_msg = f"Next LS failed to initialize within {ready_timeout} seconds. This may indicate a problem with the Elixir installation, project compilation, or Next LS itself."
self.logger.log(error_msg, logging.ERROR)
raise RuntimeError(error_msg)
raise RuntimeError(error_msg)
+5 -7
View File
@@ -1,25 +1,23 @@
import platform
from pathlib import Path
def _test_nextls_available() -> str:
"""Test if Next LS is available and return error reason if not."""
# Check if we're on Windows (Next LS doesn't support Windows)
if platform.system() == "Windows":
return "Next LS does not support Windows"
# Try to import and check Elixir availability
try:
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
# Check if Elixir is installed
elixir_version = ElixirTools._get_elixir_version()
if not elixir_version:
return "Elixir is not installed or not in PATH"
return "" # No error, Next LS should be available
except ImportError as e:
return f"Failed to import ElixirTools: {e}"
except Exception as e:
@@ -27,4 +25,4 @@ def _test_nextls_available() -> str:
NEXTLS_UNAVAILABLE_REASON = _test_nextls_available()
NEXTLS_UNAVAILABLE = bool(NEXTLS_UNAVAILABLE_REASON)
NEXTLS_UNAVAILABLE = bool(NEXTLS_UNAVAILABLE_REASON)
+39 -34
View File
@@ -1,137 +1,142 @@
"""
Elixir-specific test configuration and fixtures.
"""
import os
import subprocess
import pytest
import time
from pathlib import Path
import pytest
def ensure_elixir_test_repo_compiled(repo_path: str) -> None:
"""Ensure the Elixir test repository dependencies are installed and project is compiled.
Next LS requires the project to be fully compiled and indexed before providing
complete references and symbol resolution. This function:
1. Installs dependencies via 'mix deps.get'
2. Compiles the project via 'mix compile'
This is essential in CI environments where dependencies aren't pre-installed.
Args:
repo_path: Path to the Elixir project root directory
"""
# Check if this looks like an Elixir project
mix_file = os.path.join(repo_path, "mix.exs")
if not os.path.exists(mix_file):
return
# Check if already compiled (optimization for repeated runs)
build_path = os.path.join(repo_path, "_build")
deps_path = os.path.join(repo_path, "deps")
if os.path.exists(build_path) and os.path.exists(deps_path):
print(f"Elixir test repository already compiled in {repo_path}")
return
try:
print(f"Installing dependencies and compiling Elixir test repository for optimal Next LS performance...")
print("Installing dependencies and compiling Elixir test repository for optimal Next LS performance...")
# First, install dependencies with increased timeout for CI
print("=" * 60)
print("Step 1/2: Installing Elixir dependencies...")
print("=" * 60)
start_time = time.time()
deps_result = subprocess.run(
["mix", "deps.get"],
cwd=repo_path,
capture_output=True,
text=True,
timeout=180 # 3 minutes for dependency installation (CI can be slow)
timeout=180,
check=False, # 3 minutes for dependency installation (CI can be slow)
)
deps_duration = time.time() - start_time
print(f"Dependencies installation completed in {deps_duration:.2f} seconds")
# Always log the output for transparency
if deps_result.stdout.strip():
print("Dependencies stdout:")
print("-" * 40)
print(deps_result.stdout)
print("-" * 40)
if deps_result.stderr.strip():
print("Dependencies stderr:")
print("-" * 40)
print(deps_result.stderr)
print("-" * 40)
if deps_result.returncode != 0:
print(f"⚠️ Warning: Dependencies installation failed with exit code {deps_result.returncode}")
# Continue anyway - some projects might not have dependencies
else:
print("✓ Dependencies installed successfully")
# Then compile the project with increased timeout for CI
print("=" * 60)
print("Step 2/2: Compiling Elixir project...")
print("=" * 60)
start_time = time.time()
compile_result = subprocess.run(
["mix", "compile"],
cwd=repo_path,
capture_output=True,
text=True,
timeout=300 # 5 minutes for compilation (Credo compilation can be slow in CI)
timeout=300,
check=False, # 5 minutes for compilation (Credo compilation can be slow in CI)
)
compile_duration = time.time() - start_time
print(f"Compilation completed in {compile_duration:.2f} seconds")
# Always log the output for transparency
if compile_result.stdout.strip():
print("Compilation stdout:")
print("-" * 40)
print(compile_result.stdout)
print("-" * 40)
if compile_result.stderr.strip():
print("Compilation stderr:")
print("-" * 40)
print(compile_result.stderr)
print("-" * 40)
if compile_result.returncode == 0:
print(f"✓ Elixir test repository compiled successfully in {repo_path}")
else:
print(f"⚠️ Warning: Compilation completed with exit code {compile_result.returncode}")
# Still continue - warnings are often non-fatal
print("=" * 60)
print(f"Total setup time: {time.time() - (start_time - compile_duration - deps_duration):.2f} seconds")
print("=" * 60)
except subprocess.TimeoutExpired as e:
print("=" * 60)
print(f"❌ TIMEOUT: Elixir setup timed out after {e.timeout} seconds")
print(f"Command: {' '.join(e.cmd)}")
print("This may indicate slow CI environment - Next LS may still work but with reduced functionality")
# Try to get partial output if available
if hasattr(e, 'stdout') and e.stdout:
if hasattr(e, "stdout") and e.stdout:
print("Partial stdout before timeout:")
print("-" * 40)
print(e.stdout)
print("-" * 40)
if hasattr(e, 'stderr') and e.stderr:
if hasattr(e, "stderr") and e.stderr:
print("Partial stderr before timeout:")
print("-" * 40)
print(e.stderr)
print("-" * 40)
print("=" * 60)
except FileNotFoundError:
print("❌ ERROR: 'mix' command not found - Elixir test repository may not be compiled")
print("Please ensure Elixir is installed and available in PATH")
@@ -142,14 +147,14 @@ def ensure_elixir_test_repo_compiled(repo_path: str) -> None:
@pytest.fixture(scope="session", autouse=True)
def setup_elixir_test_environment():
"""Automatically prepare Elixir test environment for all Elixir tests.
This fixture runs once per test session and automatically:
1. Installs dependencies via 'mix deps.get'
2. Compiles the Elixir test repository via 'mix compile'
It uses autouse=True so it runs automatically without needing to be explicitly
It uses autouse=True so it runs automatically without needing to be explicitly
requested by tests. This ensures Next LS has a fully prepared project to work with.
Uses generous timeouts (3-5 minutes) to accommodate slow CI environments.
All output is logged for transparency and debugging.
"""
@@ -162,8 +167,8 @@ def setup_elixir_test_environment():
@pytest.fixture(scope="session")
def elixir_test_repo_path(setup_elixir_test_environment):
"""Get the path to the prepared Elixir test repository.
This fixture depends on setup_elixir_test_environment to ensure dependencies
are installed and compilation has completed before returning the path.
"""
return setup_elixir_test_environment
return setup_elixir_test_environment
+14 -25
View File
@@ -6,19 +6,16 @@ like request_references using the test repository.
"""
import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_utils import SymbolUtils
from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
# These marks will be applied to all tests in this module
pytestmark = [
pytest.mark.elixir,
pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")
]
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
class TestElixirBasic:
@@ -29,7 +26,7 @@ class TestElixirBasic:
"""Test finding references to a function definition."""
file_path = os.path.join("lib", "models.ex")
symbols = language_server.request_document_symbols(file_path)
# Find the User module's 'new' function
user_new_symbol = None
for symbol in symbols[0]: # Top level symbols
@@ -39,22 +36,18 @@ class TestElixirBasic:
user_new_symbol = child
break
break
if not user_new_symbol or "selectionRange" not in user_new_symbol:
pytest.skip("User.new function or its selectionRange not found")
sel_start = user_new_symbol["selectionRange"]["start"]
references = language_server.request_references(
file_path, sel_start["line"], sel_start["character"]
)
references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
assert references is not None
assert len(references) > 0
# Should find at least one reference (the definition itself)
found_definition = any(
ref["uri"].endswith("models.ex") for ref in references
)
found_definition = any(ref["uri"].endswith("models.ex") for ref in references)
assert found_definition, "Should find the function definition"
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@@ -62,7 +55,7 @@ class TestElixirBasic:
"""Test finding references to create_user function."""
file_path = os.path.join("lib", "services.ex")
symbols = language_server.request_document_symbols(file_path)
# Find the UserService module's 'create_user' function
create_user_symbol = None
for symbol in symbols[0]: # Top level symbols
@@ -72,14 +65,12 @@ class TestElixirBasic:
create_user_symbol = child
break
break
if not create_user_symbol or "selectionRange" not in create_user_symbol:
pytest.skip("UserService.create_user function or its selectionRange not found")
sel_start = create_user_symbol["selectionRange"]["start"]
references = language_server.request_references(
file_path, sel_start["line"], sel_start["character"]
)
references = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
assert references is not None
assert len(references) > 0
@@ -89,7 +80,7 @@ class TestElixirBasic:
"""Test finding symbols that reference a specific function."""
file_path = os.path.join("lib", "models.ex")
symbols = language_server.request_document_symbols(file_path)
# Find the User module's 'new' function
user_new_symbol = None
for symbol in symbols[0]: # Top level symbols
@@ -99,14 +90,12 @@ class TestElixirBasic:
user_new_symbol = child
break
break
if not user_new_symbol or "selectionRange" not in user_new_symbol:
pytest.skip("User.new function or its selectionRange not found")
sel_start = user_new_symbol["selectionRange"]["start"]
referencing_symbols = language_server.request_referencing_symbols(
file_path, sel_start["line"], sel_start["character"]
)
referencing_symbols = language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
assert referencing_symbols is not None
@@ -120,4 +109,4 @@ class TestElixirBasic:
# Test multiple symbol requests in succession
for _ in range(3):
symbols = language_server.request_document_symbols("lib/services.ex")
assert symbols is not None
assert symbols is not None
@@ -10,10 +10,7 @@ from test.conftest import create_ls
from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
# These marks will be applied to all tests in this module
pytestmark = [
pytest.mark.elixir,
pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")
]
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
@pytest.fixture(scope="module")
@@ -34,7 +31,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
root_children = root["children"]
children_names = {child["name"] for child in root_children}
# Should have lib and test directories, but not scripts or ignored_dir
expected_dirs = {"lib", "test"}
assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}"
@@ -47,7 +44,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
"""Tests that find_references ignores the configured directory."""
# Location of User struct, which is referenced in scripts and ignored_dir
definition_file = "lib/models.ex"
# Find the User struct definition
symbols = ls_with_ignored_dirs.request_document_symbols(definition_file)
user_symbol = None
@@ -55,7 +52,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None)
if user_symbol:
break
if not user_symbol or "selectionRange" not in user_symbol:
pytest.skip("User symbol not found for reference testing")
@@ -73,13 +70,13 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
ignored_paths = ["*cripts", "ignored_*"] # codespell:ignore cripts
ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.ELIXIR)
ls.start()
try:
# Same as in the above tests
root = ls.request_full_symbol_tree()[0]
root_children = root["children"]
children_names = {child["name"] for child in root_children}
# Should have lib and test directories, but not scripts or ignored_dir
expected_dirs = {"lib", "test"}
assert expected_dirs.issubset(children_names), f"Expected {expected_dirs} to be in {children_names}"
@@ -88,7 +85,7 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
# Test that the refs and symbols with glob patterns are ignored
definition_file = "lib/models.ex"
# Find the User struct definition
symbols = ls.request_document_symbols(definition_file)
user_symbol = None
@@ -96,11 +93,11 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None)
if user_symbol:
break
if user_symbol and "selectionRange" in user_symbol:
sel_start = user_symbol["selectionRange"]["start"]
references = ls.request_references(definition_file, sel_start["line"], sel_start["character"])
# Assert that scripts and ignored_dir do not appear in references
assert not any("scripts" in ref["relativePath"] for ref in references), "scripts should be ignored (glob)"
assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)"
@@ -117,7 +114,7 @@ def test_default_ignored_directories(language_server: SolidLanguageServer):
assert language_server.is_ignored_dirname(".elixir_ls"), ".elixir_ls should be ignored"
assert language_server.is_ignored_dirname("cover"), "cover should be ignored"
assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored"
# Test that important directories are not ignored
assert not language_server.is_ignored_dirname("lib"), "lib should not be ignored"
assert not language_server.is_ignored_dirname("test"), "test should not be ignored"
@@ -129,17 +126,17 @@ def test_default_ignored_directories(language_server: SolidLanguageServer):
def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
"""Test that symbol tree excludes build and dependency directories."""
symbol_tree = language_server.request_full_symbol_tree()
if symbol_tree:
root = symbol_tree[0]
children_names = {child["name"] for child in root.get("children", [])}
# Build and dependency directories should not appear
ignored_dirs = {"_build", "deps", ".elixir_ls", "cover", "node_modules"}
found_ignored = ignored_dirs.intersection(children_names)
assert len(found_ignored) == 0, f"Found ignored directories in symbol tree: {found_ignored}"
# Important directories should appear
important_dirs = {"lib", "test"}
found_important = important_dirs.intersection(children_names)
assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}"
assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}"
+18 -23
View File
@@ -5,20 +5,18 @@ These tests verify that the language server works correctly with a real Elixir p
and can perform advanced operations like cross-file symbol resolution.
"""
import pytest
import os
from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
# These marks will be applied to all tests in this module
pytestmark = [
pytest.mark.elixir,
pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")
]
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
class TestElixirIntegration:
@@ -33,7 +31,7 @@ class TestElixirIntegration:
def test_elixir_repo_structure(self, elixir_test_repo_path):
"""Test that the Elixir test repository has the expected structure."""
repo_path = Path(elixir_test_repo_path)
# Check that key files exist
assert (repo_path / "mix.exs").exists(), "mix.exs should exist"
assert (repo_path / "lib" / "test_repo.ex").exists(), "main module should exist"
@@ -49,8 +47,7 @@ class TestElixirIntegration:
"""Test that symbols can be resolved across different files."""
# Test that User struct from models.ex can be found when referenced in services.ex
services_file = os.path.join("lib", "services.ex")
models_file = os.path.join("lib", "models.ex")
# Find where User is referenced in services.ex
content = language_server.retrieve_full_file_content(services_file)
lines = content.split("\n")
@@ -59,13 +56,13 @@ class TestElixirIntegration:
if "alias TestRepo.Models.{User" in line:
user_reference_line = i
break
if user_reference_line is None:
pytest.skip("Could not find User reference in services.ex")
# Try to find the definition
defining_symbol = language_server.request_defining_symbol(services_file, user_reference_line, 30)
if defining_symbol and "location" in defining_symbol:
# Should point to models.ex
assert "models.ex" in defining_symbol["location"]["uri"]
@@ -76,14 +73,14 @@ class TestElixirIntegration:
# Search for all function definitions
function_pattern = r"def\s+\w+\s*[\(\s]"
function_matches = language_server.search_files_for_pattern(function_pattern)
# Should find functions across multiple files
if function_matches:
files_with_functions = set()
for match in function_matches:
if match.source_file_path:
files_with_functions.add(os.path.basename(match.source_file_path))
# Should find functions in multiple files
expected_files = {"models.ex", "services.ex", "examples.ex", "utils.ex", "test_repo.ex"}
found_files = expected_files.intersection(files_with_functions)
@@ -92,7 +89,7 @@ class TestElixirIntegration:
# Search for struct definitions
struct_pattern = r"defstruct\s+\["
struct_matches = language_server.search_files_for_pattern(struct_pattern)
if struct_matches:
# Should find structs primarily in models.ex
models_structs = [m for m in struct_matches if m.source_file_path and "models.ex" in m.source_file_path]
@@ -103,7 +100,7 @@ class TestElixirIntegration:
"""Test that the language server understands Elixir module hierarchy."""
models_file = os.path.join("lib", "models.ex")
symbols = language_server.request_document_symbols(models_file)
if symbols:
# Flatten symbol structure
all_symbols = []
@@ -112,9 +109,9 @@ class TestElixirIntegration:
all_symbols.extend(symbol_group)
else:
all_symbols.append(symbol_group)
symbol_names = [s.get("name", "") for s in all_symbols]
# Should understand nested module structure
expected_modules = ["TestRepo.Models", "User", "Item", "Order"]
found_modules = [name for name in expected_modules if any(name in symbol_name for symbol_name in symbol_names)]
@@ -124,7 +121,7 @@ class TestElixirIntegration:
"""Test that the Elixir language recognizes the correct file extensions."""
language = Language.ELIXIR
matcher = language.get_source_fn_matcher()
# Test Elixir file extensions
assert matcher.is_relevant_filename("lib/test_repo.ex")
assert matcher.is_relevant_filename("test/test_repo_test.exs")
@@ -132,7 +129,7 @@ class TestElixirIntegration:
assert matcher.is_relevant_filename("mix.exs")
assert matcher.is_relevant_filename("lib/models.ex")
assert matcher.is_relevant_filename("lib/services.ex")
# Test non-Elixir files
assert not matcher.is_relevant_filename("README.md")
assert not matcher.is_relevant_filename("lib/test_repo.py")
@@ -142,12 +139,10 @@ class TestElixirIntegration:
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
def test_protocol_and_implementation_understanding(self, language_server: SolidLanguageServer):
"""Test that the language server understands Elixir protocols and implementations."""
models_file = os.path.join("lib", "models.ex")
# Search for protocol definitions
protocol_pattern = r"defprotocol\s+\w+"
protocol_matches = language_server.search_files_for_pattern(protocol_pattern, paths_include_glob="**/models.ex")
if protocol_matches:
# Should find the Serializable protocol
serializable_matches = [m for m in protocol_matches if "Serializable" in str(m)]
@@ -156,7 +151,7 @@ class TestElixirIntegration:
# Search for protocol implementations
impl_pattern = r"defimpl\s+\w+"
impl_matches = language_server.search_files_for_pattern(impl_pattern, paths_include_glob="**/models.ex")
if impl_matches:
# Should find multiple implementations
assert len(impl_matches) >= 3, f"Should find at least 3 protocol implementations, found {len(impl_matches)}"
assert len(impl_matches) >= 3, f"Should find at least 3 protocol implementations, found {len(impl_matches)}"
@@ -8,6 +8,7 @@ These tests focus on the following methods:
"""
import os
import pytest
from solidlsp import SolidLanguageServer
@@ -17,10 +18,7 @@ from solidlsp.ls_types import SymbolKind
from . import NEXTLS_UNAVAILABLE, NEXTLS_UNAVAILABLE_REASON
# These marks will be applied to all tests in this module
pytestmark = [
pytest.mark.elixir,
pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")
]
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(NEXTLS_UNAVAILABLE, reason=f"Next LS not available: {NEXTLS_UNAVAILABLE_REASON}")]
class TestElixirLanguageServerSymbols:
@@ -31,7 +29,7 @@ class TestElixirLanguageServerSymbols:
"""Test request_containing_symbol for a function."""
# Test for a position inside the create_user function
file_path = os.path.join("lib", "services.ex")
# Find the create_user function in the file
content = language_server.retrieve_full_file_content(file_path)
lines = content.split("\n")
@@ -40,7 +38,7 @@ class TestElixirLanguageServerSymbols:
if "def create_user(" in line:
create_user_line = i + 2 # Go inside the function body
break
if create_user_line is None:
pytest.skip("Could not find create_user function")
@@ -59,7 +57,7 @@ class TestElixirLanguageServerSymbols:
"""Test request_containing_symbol for a module."""
# Test for a position inside the UserService module but outside any function
file_path = os.path.join("lib", "services.ex")
# Find the UserService module definition
content = language_server.retrieve_full_file_content(file_path)
lines = content.split("\n")
@@ -68,7 +66,7 @@ class TestElixirLanguageServerSymbols:
if "defmodule UserService do" in line:
user_service_line = i + 1 # Go inside the module
break
if user_service_line is None:
pytest.skip("Could not find UserService module")
@@ -84,7 +82,7 @@ class TestElixirLanguageServerSymbols:
"""Test request_containing_symbol with nested scopes."""
# Test for a position inside a function which is inside a module
file_path = os.path.join("lib", "services.ex")
# Find a function inside UserService
content = language_server.retrieve_full_file_content(file_path)
lines = content.split("\n")
@@ -93,7 +91,7 @@ class TestElixirLanguageServerSymbols:
if "def create_user(" in line:
function_body_line = i + 3 # Go deeper into the function body
break
if function_body_line is None:
pytest.skip("Could not find function body")
@@ -116,29 +114,27 @@ class TestElixirLanguageServerSymbols:
# This is acceptable behavior for module-level positions
assert containing_symbol is None or containing_symbol == {} or "TestRepo.Services" in str(containing_symbol)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
def test_request_referencing_symbols_struct(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a struct."""
# Test referencing symbols for User struct
file_path = os.path.join("lib", "models.ex")
symbols = language_server.request_document_symbols(file_path)
user_symbol = None
for symbol_group in symbols:
user_symbol = next((s for s in symbol_group if "User" in s.get("name", "")), None)
if user_symbol:
break
if not user_symbol or "selectionRange" not in user_symbol:
pytest.skip("User symbol or its selectionRange not found")
sel_start = user_symbol["selectionRange"]["start"]
ref_symbols = [
ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
]
if ref_symbols:
services_references = [
symbol
@@ -175,7 +171,7 @@ class TestElixirLanguageServerSymbols:
if "User.new(" in line:
user_new_call_line = i
break
if user_new_call_line is None:
pytest.skip("Could not find User.new call")
@@ -199,7 +195,7 @@ class TestElixirLanguageServerSymbols:
if "alias TestRepo.Models.{User" in line:
user_usage_line = i
break
if user_usage_line is None:
pytest.skip("Could not find User struct usage")
@@ -210,9 +206,9 @@ class TestElixirLanguageServerSymbols:
@pytest.mark.xfail(
reason="Known intermittent bug in Next LS v0.23.3: Protocol.UndefinedError for :timeout atom. "
"Occurs in CI environments but may pass locally. "
"See https://github.com/elixir-tools/next-ls/issues/543",
strict=False
"Occurs in CI environments but may pass locally. "
"See https://github.com/elixir-tools/next-ls/issues/543",
strict=False,
)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
@@ -229,7 +225,7 @@ class TestElixirLanguageServerSymbols:
def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
"""Test integration between different symbol methods."""
file_path = os.path.join("lib", "models.ex")
# Find User struct definition
content = language_server.retrieve_full_file_content(file_path)
lines = content.split("\n")
@@ -238,20 +234,20 @@ class TestElixirLanguageServerSymbols:
if "defmodule User do" in line:
user_struct_line = i
break
if user_struct_line is None:
pytest.skip("Could not find User struct")
# Test containing symbol
containing = language_server.request_containing_symbol(file_path, user_struct_line + 5, 10)
if containing:
# Test that we can find references to this symbol
if "location" in containing and "range" in containing["location"]:
start_pos = containing["location"]["range"]["start"]
refs = [ref.symbol for ref in language_server.request_referencing_symbols(
file_path, start_pos["line"], start_pos["character"]
)]
refs = [
ref.symbol for ref in language_server.request_referencing_symbols(file_path, start_pos["line"], start_pos["character"])
]
# We should find some references or none (both are valid outcomes)
assert isinstance(refs, list)
@@ -259,21 +255,21 @@ class TestElixirLanguageServerSymbols:
def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
"""Test that symbol tree structure is correctly built."""
symbol_tree = language_server.request_full_symbol_tree()
# Should get a tree structure
assert len(symbol_tree) > 0
# Should have our test repository structure
root = symbol_tree[0]
assert "children" in root
# Look for lib directory
lib_dir = None
for child in root["children"]:
if child["name"] == "lib":
lib_dir = child
break
if lib_dir:
# Next LS returns module names instead of file names (e.g., 'services' instead of 'services.ex')
file_names = [child["name"] for child in lib_dir.get("children", [])]
@@ -285,14 +281,14 @@ class TestElixirLanguageServerSymbols:
def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
"""Test request_dir_overview functionality."""
lib_overview = language_server.request_dir_overview("lib")
# Should get an overview of the lib directory
assert lib_overview is not None
# Next LS returns keys like 'lib/services.ex' instead of just 'lib'
overview_keys = list(lib_overview.keys()) if hasattr(lib_overview, 'keys') else []
lib_files = [key for key in overview_keys if key.startswith('lib/')]
overview_keys = list(lib_overview.keys()) if hasattr(lib_overview, "keys") else []
lib_files = [key for key in overview_keys if key.startswith("lib/")]
assert len(lib_files) > 0, f"Expected to find lib/ files in overview keys: {overview_keys}"
# Should contain information about our modules
overview_text = str(lib_overview).lower()
expected_terms = ["models", "services", "user", "item"]
@@ -309,10 +305,10 @@ class TestElixirLanguageServerSymbols:
# #
# file_path = os.path.join("lib", "models.ex")
# doc_overview = language_server.request_document_overview(file_path)
#
#
# # Should get an overview of the models.ex file
# assert doc_overview is not None
#
#
# # Should contain information about our structs and functions
# overview_text = str(doc_overview).lower()
# expected_terms = ["user", "item", "order", "struct", "defmodule"]
@@ -323,7 +319,7 @@ class TestElixirLanguageServerSymbols:
def test_containing_symbol_of_module_attribute(self, language_server: SolidLanguageServer) -> None:
"""Test containing symbol for module attributes."""
file_path = os.path.join("lib", "models.ex")
# Find a module attribute like @type or @doc
content = language_server.retrieve_full_file_content(file_path)
lines = content.split("\n")
@@ -332,15 +328,15 @@ class TestElixirLanguageServerSymbols:
if line.strip().startswith("@type") or line.strip().startswith("@doc"):
attribute_line = i
break
if attribute_line is None:
pytest.skip("Could not find module attribute")
containing_symbol = language_server.request_containing_symbol(file_path, attribute_line, 5)
if containing_symbol:
# Should be contained within a module
assert "name" in containing_symbol
# The containing symbol should be a module
expected_names = ["User", "Item", "Order", "TestRepo.Models"]
assert any(name in containing_symbol["name"] for name in expected_names)
assert any(name in containing_symbol["name"] for name in expected_names)