mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 00:16:57 +00:00
Merge pull request #797 from w0wl0lxd/feature/toml-support
feat: Add TOML language support via Taplo
This commit is contained in:
13 files changed
+1241
-2
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
@@ -0,0 +1 @@
|
||||
"""TOML language server tests."""
|
||||
@@ -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)"
|
||||
@@ -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}"
|
||||
Reference in new issue
Block a user