feat: Add TOML language support via Taplo

Add TOML language server using Taplo with automatic binary download
for .toml file validation, formatting, and schema support.

Security:
- SHA256 checksum verification for downloaded binaries
- Zip slip vulnerability protection for archive extraction
- Download timeout (120s) to prevent indefinite hangs

Fixes #796: serena project index fails on Rust projects when
project.yml contains languages: ["toml"]. Without TOML in the
Language enum, loading such configurations raises ValueError:
'toml' is not a valid Language. This commonly occurs in Rust
projects where users want to index Cargo.toml alongside .rs files.
This commit is contained in:
w0wl0lxd committed 2025-12-01 02:49:52 -06:00
1 parent 855818da63
commit c01d9d06e8
7 files changed
+654 -1

No files matched your search

+1
View File
@@ -46,6 +46,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
View File
@@ -299,6 +299,7 @@ markers = [
"fortran: language server running for Fortran",
"haskell: Haskell language server tests",
"yaml: language server running for YAML",
"toml: language server running for TOML",
]
[tool.codespell]
@@ -0,0 +1,330 @@
"""
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."""
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},
"formatting": {"dynamicRegistration": True},
"semanticTokens": {"dynamicRegistration": True},
},
"workspace": {
"workspaceFolders": True,
"didChangeConfiguration": {"dynamicRegistration": True},
"symbol": {"dynamicRegistration": True},
"configuration": True,
},
},
"processId": os.getpid(),
"rootPath": repository_absolute_path,
"rootUri": root_uri,
"workspaceFolders": [
{
"uri": root_uri,
"name": os.path.basename(repository_absolute_path),
}
],
"initializationOptions": {
"configuration": {
"evenBetterToml": {
"schema": {
"enabled": True,
"repositoryEnabled": True,
"repositoryUrl": "https://taplo.tamasfe.dev/schema_index.json",
},
"formatter": {"alignEntries": False, "alignComments": True, "arrayTrailingComma": True},
}
}
},
}
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"]
+18 -1
View File
@@ -82,6 +82,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]:
@@ -93,7 +97,14 @@ class Language(str, Enum):
"""
Check if the language server is experimental or deprecated.
"""
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,
}
def __str__(self) -> str:
return self.value
@@ -148,6 +159,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:
@@ -269,6 +282,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,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"
+231
View File
@@ -0,0 +1,231 @@
"""
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 - should be kind 2 (module/namespace)
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") == 2, "Top-level table should have kind 2 (module/namespace)"
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") == 2, "'dependencies' table should have kind 2 (module/namespace)"
@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
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") == 2, "'project' table should have kind 2 (module/namespace)"
@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
assert "body" in package_symbol, "'package' symbol should have body"
package_body = package_symbol["body"]
assert "[package]" in package_body, "Body should contain '[package]'"
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 "[dependencies]" in deps_body, "Body should contain '[dependencies]'"
assert "serde" in deps_body, "Body should contain serde dependency"
assert "tokio" in deps_body, "Body should contain tokio dependency"
# Find the features symbol and check its body
features_symbol = next((s for s in all_symbols if s.get("name") == "features"), None)
assert features_symbol is not None, "Should find 'features' symbol"
assert "body" in features_symbol, "'features' symbol should have body"
features_body = features_symbol["body"]
assert "[features]" in features_body, "Body should contain '[features]'"
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)"