Merge pull request #797 from w0wl0lxd/feature/toml-support

feat: Add TOML language support via Taplo
This commit is contained in:
Michael Panchenko authored and GitHub committed 2025-12-12 11:07:59 +07:00
commit c35f0fa5b5
13 files changed
+1241 -2

No files matched your search

+1
View File
@@ -47,6 +47,7 @@ Status of the `main` branch. Changes prior to the next official version change w
* **Add support for Lua** via lua-language-server
* **Add support for Nix** requires nixd installation (Windows not supported)
* **Add experimental support for YAML** via yaml-language-server with LSP integration for .yaml and .yml files
* **Add support for TOML** via Taplo language server with automatic binary download, validation, formatting, and schema support for .toml files
* **Dart now officially supported**: Dart was always working, but now tests were added, and it is promoted to "officially supported"
* **Rust now uses already installed rustup**: The rust-analyzer is no longer bundled with Serena. Instead, it uses the rust-analyzer from your Rust toolchain managed by rustup. This ensures compatibility with your Rust version and eliminates outdated bundled binaries.
* **Kotlin now officially supported**: We now use the official Kotlin LS, tests run through and performance is good, even though the LS is in an early development stage.
+1 -1
View File
@@ -68,7 +68,7 @@ than existing solutions that charge a premium.
Language servers provide support for a wide range of programming languages.
With Serena's LSP library, we provide **support for over 30 programming languages**, including
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TypeScript, YAML and Zig.
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, YAML, and Zig.
> [!IMPORTANT]
> Some languages require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details.
+1
View File
@@ -303,6 +303,7 @@ markers = [
"yaml: language server running for YAML",
"powershell: language server running for PowerShell",
"slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
"toml: language server running for TOML",
]
[tool.codespell]
@@ -0,0 +1,316 @@
"""
Provides TOML specific instantiation of the LanguageServer class using Taplo.
Contains various configurations and settings specific to TOML files.
"""
import gzip
import hashlib
import logging
import os
import platform
import shutil
import socket
import stat
import threading
import urllib.request
from typing import Any
# Download timeout in seconds (prevents indefinite hangs)
DOWNLOAD_TIMEOUT_SECONDS = 120
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PathUtils
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
# Taplo release version and download URLs
TAPLO_VERSION = "0.10.0"
TAPLO_DOWNLOAD_BASE = f"https://github.com/tamasfe/taplo/releases/download/{TAPLO_VERSION}"
# SHA256 checksums for Taplo releases (verified from official GitHub releases)
# Source: https://github.com/tamasfe/taplo/releases/tag/0.10.0
# To update: download each release file and run: sha256sum <filename>
TAPLO_SHA256_CHECKSUMS: dict[str, str] = {
"taplo-windows-x86_64.zip": "1615eed140039bd58e7089109883b1c434de5d6de8f64a993e6e8c80ca57bdf9",
"taplo-windows-x86.zip": "b825701daab10dcfc0251e6d668cd1a9c0e351e7f6762dd20844c3f3f3553aa0",
"taplo-darwin-x86_64.gz": "898122cde3a0b1cd1cbc2d52d3624f23338218c91b5ddb71518236a4c2c10ef2",
"taplo-darwin-aarch64.gz": "713734314c3e71894b9e77513c5349835eefbd52908445a0d73b0c7dc469347d",
"taplo-linux-x86_64.gz": "8fe196b894ccf9072f98d4e1013a180306e17d244830b03986ee5e8eabeb6156",
"taplo-linux-aarch64.gz": "033681d01eec8376c3fd38fa3703c79316f5e14bb013d859943b60a07bccdcc3",
"taplo-linux-armv7.gz": "6b728896afe2573522f38b8e668b1ff40eb5928fd9d6d0c253ecae508274d417",
}
def _verify_sha256(file_path: str, expected_hash: str) -> bool:
"""Verify SHA256 checksum of a downloaded file."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256_hash.update(chunk)
actual_hash = sha256_hash.hexdigest()
return actual_hash.lower() == expected_hash.lower()
def _get_taplo_download_url() -> tuple[str, str]:
"""
Get the appropriate Taplo download URL for the current platform.
Returns:
Tuple of (download_url, executable_name)
"""
system = platform.system().lower()
machine = platform.machine().lower()
# Map machine architecture to Taplo naming convention
arch_map = {
"x86_64": "x86_64",
"amd64": "x86_64",
"x86": "x86",
"i386": "x86",
"i686": "x86",
"aarch64": "aarch64",
"arm64": "aarch64",
"armv7l": "armv7",
}
arch = arch_map.get(machine, "x86_64") # Default to x86_64
if system == "windows":
filename = f"taplo-windows-{arch}.zip"
executable = "taplo.exe"
elif system == "darwin":
filename = f"taplo-darwin-{arch}.gz"
executable = "taplo"
else: # Linux and others
filename = f"taplo-linux-{arch}.gz"
executable = "taplo"
return f"{TAPLO_DOWNLOAD_BASE}/{filename}", executable
class TaploServer(SolidLanguageServer):
"""
Provides TOML specific instantiation of the LanguageServer class using Taplo.
Taplo is a TOML toolkit with LSP support for validation, formatting, and schema support.
"""
@staticmethod
def _determine_log_level(line: str) -> int:
"""Classify Taplo stderr output to avoid false-positive errors."""
line_lower = line.lower()
# Known informational messages from Taplo
if any(
[
"schema" in line_lower and "not found" in line_lower,
"warning" in line_lower,
]
):
return logging.DEBUG
return SolidLanguageServer._determine_log_level(line)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
"""
Creates a TaploServer instance. This class is not meant to be instantiated directly.
Use LanguageServer.create() instead.
"""
taplo_executable_path = self._setup_runtime_dependencies(solidlsp_settings)
super().__init__(
config,
repository_root_path,
ProcessLaunchInfo(cmd=f"{taplo_executable_path} lsp stdio", cwd=repository_root_path),
"toml",
solidlsp_settings,
)
self.server_ready = threading.Event()
@classmethod
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
"""
Setup runtime dependencies for Taplo and return the command to start the server.
"""
# First check if taplo is already installed system-wide
system_taplo = shutil.which("taplo")
if system_taplo:
log.info(f"Using system-installed Taplo at: {system_taplo}")
return system_taplo
# Setup local installation directory
taplo_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "taplo")
os.makedirs(taplo_dir, exist_ok=True)
_, executable_name = _get_taplo_download_url()
taplo_executable = os.path.join(taplo_dir, executable_name)
if os.path.exists(taplo_executable) and os.access(taplo_executable, os.X_OK):
log.info(f"Using cached Taplo at: {taplo_executable}")
return taplo_executable
# Download and install Taplo
log.info(f"Taplo not found. Downloading version {TAPLO_VERSION}...")
cls._download_taplo(taplo_dir, taplo_executable)
if not os.path.exists(taplo_executable):
raise FileNotFoundError(
f"Taplo executable not found at {taplo_executable}. "
"Installation may have failed. Try installing manually: cargo install taplo-cli --locked"
)
return taplo_executable
@classmethod
def _download_taplo(cls, install_dir: str, executable_path: str) -> None:
"""Download and extract Taplo binary with SHA256 verification."""
# TODO: consider using existing download utilities in SolidLSP instead of the custom logic here
download_url, _ = _get_taplo_download_url()
archive_filename = os.path.basename(download_url)
try:
log.info(f"Downloading Taplo from: {download_url}")
archive_path = os.path.join(install_dir, archive_filename)
# Download the archive with timeout to prevent indefinite hangs
old_timeout = socket.getdefaulttimeout()
try:
socket.setdefaulttimeout(DOWNLOAD_TIMEOUT_SECONDS)
urllib.request.urlretrieve(download_url, archive_path)
finally:
socket.setdefaulttimeout(old_timeout)
# Verify SHA256 checksum
expected_hash = TAPLO_SHA256_CHECKSUMS.get(archive_filename)
if expected_hash:
if not _verify_sha256(archive_path, expected_hash):
os.remove(archive_path)
raise RuntimeError(
f"SHA256 checksum verification failed for {archive_filename}. "
"The downloaded file may be corrupted or tampered with. "
"Try installing manually: cargo install taplo-cli --locked"
)
log.info(f"SHA256 checksum verified for {archive_filename}")
else:
log.warning(
f"No SHA256 checksum available for {archive_filename}. "
"Skipping verification - consider installing manually: cargo install taplo-cli --locked"
)
# Extract based on format
if archive_path.endswith(".gz") and not archive_path.endswith(".tar.gz"):
# Single file gzip
with gzip.open(archive_path, "rb") as f_in:
with open(executable_path, "wb") as f_out:
f_out.write(f_in.read())
elif archive_path.endswith(".zip"):
import zipfile
with zipfile.ZipFile(archive_path, "r") as zip_ref:
# Security: Validate paths to prevent zip slip vulnerability
for member in zip_ref.namelist():
member_path = os.path.normpath(os.path.join(install_dir, member))
if not member_path.startswith(os.path.normpath(install_dir)):
raise RuntimeError(f"Zip slip detected: {member} attempts to escape install directory")
zip_ref.extractall(install_dir)
# Make executable on Unix systems
if os.name != "nt":
os.chmod(executable_path, os.stat(executable_path).st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
# Clean up archive
os.remove(archive_path)
log.info(f"Taplo installed successfully at: {executable_path}")
except Exception as e:
log.error(f"Failed to download Taplo: {e}")
raise RuntimeError(
f"Failed to download Taplo from {download_url}. Try installing manually: cargo install taplo-cli --locked"
) from e
@staticmethod
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
"""
Returns the initialize params for the Taplo Language Server.
"""
root_uri = PathUtils.path_to_uri(repository_absolute_path)
initialize_params = {
"locale": "en",
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
"completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
"definition": {"dynamicRegistration": True},
"references": {"dynamicRegistration": True},
"documentSymbol": {
"dynamicRegistration": True,
"hierarchicalDocumentSymbolSupport": True,
"symbolKind": {"valueSet": list(range(1, 27))},
},
"hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
"codeAction": {"dynamicRegistration": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"symbol": {"dynamicRegistration": True},
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
}
return initialize_params # type: ignore
def _start_server(self) -> None:
"""
Starts the Taplo Language Server and initializes it.
"""
def register_capability_handler(params: Any) -> None:
return
def do_nothing(params: Any) -> None:
return
def window_log_message(msg: dict) -> None:
log.info(f"LSP: window/logMessage: {msg}")
self.server.on_request("client/registerCapability", register_capability_handler)
self.server.on_notification("window/logMessage", window_log_message)
self.server.on_notification("$/progress", do_nothing)
self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
log.info("Starting Taplo server process")
self.server.start()
initialize_params = self._get_initialize_params(self.repository_root_path)
log.info("Sending initialize request to Taplo server")
init_response = self.server.send.initialize(initialize_params)
log.debug(f"Received initialize response from Taplo: {init_response}")
# Verify document symbol support
capabilities = init_response.get("capabilities", {})
if capabilities.get("documentSymbolProvider"):
log.info("Taplo server supports document symbols")
else:
log.warning("Taplo server may have limited document symbol support")
self.server.notify.initialized({})
log.info("Taplo server initialization complete")
self.server_ready.set()
self.completions_available.set()
def is_ignored_dirname(self, dirname: str) -> bool:
"""Define TOML-specific directories to ignore."""
return super().is_ignored_dirname(dirname) or dirname in ["target", ".cargo", "node_modules"]
+23 -1
View File
@@ -85,6 +85,10 @@ class Language(str, Enum):
"""YAML language server (experimental).
Must be explicitly specified as the main language, not auto-detected.
"""
TOML = "toml"
"""TOML language server using Taplo.
Supports TOML validation, formatting, and schema support.
"""
@classmethod
def iter_all(cls, include_experimental: bool = False) -> Iterable[Self]:
@@ -95,8 +99,20 @@ class Language(str, Enum):
def is_experimental(self) -> bool:
"""
Check if the language server is experimental or deprecated.
Note for serena users/developers:
Experimental languages are not autodetected and must be explicitly specified
in the project.yml configuration.
"""
return self in {self.TYPESCRIPT_VTS, self.PYTHON_JEDI, self.CSHARP_OMNISHARP, self.RUBY_SOLARGRAPH, self.MARKDOWN, self.YAML}
return self in {
self.TYPESCRIPT_VTS,
self.PYTHON_JEDI,
self.CSHARP_OMNISHARP,
self.RUBY_SOLARGRAPH,
self.MARKDOWN,
self.YAML,
self.TOML,
}
def __str__(self) -> str:
return self.value
@@ -168,6 +184,8 @@ class Language(str, Enum):
return FilenameMatcher("*.sh", "*.bash")
case self.YAML:
return FilenameMatcher("*.yaml", "*.yml")
case self.TOML:
return FilenameMatcher("*.toml")
case self.ZIG:
return FilenameMatcher("*.zig", "*.zon")
case self.LUA:
@@ -304,6 +322,10 @@ class Language(str, Enum):
from solidlsp.language_servers.yaml_language_server import YamlLanguageServer
return YamlLanguageServer
case self.TOML:
from solidlsp.language_servers.taplo_server import TaploServer
return TaploServer
case self.ZIG:
from solidlsp.language_servers.zls import ZigLanguageServer
@@ -0,0 +1,30 @@
[package]
name = "test_project"
version = "0.1.0"
edition = "2021"
description = "A test TOML file for Serena TOML language support"
authors = ["Test Author <test@example.com>"]
license = "MIT"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
[dev-dependencies]
proptest = "1.0"
[features]
default = ["feature1"]
feature1 = []
feature2 = ["feature1"]
[profile.release]
lto = true
opt-level = 3
[[bin]]
name = "main"
path = "src/main.rs"
[workspace]
members = ["crates/*"]
@@ -0,0 +1,66 @@
# Configuration file with various TOML features for testing
[server]
host = "localhost"
port = 8080
debug = false
timeout = 30.5
# Inline table example
endpoint = { url = "https://api.example.com", version = "v1" }
[server.ssl]
enabled = true
cert_path = "/etc/ssl/cert.pem"
key_path = "/etc/ssl/key.pem"
[database]
connection_string = """
postgresql://user:password@localhost:5432/mydb?
sslmode=require&
pool_size=10
"""
[database.pool]
min_connections = 5
max_connections = 20
idle_timeout = 300
[logging]
level = "info"
format = "json"
outputs = ["stdout", "file"]
[logging.file]
path = "/var/log/app.log"
max_size = 10485760 # 10MB
max_backups = 5
compress = true
# Array of inline tables
[[endpoints]]
name = "users"
path = "/api/users"
methods = ["GET", "POST", "PUT", "DELETE"]
auth_required = true
[[endpoints]]
name = "health"
path = "/health"
methods = ["GET"]
auth_required = false
# Datetime values
[metadata]
created = 1979-05-27T07:32:00Z
updated = 2024-01-15T10:30:00-05:00
# Special characters in strings
[messages]
welcome = "Hello, World!"
multiline = '''
This is a
multiline literal
string.
'''
with_escapes = "Line1\nLine2\tTabbed"
@@ -0,0 +1,43 @@
[project]
name = "test-project"
version = "0.1.0"
description = "A test Python project for TOML support"
readme = "README.md"
requires-python = ">=3.11"
license = {text = "MIT"}
authors = [
{name = "Test Author", email = "test@example.com"}
]
dependencies = [
"pydantic>=2.0",
"httpx>=0.24",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"ruff>=0.1",
"mypy>=1.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 88
target-version = "py311"
select = ["E", "F", "I", "N", "W", "B", "S"]
[tool.ruff.isort]
known-first-party = ["test_project"]
[tool.mypy]
python_version = "3.11"
strict = true
warn_unreachable = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
addopts = "-v --cov=src"
+1
View File
@@ -0,0 +1 @@
"""TOML language server tests."""
+235
View File
@@ -0,0 +1,235 @@
"""
Basic integration tests for the TOML language server functionality.
These tests validate the functionality of the Taplo language server APIs
like request_document_symbols using the TOML test repository.
"""
from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
@pytest.mark.toml
class TestTomlLanguageServerBasics:
"""Test basic functionality of the TOML language server (Taplo)."""
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_toml_language_server_initialization(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that TOML language server can be initialized successfully."""
assert language_server is not None
assert language_server.language == Language.TOML
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_toml_cargo_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test document symbols detection in Cargo.toml with specific symbol verification."""
all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
assert all_symbols is not None, "Should return symbols for Cargo.toml"
assert len(all_symbols) > 0, f"Should find symbols in Cargo.toml, found {len(all_symbols)}"
# Verify specific top-level table names are detected
symbol_names = [sym.get("name") for sym in all_symbols]
assert "package" in symbol_names, "Should detect 'package' table in Cargo.toml"
assert "dependencies" in symbol_names, "Should detect 'dependencies' table in Cargo.toml"
assert "dev-dependencies" in symbol_names, "Should detect 'dev-dependencies' table in Cargo.toml"
assert "features" in symbol_names, "Should detect 'features' table in Cargo.toml"
assert "workspace" in symbol_names, "Should detect 'workspace' table in Cargo.toml"
# Verify nested symbols exist (keys under 'package')
assert "name" in symbol_names, "Should detect nested 'name' key"
assert "version" in symbol_names, "Should detect nested 'version' key"
assert "edition" in symbol_names, "Should detect nested 'edition' key"
# Check symbol kind for tables - Taplo uses kind 19 (object) for TOML tables
package_symbol = next((s for s in all_symbols if s.get("name") == "package"), None)
assert package_symbol is not None, "Should find 'package' symbol"
assert package_symbol.get("kind") == 19, "Top-level table should have kind 19 (object)"
dependencies_symbol = next((s for s in all_symbols if s.get("name") == "dependencies"), None)
assert dependencies_symbol is not None, "Should find 'dependencies' symbol"
assert dependencies_symbol.get("kind") == 19, "'dependencies' table should have kind 19 (object)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_toml_pyproject_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test document symbols detection in pyproject.toml."""
all_symbols, root_symbols = language_server.request_document_symbols("pyproject.toml").get_all_symbols_and_roots()
assert all_symbols is not None, "Should return symbols for pyproject.toml"
assert len(all_symbols) > 0, f"Should find symbols in pyproject.toml, found {len(all_symbols)}"
# Verify specific top-level table names
symbol_names = [sym.get("name") for sym in all_symbols]
assert "project" in symbol_names, "Should detect 'project' table"
assert "build-system" in symbol_names, "Should detect 'build-system' table"
# Verify tool sections (nested tables)
# These could appear as 'tool' or 'tool.ruff' depending on Taplo's parsing
has_tool_section = any("tool" in name for name in symbol_names if name)
assert has_tool_section, "Should detect tool sections"
# Verify nested keys under project
assert "name" in symbol_names, "Should detect 'name' under project"
assert "version" in symbol_names, "Should detect 'version' under project"
assert "requires-python" in symbol_names or "dependencies" in symbol_names, "Should detect project dependencies"
# Check symbol kind for tables - Taplo uses kind 19 (object) for TOML tables
project_symbol = next((s for s in all_symbols if s.get("name") == "project"), None)
assert project_symbol is not None, "Should find 'project' symbol"
assert project_symbol.get("kind") == 19, "'project' table should have kind 19 (object)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_toml_symbol_kinds(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that TOML symbols have appropriate LSP kinds for different value types."""
all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
assert all_symbols is not None
assert len(all_symbols) > 0
# Check boolean symbol kind (lto = true at line 22)
# LSP kind 17 = boolean
lto_symbol = next((s for s in all_symbols if s.get("name") == "lto"), None)
assert lto_symbol is not None, "Should find 'lto' boolean symbol"
assert lto_symbol.get("kind") == 17, "'lto' should have kind 17 (boolean)"
# Check number symbol kind (opt-level = 3 at line 23)
# LSP kind 16 = number
opt_level_symbol = next((s for s in all_symbols if s.get("name") == "opt-level"), None)
assert opt_level_symbol is not None, "Should find 'opt-level' number symbol"
assert opt_level_symbol.get("kind") == 16, "'opt-level' should have kind 16 (number)"
# Check string symbol kind (name = "test_project" at line 2)
# LSP kind 15 = string
name_symbols = [s for s in all_symbols if s.get("name") == "name"]
assert len(name_symbols) > 0, "Should find 'name' symbols"
# At least one should be a string
string_name_symbol = next((s for s in name_symbols if s.get("kind") == 15), None)
assert string_name_symbol is not None, "Should find 'name' with kind 15 (string)"
# Check array symbol kind (default = ["feature1"] at line 17)
# LSP kind 18 = array
default_symbol = next((s for s in all_symbols if s.get("name") == "default"), None)
assert default_symbol is not None, "Should find 'default' array symbol"
assert default_symbol.get("kind") == 18, "'default' should have kind 18 (array)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_toml_symbols_with_body(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test request_document_symbols with body extraction."""
all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
assert all_symbols is not None, "Should return symbols for Cargo.toml"
assert len(all_symbols) > 0, "Should have symbols"
# Find the 'package' symbol and verify its body
package_symbol = next((s for s in all_symbols if s.get("name") == "package"), None)
assert package_symbol is not None, "Should find 'package' symbol"
# Check that body exists and contains expected content
# Note: Taplo includes the section header in the body
assert "body" in package_symbol, "'package' symbol should have body"
package_body = package_symbol["body"]
assert 'name = "test_project"' in package_body, "Body should contain 'name' field"
assert 'version = "0.1.0"' in package_body, "Body should contain 'version' field"
assert 'edition = "2021"' in package_body, "Body should contain 'edition' field"
# Find the dependencies symbol and check its body
deps_symbol = next((s for s in all_symbols if s.get("name") == "dependencies"), None)
assert deps_symbol is not None, "Should find 'dependencies' symbol"
assert "body" in deps_symbol, "'dependencies' symbol should have body"
deps_body = deps_symbol["body"]
assert "serde" in deps_body, "Body should contain serde dependency"
assert "tokio" in deps_body, "Body should contain tokio dependency"
# Find the top-level [features] section (not the nested 'features' in serde dependency)
# The [features] section should be kind 19 (object) and at line 15 (0-indexed)
features_symbols = [s for s in all_symbols if s.get("name") == "features"]
# Find the top-level one - should be kind 19 (object) with children
features_symbol = next(
(s for s in features_symbols if s.get("kind") == 19 and s.get("children")),
None,
)
assert features_symbol is not None, "Should find top-level 'features' table symbol"
assert "body" in features_symbol, "'features' symbol should have body"
features_body = features_symbol["body"]
assert "default" in features_body, "Body should contain 'default' feature"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_toml_symbol_ranges(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that symbols have proper range information."""
all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
assert all_symbols is not None
assert len(all_symbols) > 0
# Check the 'package' symbol range - should start at line 0 (0-indexed, actual line 1)
package_symbol = next((s for s in all_symbols if s.get("name") == "package"), None)
assert package_symbol is not None, "Should find 'package' symbol"
assert "range" in package_symbol, "'package' symbol should have range"
package_range = package_symbol["range"]
assert "start" in package_range, "Range should have start"
assert "end" in package_range, "Range should have end"
assert package_range["start"]["line"] == 0, "'package' should start at line 0 (0-indexed, actual line 1)"
# Package block spans from line 1 to line 7 in file (1-indexed)
# In 0-indexed LSP coordinates: line 0 (start) to line 6 or 7 (end)
assert package_range["end"]["line"] >= 6, "'package' should end at or after line 6 (0-indexed)"
# Check a nested symbol range - 'name' under package at line 2 (1-indexed), line 1 (0-indexed)
name_symbols = [s for s in all_symbols if s.get("name") == "name"]
assert len(name_symbols) > 0, "Should find 'name' symbols"
# Find the one under 'package' (should be at line 1 in 0-indexed)
package_name = next((s for s in name_symbols if s["range"]["start"]["line"] == 1), None)
assert package_name is not None, "Should find 'name' under 'package'"
# Check the dependencies range - starts at line 9 (1-indexed), line 8 (0-indexed)
deps_symbol = next((s for s in all_symbols if s.get("name") == "dependencies"), None)
assert deps_symbol is not None, "Should find 'dependencies' symbol"
deps_range = deps_symbol["range"]
assert deps_range["start"]["line"] == 8, "'dependencies' should start at line 8 (0-indexed, actual line 9)"
# Check that range includes line and character positions
assert "line" in package_range["start"], "Start should have line"
assert "character" in package_range["start"], "Start should have character"
assert "line" in package_range["end"], "End should have line"
assert "character" in package_range["end"], "End should have character"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_toml_nested_table_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test detection of nested table symbols like profile.release and tool.ruff."""
# Test Cargo.toml for profile.release
cargo_symbols, _ = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
assert cargo_symbols is not None
symbol_names = [sym.get("name") for sym in cargo_symbols]
# Should detect profile.release or profile section
has_profile = any("profile" in name for name in symbol_names if name)
assert has_profile, "Should detect profile section in Cargo.toml"
# Test pyproject.toml for tool sections
pyproject_symbols, _ = language_server.request_document_symbols("pyproject.toml").get_all_symbols_and_roots()
assert pyproject_symbols is not None
pyproject_names = [sym.get("name") for sym in pyproject_symbols]
# Should detect tool.ruff, tool.mypy sections
has_ruff = any("ruff" in name for name in pyproject_names if name)
has_mypy = any("mypy" in name for name in pyproject_names if name)
assert has_ruff or has_mypy, "Should detect tool sections in pyproject.toml"
# Verify pyproject has expected boolean: strict = true
strict_symbol = next((s for s in pyproject_symbols if s.get("name") == "strict"), None)
if strict_symbol:
assert strict_symbol.get("kind") == 17, "'strict' should have kind 17 (boolean)"
+273
View File
@@ -0,0 +1,273 @@
"""
Tests for TOML language server edge cases and advanced features.
These tests cover:
- Inline tables
- Multiline strings
- Arrays of tables
- Nested tables
- Various TOML data types
"""
from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
pytestmark = pytest.mark.toml
class TestTomlEdgeCases:
"""Test TOML language server handling of edge cases and advanced features."""
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_inline_table_detection(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that inline tables are properly detected."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
assert all_symbols is not None
assert len(all_symbols) > 0
symbol_names = [sym.get("name") for sym in all_symbols]
# The inline table 'endpoint' should be detected
assert "endpoint" in symbol_names, "Should detect 'endpoint' inline table"
# Find the endpoint symbol and check its properties
endpoint_symbol = next((s for s in all_symbols if s.get("name") == "endpoint"), None)
assert endpoint_symbol is not None
# Inline tables should be kind 19 (object)
assert endpoint_symbol.get("kind") == 19, "Inline table should have kind 19 (object)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_nested_table_detection(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that deeply nested tables are properly detected."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect nested tables like server.ssl and database.pool
has_ssl = any("ssl" in str(name).lower() for name in symbol_names if name)
has_pool = any("pool" in str(name).lower() for name in symbol_names if name)
assert has_ssl, f"Should detect 'server.ssl' nested table, got: {symbol_names}"
assert has_pool, f"Should detect 'database.pool' nested table, got: {symbol_names}"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_array_of_tables_detection(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that [[array_of_tables]] syntax is properly detected."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect [[endpoints]] array of tables
assert "endpoints" in symbol_names, f"Should detect '[[endpoints]]' array of tables, got: {symbol_names}"
# Find the endpoints symbol
endpoints_symbol = next((s for s in all_symbols if s.get("name") == "endpoints"), None)
assert endpoints_symbol is not None
# Array of tables should be kind 18 (array)
assert endpoints_symbol.get("kind") == 18, "Array of tables should have kind 18 (array)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_multiline_string_handling(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that multiline strings are handled correctly."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect connection_string and multiline fields
assert "connection_string" in symbol_names, "Should detect 'connection_string' with multiline value"
assert "multiline" in symbol_names, "Should detect 'multiline' literal string"
# Find connection_string and verify it's a string type
conn_symbol = next((s for s in all_symbols if s.get("name") == "connection_string"), None)
assert conn_symbol is not None
# String type should be kind 15
assert conn_symbol.get("kind") == 15, "Multiline string should have kind 15 (string)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_array_value_detection(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that array values are properly detected."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect 'outputs' and 'methods' arrays
assert "outputs" in symbol_names, "Should detect 'outputs' array"
assert "methods" in symbol_names, "Should detect 'methods' array"
# Find outputs array and verify kind
outputs_symbol = next((s for s in all_symbols if s.get("name") == "outputs"), None)
assert outputs_symbol is not None
# Arrays should have kind 18
assert outputs_symbol.get("kind") == 18, "'outputs' should have kind 18 (array)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_float_value_detection(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that float values are properly detected."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect 'timeout' which has a float value (30.5)
assert "timeout" in symbol_names, "Should detect 'timeout' float value"
# Find timeout and verify it's a number
timeout_symbol = next((s for s in all_symbols if s.get("name") == "timeout"), None)
assert timeout_symbol is not None
# Numbers should have kind 16
assert timeout_symbol.get("kind") == 16, "'timeout' should have kind 16 (number)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_datetime_value_detection(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that datetime values are detected."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect metadata section with datetime values
assert "metadata" in symbol_names, "Should detect 'metadata' section"
assert "created" in symbol_names, "Should detect 'created' datetime field"
assert "updated" in symbol_names, "Should detect 'updated' datetime field"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_symbol_body_with_inline_table(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that symbol bodies include inline table content."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
# Find the endpoint symbol with body
endpoint_symbol = next((s for s in all_symbols if s.get("name") == "endpoint"), None)
assert endpoint_symbol is not None
if "body" in endpoint_symbol:
body = endpoint_symbol["body"]
# Body should contain the inline table syntax
assert "url" in body or "version" in body, f"Body should contain inline table contents, got: {body}"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_symbol_ranges_in_config(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that symbol ranges are correct in config.toml."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
# Find the server symbol
server_symbol = next((s for s in all_symbols if s.get("name") == "server"), None)
assert server_symbol is not None
assert "range" in server_symbol
# Server should start near the beginning (line 2 is [server], 0-indexed: line 2)
server_range = server_symbol["range"]
assert server_range["start"]["line"] >= 0, "Server should start at or near the beginning"
assert server_range["end"]["line"] > server_range["start"]["line"], "Server block should span multiple lines"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_comment_handling(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that comments don't interfere with symbol detection."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# File has comments but symbols should still be detected correctly
expected_sections = {"server", "database", "logging", "endpoints", "metadata", "messages"}
found_sections = expected_sections.intersection(set(symbol_names))
assert len(found_sections) >= 4, f"Should find most sections despite comments, found: {found_sections}"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_special_characters_in_strings(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that strings with escape sequences are handled."""
all_symbols, root_symbols = language_server.request_document_symbols("config.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect the messages section with special strings
assert "messages" in symbol_names, "Should detect 'messages' section"
assert "with_escapes" in symbol_names, "Should detect 'with_escapes' field"
assert "welcome" in symbol_names, "Should detect 'welcome' field"
class TestTomlDependencyTables:
"""Test handling of dependency-style tables common in Cargo.toml and pyproject.toml."""
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_complex_dependency_inline_table(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test detection of complex inline table dependencies like serde = { version = "1.0", features = ["derive"] }."""
all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect serde and tokio dependencies
assert "serde" in symbol_names, "Should detect 'serde' dependency"
assert "tokio" in symbol_names, "Should detect 'tokio' dependency"
# Find serde symbol
serde_symbol = next((s for s in all_symbols if s.get("name") == "serde"), None)
assert serde_symbol is not None
# Dependency with inline table should be kind 19 (object)
assert serde_symbol.get("kind") == 19, "Complex dependency should have kind 19 (object)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_simple_dependency_string(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test detection of simple string dependencies like proptest = "1.0"."""
all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect proptest dev-dependency
assert "proptest" in symbol_names, "Should detect 'proptest' dependency"
# Find proptest symbol
proptest_symbol = next((s for s in all_symbols if s.get("name") == "proptest"), None)
assert proptest_symbol is not None
# Simple string dependency should be kind 15 (string)
assert proptest_symbol.get("kind") == 15, "Simple string dependency should have kind 15 (string)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_pyproject_dependencies_array(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test detection of pyproject.toml dependencies array."""
all_symbols, root_symbols = language_server.request_document_symbols("pyproject.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect dependencies array
assert "dependencies" in symbol_names, "Should detect 'dependencies' array"
# Find dependencies symbol
deps_symbol = next((s for s in all_symbols if s.get("name") == "dependencies"), None)
assert deps_symbol is not None
# Dependencies array should be kind 18 (array)
assert deps_symbol.get("kind") == 18, "Dependencies array should have kind 18 (array)"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_optional_dependencies_table(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test detection of optional-dependencies in pyproject.toml."""
all_symbols, root_symbols = language_server.request_document_symbols("pyproject.toml").get_all_symbols_and_roots()
symbol_names = [sym.get("name") for sym in all_symbols]
# Should detect optional-dependencies or its nested form
has_optional_deps = any("optional" in str(name).lower() for name in symbol_names if name)
has_dev = "dev" in symbol_names
assert has_optional_deps or has_dev, f"Should detect optional-dependencies or dev group, got: {symbol_names}"
@@ -0,0 +1,60 @@
"""
Tests for TOML language server directory ignoring functionality.
These tests validate that the Taplo language server correctly ignores
TOML-specific directories like target, .cargo, and node_modules.
"""
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
pytestmark = pytest.mark.toml
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
class TestTomlIgnoredDirectories:
"""Test TOML-specific directory ignoring behavior."""
def test_default_ignored_directories(self, language_server: SolidLanguageServer) -> None:
"""Test that default TOML directories are ignored."""
# Test that TOML/Rust/Node-specific directories are ignored by default
assert language_server.is_ignored_dirname("target"), "target should be ignored"
assert language_server.is_ignored_dirname(".cargo"), ".cargo should be ignored"
assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored"
# Directories starting with . are ignored by base class
assert language_server.is_ignored_dirname(".git"), ".git should be ignored"
assert language_server.is_ignored_dirname(".venv"), ".venv should be ignored"
def test_important_directories_not_ignored(self, language_server: SolidLanguageServer) -> None:
"""Test that important directories are not ignored."""
# Common project directories should not be ignored
assert not language_server.is_ignored_dirname("src"), "src should not be ignored"
assert not language_server.is_ignored_dirname("crates"), "crates should not be ignored"
assert not language_server.is_ignored_dirname("lib"), "lib should not be ignored"
assert not language_server.is_ignored_dirname("tests"), "tests should not be ignored"
assert not language_server.is_ignored_dirname("config"), "config should not be ignored"
def test_cargo_related_directories(self, language_server: SolidLanguageServer) -> None:
"""Test Cargo/Rust-related directory handling."""
# Rust build directories should be ignored
assert language_server.is_ignored_dirname("target"), "target (Rust build) should be ignored"
assert language_server.is_ignored_dirname(".cargo"), ".cargo should be ignored"
# But important Rust directories should not be ignored
assert not language_server.is_ignored_dirname("benches"), "benches should not be ignored"
assert not language_server.is_ignored_dirname("examples"), "examples should not be ignored"
def test_various_cache_directories(self, language_server: SolidLanguageServer) -> None:
"""Test various cache and temporary directories are ignored."""
# Directories starting with . are ignored by base class
assert language_server.is_ignored_dirname(".cache"), ".cache should be ignored"
# IDE directories (start with .)
assert language_server.is_ignored_dirname(".idea"), ".idea should be ignored"
assert language_server.is_ignored_dirname(".vscode"), ".vscode should be ignored"
# Note: __pycache__ is NOT ignored by TOML server (only Python servers ignore it)
assert not language_server.is_ignored_dirname("__pycache__"), "__pycache__ is not TOML-specific"
@@ -0,0 +1,191 @@
"""
Tests for TOML language server symbol retrieval functionality.
These tests focus on advanced symbol operations:
- request_containing_symbol
- request_document_overview
- request_full_symbol_tree
- request_dir_overview
"""
from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
pytestmark = pytest.mark.toml
class TestTomlSymbolRetrieval:
"""Test advanced symbol retrieval functionality for TOML files."""
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_request_containing_symbol_behavior(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test request_containing_symbol behavior for TOML files.
Note: Taplo LSP doesn't support definition/containing symbol lookups for TOML files
since TOML is a configuration format, not code. This test verifies the behavior.
"""
# Line 2 (0-indexed: 1) is inside the [package] table
containing_symbol = language_server.request_containing_symbol("Cargo.toml", 1, 5)
# Taplo doesn't support containing symbol lookup - returns None
# This is expected behavior for a configuration file format
assert containing_symbol is None, "TOML LSP doesn't support containing symbol lookup"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_request_document_overview_cargo(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test request_document_overview for Cargo.toml."""
overview = language_server.request_document_overview("Cargo.toml")
assert overview is not None
assert len(overview) > 0
# Get symbol names from overview
symbol_names = {symbol.get("name") for symbol in overview if "name" in symbol}
# Verify expected top-level tables appear
expected_tables = {"package", "dependencies", "dev-dependencies", "features", "workspace"}
assert expected_tables.issubset(symbol_names), f"Missing expected tables in overview: {expected_tables - symbol_names}"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_request_document_overview_pyproject(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test request_document_overview for pyproject.toml."""
overview = language_server.request_document_overview("pyproject.toml")
assert overview is not None
assert len(overview) > 0
# Get symbol names from overview
symbol_names = {symbol.get("name") for symbol in overview if "name" in symbol}
# Verify expected top-level tables appear
assert "project" in symbol_names, "Should detect 'project' table"
assert "build-system" in symbol_names, "Should detect 'build-system' table"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_request_full_symbol_tree(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test request_full_symbol_tree returns TOML files."""
symbol_tree = language_server.request_full_symbol_tree()
assert symbol_tree is not None
assert len(symbol_tree) > 0
# The root should be test_repo
root = symbol_tree[0]
assert root["name"] == "test_repo"
assert "children" in root
# Children should include TOML files
child_names = {child["name"] for child in root.get("children", [])}
# Note: File names are stripped of extension in some cases
assert (
"Cargo" in child_names or "Cargo.toml" in child_names or any("cargo" in name.lower() for name in child_names)
), f"Should find Cargo.toml in tree, got: {child_names}"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_request_dir_overview(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test request_dir_overview returns symbols for TOML files."""
overview = language_server.request_dir_overview(".")
assert overview is not None
assert len(overview) > 0
# Should have entries for both Cargo.toml and pyproject.toml
file_paths = list(overview.keys())
assert any("Cargo.toml" in path for path in file_paths), f"Should find Cargo.toml in overview, got: {file_paths}"
assert any("pyproject.toml" in path for path in file_paths), f"Should find pyproject.toml in overview, got: {file_paths}"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_symbol_hierarchy_in_cargo(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that symbol hierarchy is properly preserved in Cargo.toml."""
all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
# Find the 'package' table
package_symbol = next((s for s in root_symbols if s.get("name") == "package"), None)
assert package_symbol is not None, "Should find 'package' as root symbol"
# Verify it has children (nested keys)
assert "children" in package_symbol, "'package' should have children"
child_names = {child.get("name") for child in package_symbol.get("children", [])}
# Package should have name, version, edition at minimum
assert "name" in child_names, "'package' should have 'name' child"
assert "version" in child_names, "'package' should have 'version' child"
assert "edition" in child_names, "'package' should have 'edition' child"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_symbol_hierarchy_in_pyproject(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that symbol hierarchy is properly preserved in pyproject.toml."""
all_symbols, root_symbols = language_server.request_document_symbols("pyproject.toml").get_all_symbols_and_roots()
# Find the 'project' table
project_symbol = next((s for s in root_symbols if s.get("name") == "project"), None)
assert project_symbol is not None, "Should find 'project' as root symbol"
# Verify it has children
assert "children" in project_symbol, "'project' should have children"
child_names = {child.get("name") for child in project_symbol.get("children", [])}
# Project should have name, version, dependencies at minimum
assert "name" in child_names, "'project' should have 'name' child"
assert "version" in child_names, "'project' should have 'version' child"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_tool_section_hierarchy(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that tool sections in pyproject.toml are properly structured."""
all_symbols, root_symbols = language_server.request_document_symbols("pyproject.toml").get_all_symbols_and_roots()
# Get all symbol names
all_names = [s.get("name") for s in all_symbols]
# Should detect tool.ruff, tool.mypy, or tool.pytest
has_ruff = any("ruff" in name.lower() for name in all_names if name)
has_mypy = any("mypy" in name.lower() for name in all_names if name)
has_pytest = any("pytest" in name.lower() for name in all_names if name)
assert has_ruff or has_mypy or has_pytest, f"Should detect tool sections, got names: {all_names}"
@pytest.mark.parametrize("language_server", [Language.TOML], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.TOML], indirect=True)
def test_array_of_tables_symbol(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that [[bin]] array of tables is detected."""
all_symbols, root_symbols = language_server.request_document_symbols("Cargo.toml").get_all_symbols_and_roots()
# Get all symbol names
all_names = [s.get("name") for s in all_symbols]
# Should detect bin array of tables
has_bin = "bin" in all_names
assert has_bin, f"Should detect [[bin]] array of tables, got names: {all_names}"
# Find the bin symbol and verify its structure
bin_symbol = next((s for s in all_symbols if s.get("name") == "bin"), None)
assert bin_symbol is not None, "Should find bin symbol"
# Array of tables should be kind 18 (array)
assert bin_symbol.get("kind") == 18, "[[bin]] should have kind 18 (array)"
# Children of array of tables are indexed by position ('0', '1', etc.)
if "children" in bin_symbol:
bin_children = bin_symbol.get("children", [])
assert len(bin_children) > 0, "[[bin]] should have at least one child element"
# First child is index '0'
first_child = bin_children[0]
assert first_child.get("name") == "0", f"First array element should be named '0', got: {first_child.get('name')}"
# The '0' element should contain name and path as grandchildren
if "children" in first_child:
grandchild_names = {gc.get("name") for gc in first_child.get("children", [])}
assert "name" in grandchild_names, f"[[bin]] element should have 'name' field, got: {grandchild_names}"
assert "path" in grandchild_names, f"[[bin]] element should have 'path' field, got: {grandchild_names}"