Refactoring: Rename Language to LanguageServerId

This resolves a long-standing naming issue in Serena & SolidLSP.
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-07-22 20:55:36 +02:00
1 parent ef0e218fb1
commit d2cf18ddcb
180 files changed
+1806 -1767

No files matched your search

@@ -111,12 +111,12 @@ For an example, see `EclipseJDTLS._start_server`.
## Step 2: Language Registration
### 2.1 Add to Language Enum
### 2.1 Add to LanguageServerId Enum
In `src/solidlsp/ls_config.py`, add your language to the `Language` enum:
In `src/solidlsp/ls_config.py`, add your language to the enum:
```python
class Language(str, Enum):
class LanguageServerId(str, Enum):
# Existing languages...
NEW_LANGUAGE = "new_language"
@@ -125,20 +125,15 @@ class Language(str, Enum):
# Existing cases...
case self.NEW_LANGUAGE:
return FilenameMatcher(".newlang", ".nl") # File extensions
```
### 2.2 Update Language Server Factory
In `src/solidlsp/ls.py`, add your language to the `create` method:
```python
@classmethod
def create(cls, config: LanguageServerConfig, repository_root_path: str) -> "SolidLanguageServer":
match config.code_language:
# Existing cases...
case Language.NEW_LANGUAGE:
from solidlsp.language_servers.new_language_server import NewLanguageServer
return NewLanguageServer(config, repository_root_path)
...
def get_ls_class(self) -> type["SolidLanguageServer"]:
match self:
# Existing cases...
case self.NEW_LANGUAGE:
from solidlsp.language_servers.new_language_server import NewLanguageServer
return NewLanguageServer
```
## Step 3: Test Repository
@@ -214,10 +209,6 @@ You should at least test:
Have a look at `test/solidlsp/php/test_php_basic.py` as an example for what should be tested.
Don't forget to add a new language marker to `pytest.ini`.
### 4.2 Integration Tests
Consider adding new cases to the parametrized tests in `test_serena_agent.py` for the new language.
### 5 Documentation
+2 -2
View File
@@ -23,7 +23,7 @@ from serena.tools import (
GetDiagnosticsForSymbolTool,
ReplaceContentTool,
)
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
SEPARATOR = "=" * 80
REPO_PATH = Path(REPO_ROOT)
@@ -40,7 +40,7 @@ def make_agent() -> SerenaAgent:
project_root=str(REPO_PATH),
project_config=ProjectConfig(
project_name="demo_serena_repo",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
ignored_paths=[],
excluded_tools=[],
read_only=False,
+3 -3
View File
@@ -12,14 +12,14 @@ from serena.config.serena_config import LanguageBackend, ProjectConfig, Register
from serena.constants import REPO_ROOT
from serena.project import Project
from serena.tools import FindDeclarationTool
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
SEPARATOR = "=" * 80
PYTHON_TEST_REPO = Path(REPO_ROOT) / "test" / "resources" / "repos" / "python" / "test_repo"
SERVICES_FILE = Path("test_repo") / "services.py"
def make_agent(project_root: Path, language: Language, project_name: str) -> SerenaAgent:
def make_agent(project_root: Path, language: LanguageServerId, project_name: str) -> SerenaAgent:
"""Create an LSP-backed Serena agent for a single explicit project."""
serena_config = SerenaConfig.from_config_file()
serena_config.web_dashboard = False
@@ -64,7 +64,7 @@ def find_identifier_occurrence_position(file_path: Path, identifier: str, occurr
if __name__ == "__main__":
agent = make_agent(PYTHON_TEST_REPO, Language.PYTHON, "demo_python_test_repo")
agent = make_agent(PYTHON_TEST_REPO, LanguageServerId.PYTHON, "demo_python_test_repo")
try:
# letting the language server finish startup
+3 -3
View File
@@ -11,13 +11,13 @@ from serena.config.serena_config import LanguageBackend, ProjectConfig, Register
from serena.constants import REPO_ROOT
from serena.project import Project
from serena.tools import FindImplementationsTool
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
SEPARATOR = "=" * 80
GO_TEST_REPO = Path(REPO_ROOT) / "test" / "resources" / "repos" / "go" / "test_repo"
def make_agent(project_root: Path, language: Language, project_name: str) -> SerenaAgent:
def make_agent(project_root: Path, language: LanguageServerId, project_name: str) -> SerenaAgent:
"""Create an LSP-backed Serena agent for a single explicit project."""
serena_config = SerenaConfig.from_config_file()
serena_config.web_dashboard = False
@@ -49,7 +49,7 @@ def print_section(title: str) -> None:
if __name__ == "__main__":
agent = make_agent(GO_TEST_REPO, Language.GO, "demo_go_test_repo")
agent = make_agent(GO_TEST_REPO, LanguageServerId.GO, "demo_go_test_repo")
try:
# letting the language server finish startup
+2 -2
View File
@@ -2,10 +2,10 @@
Prints the list of supported languages, for use in the project.yml template
"""
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
if __name__ == "__main__":
lang_strings = sorted([l.value for l in Language])
lang_strings = sorted([l.value for l in LanguageServerId])
max_len = max(len(s) for s in lang_strings)
fmt = f"%-{max_len + 2}s"
for i, l in enumerate(lang_strings):
+11 -11
View File
@@ -61,7 +61,7 @@ from serena.tools import (
from serena.util.gui import system_has_usable_display
from serena.util.inspection import iter_subclasses
from serena.util.logging import MemoryLogHandler
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.util import subprocess_util
from solidlsp.util.subprocess_util import terminate_process_tree_with_kill_fallback
@@ -1385,24 +1385,24 @@ class SerenaAgent:
"""
self.get_active_project_or_raise().create_language_server_manager()
def add_language(self, language: Language) -> None:
def add_language_server(self, ls_id: LanguageServerId) -> None:
"""
Adds a new language to the active project, spawning the respective language server and updating the project configuration.
Adds a new language server to the active project, spawning the respective language server and updating the project configuration.
The addition is scheduled via the agent's task executor and executed synchronously, i.e. the method returns
when the addition is complete.
:param language: the language to add
:param ls_id: the language server to add
"""
self.execute_task(lambda: self.get_active_project_or_raise().add_language(language), name=f"AddLanguage:{language.value}")
self.execute_task(lambda: self.get_active_project_or_raise().add_language_server(ls_id), name=f"AddLanguage:{ls_id.value}")
def remove_language(self, language: Language) -> None:
def remove_language_server(self, ls_id: LanguageServerId) -> None:
"""
Removes a language from the active project, shutting down the respective language server and updating the project configuration.
Removes a language server from the active project, shutting down the respective server and updating the project configuration.
The removal is scheduled via the agent's task executor and executed asynchronously.
:param language: the language to remove
:param ls_id: the language to remove
"""
self.issue_task(lambda: self.get_active_project_or_raise().remove_language(language), name=f"RemoveLanguage:{language.value}")
self.issue_task(lambda: self.get_active_project_or_raise().remove_language_server(ls_id), name=f"RemoveLanguage:{ls_id.value}")
def get_tool(self, tool_class: type[TTool]) -> TTool:
return self._all_tools[tool_class]
@@ -1444,11 +1444,11 @@ class SerenaAgent:
tool_class = ToolRegistry().get_tool_class_by_name(tool_name)
return self.get_tool(tool_class)
def get_active_lsp_languages(self) -> list[Language]:
def get_active_language_server_ids(self) -> list[LanguageServerId]:
ls_manager = self.get_language_server_manager()
if ls_manager is None:
return []
return ls_manager.get_active_languages()
return ls_manager.get_active_language_server_ids()
@contextmanager
def active_project_context(self, project: Project) -> Iterator[None]:
+7 -7
View File
@@ -39,7 +39,7 @@ from serena.constants import (
from serena.prompt_factory import SerenaPromptFactory
from serena.util.cli_util import AutoRegisteringGroup
from serena.util.logging import MemoryLogHandler
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from solidlsp.util.subprocess_util import subprocess_kwargs
@@ -701,13 +701,13 @@ class ProjectCommands(AutoRegisteringGroup):
if os.path.exists(yml_path):
raise FileExistsError(f"Project file {yml_path} already exists.")
languages: list[Language] = []
languages: list[LanguageServerId] = []
if language:
for lang in language:
try:
languages.append(Language(lang.lower()))
languages.append(LanguageServerId(lang.lower()))
except ValueError:
all_langs = [l.value for l in Language]
all_langs = [l.value for l in LanguageServerId]
raise ValueError(f"Unknown language '{lang}'. Supported: {all_langs}")
generated_conf = ProjectConfig.autogenerate(
@@ -801,13 +801,13 @@ class ProjectCommands(AutoRegisteringGroup):
collected_exceptions: list[Exception] = []
files_failed = []
language_file_counts: dict[Language, int] = collections.defaultdict(lambda: 0)
language_file_counts: dict[LanguageServerId, int] = collections.defaultdict(lambda: 0)
last_save_time = time.monotonic()
for i, f in enumerate(tqdm(files, desc="Indexing")):
try:
ls = ls_mgr.get_language_server(f)
ls.request_document_symbols(f)
language_file_counts[ls.language] += 1
language_file_counts[ls.ls_id] += 1
except Exception as e:
log.error(f"Failed to index {f}, continuing.")
collected_exceptions.append(e)
@@ -882,7 +882,7 @@ class ProjectCommands(AutoRegisteringGroup):
ls_mgr = proj.create_language_server_manager()
try:
for ls in ls_mgr.iter_language_servers():
click.echo(f"Indexing for language {ls.language.value} …")
click.echo(f"Indexing for language {ls.ls_id.value} …")
document_symbols = ls.request_document_symbols(file)
symbols, _ = document_symbols.get_all_symbols_and_roots()
if verbose:
+41 -40
View File
@@ -32,10 +32,10 @@ from serena.constants import (
SERENA_FILE_ENCODING,
SERENA_MANAGED_DIR_NAME,
)
from serena.util.inspection import determine_programming_language_composition
from serena.util.inspection import compute_language_server_support_composition
from serena.util.text_utils import glob_match
from serena.util.yaml import YamlCommentNormalisation, load_yaml, normalise_yaml_comments, save_yaml, transfer_yaml_comments
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from ..analytics import RegisteredTokenCountEstimator
from ..util.class_decorators import singleton
@@ -321,7 +321,7 @@ class ProjectConfigAutoGenerationMode(Enum):
@dataclass(kw_only=True)
class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
project_name: str
languages: list[Language]
languages: list[LanguageServerId]
ignored_paths: list[str] = field(default_factory=list)
ls_workspace_folders: list[str] = field(default_factory=lambda: ["."])
ls_additional_workspace_folders: list[str] = field(default_factory=list)
@@ -356,57 +356,58 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
return ["project_name"]
@classmethod
def _determine_project_languages(cls, project_root: str, interactive: bool, serena_config: "SerenaConfig") -> list[Language]:
log.info("Determining programming languages used in the project")
def _determine_project_language_servers(
cls, project_root: str, interactive: bool, serena_config: "SerenaConfig"
) -> list[LanguageServerId]:
log.info("Determining suitable language servers for the project")
# determine languages to be considered and their priorities
language_priorities = {}
for language in Language:
# determine language servers to be considered and their priorities
ls_priorities = {}
for language in LanguageServerId:
priority = serena_config.get_ls_priority(language)
if priority > 0:
language_priorities[language] = priority
ls_priorities[language] = priority
log.debug("Language priorities: %s", language_priorities)
language_composition = determine_programming_language_composition(project_root, list(language_priorities.keys()))
log.info("Project language composition: %s", language_composition)
log.debug("Language server priorities: %s", ls_priorities)
ls_composition = compute_language_server_support_composition(project_root, list(ls_priorities.keys()))
log.info("Project composition: %s", ls_composition)
if len(language_composition) == 0:
if len(ls_composition) == 0:
log.warning(
"No source files for supported language servers were found in %s. "
"Creating project with no configured languages. "
"Creating project with no configured language servers. "
"Symbol-related tools (e.g. find_symbol, get_symbols_overview) will not work "
"when using the LSP backend. You can add languages later via the Serena dashboard "
"or by manually editing the project configuration.",
project_root,
)
languages_to_use = []
language_servers_to_use = []
else:
# sort languages by number of files found
languages_and_percentages = sorted(
language_composition.items(), key=lambda item: (item[1], language_priorities[item[0]]), reverse=True
)
languages_and_percentages = sorted(ls_composition.items(), key=lambda item: (item[1], ls_priorities[item[0]]), reverse=True)
# find the language with the highest percentage and enable it
top_language_pair = languages_and_percentages[0]
other_language_pairs = languages_and_percentages[1:]
languages_to_use = [top_language_pair[0]]
language_servers_to_use = [top_language_pair[0]]
# if in interactive mode, ask the user which other languages to enable
if len(other_language_pairs) > 0 and interactive:
print(
"Detected and enabled main language '%s' (%.2f%% of source files)." % (top_language_pair[0].value, top_language_pair[1])
"Detected and enabled main language server '%s' (%.2f%% of source files)."
% (top_language_pair[0].value, top_language_pair[1])
)
print(f"Additionally detected {len(other_language_pairs)} other language(s).\n")
print("Note: Enable only languages you need symbolic retrieval/editing capabilities for.")
print(" Additional language servers use resources and some languages may require additional")
print(f"Additionally detected {len(other_language_pairs)} other applicable language servers.\n")
print("Note: Enable only servers for languages you need symbolic retrieval/editing capabilities for.")
print(" Additional language servers use resources and some may require additional")
print(" system-level installations/configuration (see Serena documentation).")
print("\nWhich additional languages do you want to enable?")
for lang, perc in other_language_pairs:
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (lang.value, perc), default=False)
print("\nWhich additional language servers do you want to enable?")
for ls_id, perc in other_language_pairs:
enable = ask_yes_no("Enable %s (%.2f%% of source files)?" % (ls_id.value, perc), default=False)
if enable:
languages_to_use.append(lang)
language_servers_to_use.append(ls_id)
print()
log.info("Using languages: %s", languages_to_use)
return languages_to_use
log.info("Using language servers: %s", language_servers_to_use)
return language_servers_to_use
@classmethod
def autogenerate(
@@ -414,7 +415,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
project_root: str | Path,
serena_config: "SerenaConfig",
project_name: str | None = None,
languages: list[Language] | None = None,
languages: list[LanguageServerId] | None = None,
save_to_disk: bool = True,
interactive: bool = False,
asynchronous: bool = False,
@@ -448,7 +449,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
use_asynchronous_language_determination = True
languages_to_use = [] # temporarily empty, will be determined in background thread
else:
determined_languages = cls._determine_project_languages(
determined_languages = cls._determine_project_language_servers(
str(project_root), interactive=interactive, serena_config=serena_config
)
languages_to_use = [l.value for l in determined_languages]
@@ -477,7 +478,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
def async_language_determination():
try:
with LogTime("Asynchronous language determination", logger=log):
project_config.languages = cls._determine_project_languages(
project_config.languages = cls._determine_project_language_servers(
str(project_root), interactive=False, serena_config=serena_config
)
if save_to_disk:
@@ -579,18 +580,18 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
"""
# map languages to list of enum items, checking for errors
lang_name_mapping = {"javascript": "typescript"}
languages: list[Language] = []
languages: list[LanguageServerId] = []
for language_str in data["languages"]:
orig_language_str = language_str
try:
language_str = language_str.lower()
if language_str in lang_name_mapping:
language_str = lang_name_mapping[language_str]
language = Language(language_str)
language = LanguageServerId(language_str)
languages.append(language)
except ValueError as e:
raise ValueError(
f"Invalid language: {orig_language_str}.\nValid language_strings are: {[l.value for l in Language]}"
f"Invalid language: {orig_language_str}.\nValid language_strings are: {[l.value for l in LanguageServerId]}"
) from e
# Validate activation_command_timeout
@@ -1451,18 +1452,18 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
log.info(f"Using language backend from global configuration: {language_backend.name}")
return language_backend
def get_ls_priority(self, language: Language) -> int:
def get_ls_priority(self, ls_id: LanguageServerId) -> int:
"""
Gets the priority value associated with a language server
:param language: identifies the language server
:param ls_id: identifies the language server
:return: the integer priority
"""
if self.ls_priorities is not None:
try:
configured_value = self.ls_priorities.get(language.value)
configured_value = self.ls_priorities.get(ls_id.value)
if configured_value is not None:
return int(configured_value)
except Exception as e:
log.error("Error reading language priority for %s: %s. Using default priority.", language.value, e)
return language.get_priority()
log.error("Error reading language priority for %s: %s. Using default priority.", ls_id.value, e)
return ls_id.get_priority()
+10 -10
View File
@@ -638,10 +638,10 @@ class SerenaDashboardAPI:
self._current_config_overview = self._compute_config_overview().model_dump()
def _get_available_languages(self) -> ResponseAvailableLanguages:
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
def run() -> ResponseAvailableLanguages:
all_languages = [lang.value for lang in Language.iter_all(include_experimental=True)]
all_languages = [lang.value for lang in LanguageServerId.iter_all(include_experimental=True)]
# Filter out already added languages for the active project
project = self._agent.get_active_project()
@@ -776,24 +776,24 @@ class SerenaDashboardAPI:
return {}
def _add_language(self, request_add_language: RequestAddLanguage) -> None:
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
try:
language = Language(request_add_language.language)
language = LanguageServerId(request_add_language.language)
except ValueError:
raise ValueError(f"Invalid language: {request_add_language.language}")
raise ValueError(f"Invalid language server identifier: {request_add_language.language}")
# add_language is already thread-safe
self._agent.add_language(language)
self._agent.add_language_server(language)
def _remove_language(self, request_remove_language: RequestRemoveLanguage) -> None:
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
try:
language = Language(request_remove_language.language)
language = LanguageServerId(request_remove_language.language)
except ValueError:
raise ValueError(f"Invalid language: {request_remove_language.language}")
raise ValueError(f"Invalid language server identifier: {request_remove_language.language}")
# remove_language is already thread-safe
self._agent.remove_language(language)
self._agent.remove_language_server(language)
@staticmethod
def _find_first_free_port(start_port: int, host: str) -> int:
+35 -33
View File
@@ -9,7 +9,7 @@ from sensai.util.logging import LogTime
from serena.config.serena_config import ProjectConfig, SerenaPaths
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.lsp_protocol_handler.lsp_types import DidChangeWatchedFilesParams, FileChangeType, FileEvent
from solidlsp.settings import SolidLSPSettings
@@ -45,17 +45,17 @@ class LanguageServerFactory:
self.ls_specific_settings = ls_specific_settings
self.trace_lsp_communication = trace_lsp_communication
def create_language_server(self, language: Language) -> SolidLanguageServer:
def create_language_server(self, ls_id: LanguageServerId) -> SolidLanguageServer:
ls_config = LanguageServerConfig(
workspace_folders=self.project_config.ls_workspace_folders,
additional_workspace_folders=self.project_config.ls_additional_workspace_folders,
code_language=language,
ls_id=ls_id,
ignored_paths=self.ignored_patterns,
trace_lsp_communication=self.trace_lsp_communication,
encoding=self.encoding,
)
log.info(f"Creating language server instance for {self.project_root}, language={language}.")
log.info(f"Creating language server instance for {self.project_root}, language={ls_id}.")
return SolidLanguageServer.create(
ls_config,
self.project_root,
@@ -74,7 +74,10 @@ class LanguageServerManager:
"""
def __init__(
self, language_servers: dict[Language, SolidLanguageServer], language_server_factory: LanguageServerFactory, project: "Project"
self,
language_servers: dict[LanguageServerId, SolidLanguageServer],
language_server_factory: LanguageServerFactory,
project: "Project",
) -> None:
"""
:param language_servers: a mapping from language to language server; the servers are assumed to be already started.
@@ -94,7 +97,7 @@ class LanguageServerManager:
return next(iter(self._language_servers.values()))
@staticmethod
def from_languages(languages: list[Language], factory: LanguageServerFactory, project: "Project") -> "LanguageServerManager":
def from_languages(languages: list[LanguageServerId], factory: LanguageServerFactory, project: "Project") -> "LanguageServerManager":
"""
Creates a manager with language servers for the given languages using the given factory.
The language servers are started in parallel threads.
@@ -106,21 +109,21 @@ class LanguageServerManager:
"""
class StartLSThread(threading.Thread):
def __init__(self, language: Language):
super().__init__(target=self._start_language_server, name="StartLS:" + language.value)
self.language = language
def __init__(self, ls_id: LanguageServerId):
super().__init__(target=self._start_language_server, name="StartLS:" + ls_id.value)
self.ls_id = ls_id
self.language_server: SolidLanguageServer | None = None
self.exception: Exception | None = None
def _start_language_server(self) -> None:
try:
with LogTime(f"Language server startup (language={self.language.value})"):
self.language_server = factory.create_language_server(self.language)
with LogTime(f"Language server startup (language={self.ls_id.value})"):
self.language_server = factory.create_language_server(self.ls_id)
self.language_server.start()
if not self.language_server.is_running():
raise RuntimeError(f"Failed to start the language server for language {self.language.value}")
raise RuntimeError(f"Failed to start the language server for language {self.ls_id.value}")
except Exception as e:
log.error(f"Error starting language server for language {self.language.value}: {e}", exc_info=e)
log.error(f"Error starting language server for language {self.ls_id.value}: {e}", exc_info=e)
self.exception = e
# start language servers in parallel threads
@@ -131,14 +134,14 @@ class LanguageServerManager:
threads.append(thread)
# collect language servers and exceptions
language_servers: dict[Language, SolidLanguageServer] = {}
exceptions: dict[Language, Exception] = {}
language_servers: dict[LanguageServerId, SolidLanguageServer] = {}
exceptions: dict[LanguageServerId, Exception] = {}
for thread in threads:
thread.join()
if thread.exception is not None:
exceptions[thread.language] = thread.exception
exceptions[thread.ls_id] = thread.exception
elif thread.language_server is not None:
language_servers[thread.language] = thread.language_server
language_servers[thread.ls_id] = thread.language_server
# If any server failed to start up, raise an exception and stop all started language servers.
# We intentionally fail fast here. The user's intention is to work with all the specified languages,
@@ -155,8 +158,8 @@ class LanguageServerManager:
def _ensure_functional_ls(self, ls: SolidLanguageServer) -> SolidLanguageServer:
if not ls.is_running():
log.warning(f"Language server for language {ls.language} is not running; restarting ...")
ls = self.restart_language_server(ls.language)
log.warning(f"Language server for language {ls.ls_id} is not running; restarting ...")
ls = self.restart_language_server(ls.ls_id)
return ls
def _get_suitable_language_server(self, relative_path: str) -> SolidLanguageServer | None:
@@ -177,15 +180,15 @@ class LanguageServerManager:
ls = self._default_language_server
return self._ensure_functional_ls(ls)
def _create_and_start_language_server(self, language: Language) -> SolidLanguageServer:
def _create_and_start_language_server(self, ls_id: LanguageServerId) -> SolidLanguageServer:
if self._language_server_factory is None:
raise ValueError(f"No language server factory available to create language server for {language}")
language_server = self._language_server_factory.create_language_server(language)
raise ValueError(f"No language server factory available to create language server for {ls_id}")
language_server = self._language_server_factory.create_language_server(ls_id)
language_server.start()
self._language_servers[language] = language_server
self._language_servers[ls_id] = language_server
return language_server
def restart_language_server(self, language: Language) -> SolidLanguageServer:
def restart_language_server(self, language: LanguageServerId) -> SolidLanguageServer:
"""
Forces recreation and restart of the language server for the given language.
It is assumed that the language server for the given language is no longer running.
@@ -197,19 +200,18 @@ class LanguageServerManager:
raise ValueError(f"No language server for language {language.value} present; cannot restart")
return self._create_and_start_language_server(language)
def add_language_server(self, language: Language) -> SolidLanguageServer:
def add_language_server(self, ls_id: LanguageServerId) -> SolidLanguageServer:
"""
Dynamically adds a new language server for the given language.
:param language: the language
:param factory: the factory to create the language server
:param ls_id: the language server to add
:return: the newly created language server
"""
if language in self._language_servers:
raise ValueError(f"Language server for language {language.value} already present")
return self._create_and_start_language_server(language)
if ls_id in self._language_servers:
raise ValueError(f"Language server for language {ls_id.value} already present")
return self._create_and_start_language_server(ls_id)
def remove_language_server(self, language: Language, save_cache: bool = False) -> None:
def remove_language_server(self, language: LanguageServerId, save_cache: bool = False) -> None:
"""
Removes the language server for the given language, stopping it if it is running.
@@ -220,7 +222,7 @@ class LanguageServerManager:
ls = self._language_servers.pop(language)
self._stop_language_server(ls, save_cache=save_cache)
def get_active_languages(self) -> list[Language]:
def get_active_language_server_ids(self) -> list[LanguageServerId]:
"""
Returns the list of languages for which language servers are currently managed.
@@ -233,7 +235,7 @@ class LanguageServerManager:
if ls.is_running():
if save_cache:
ls.save_cache()
log.info(f"Stopping language server for language {ls.language} ...")
log.info(f"Stopping language server for language {ls.ls_id} ...")
ls.stop(shutdown_timeout=timeout)
def iter_language_servers(self) -> Iterator[SolidLanguageServer]:
+20 -20
View File
@@ -20,7 +20,7 @@ from serena.util.file_proxy import FileCollection, FileProxy
from serena.util.file_system import GitignoreParser, match_path, scan_directory
from serena.util.text_utils import MatchedConsecutiveLines, search_files
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
if TYPE_CHECKING:
from serena.agent import SerenaAgent
@@ -493,7 +493,7 @@ class Project(ToStringMixin):
ls_specific_settings=ls_specific_settings,
trace_lsp_communication=self.serena_config.trace_lsp_communication,
)
self.language_server_manager = LanguageServerManager.from_languages(self.project_config.languages, factory, self)
self.language_server_manager = LanguageServerManager.from_languages(self.project_config.language_servers, factory, self)
return self.language_server_manager
except Exception as e:
self._language_server_manager_init_error = e
@@ -512,50 +512,50 @@ class Project(ToStringMixin):
raise Exception(msg.build())
return self.language_server_manager
def add_language(self, language: Language) -> None:
def add_language_server(self, ls_id: LanguageServerId) -> None:
"""
Adds a new programming language to the project configuration, starting the corresponding
language server instance if the LS manager is active.
Adds a new language server to the project configuration, starting the corresponding
server instance if the LS manager is active.
The project configuration is saved to disk after adding the language.
:param language: the programming language to add
:param ls_id: the language server to add
"""
if language in self.project_config.languages:
log.info(f"Language {language.value} is already present in the project configuration.")
if ls_id in self.project_config.language_servers:
log.info(f"Language server {ls_id.value} is already present in the project configuration.")
return
# start the language server (if the LS manager is active)
if self.language_server_manager is None:
log.info("Language server manager is not active; skipping language server startup for the new language.")
else:
log.info("Adding and starting the language server for new language %s ...", language.value)
self.language_server_manager.add_language_server(language)
log.info("Adding and starting the language server '%s' ...", ls_id.value)
self.language_server_manager.add_language_server(ls_id)
# update the project configuration
self.project_config.languages.append(language)
self.project_config.language_servers.append(ls_id)
self.save_config()
def remove_language(self, language: Language) -> None:
def remove_language_server(self, ls_id: LanguageServerId) -> None:
"""
Removes a programming language from the project configuration, stopping the corresponding
language server instance if the LS manager is active.
Removes a language server from the project configuration, stopping the corresponding
server instance if the LS manager is active.
The project configuration is saved to disk after removing the language.
:param language: the programming language to remove
:param ls_id: the language server to remove
"""
if language not in self.project_config.languages:
log.info(f"Language {language.value} is not present in the project configuration.")
if ls_id not in self.project_config.language_servers:
log.info(f"Language {ls_id.value} is not present in the project configuration.")
return
# update the project configuration
self.project_config.languages.remove(language)
self.project_config.language_servers.remove(ls_id)
self.save_config()
# stop the language server (if the LS manager is active)
if self.language_server_manager is None:
log.info("Language server manager is not active; skipping language server shutdown for the removed language.")
else:
log.info("Removing and stopping the language server for language %s ...", language.value)
self.language_server_manager.remove_language_server(language)
log.info("Removing and stopping the language server for language %s ...", ls_id.value)
self.language_server_manager.remove_language_server(ls_id)
def ls_sync_file_system_changes(self) -> int:
"""
+1 -1
View File
@@ -107,7 +107,7 @@ class GetSymbolsOverviewTool(Tool, ToolMarkerSymbolicRead):
raise ValueError(f"Expected a file path, but got a directory path: {relative_path}. ")
if not symbol_retriever.can_analyze_file(relative_path):
raise ValueError(
f"Cannot extract symbols from file {relative_path}. Active languages: {[l.value for l in self.agent.get_active_lsp_languages()]}"
f"Cannot extract symbols from file {relative_path}. Active language servers: {[l.value for l in self.agent.get_active_language_server_ids()]}"
)
symbols = symbol_retriever.get_symbol_overview(relative_path)[relative_path]
+16 -14
View File
@@ -4,7 +4,7 @@ from collections.abc import Callable, Iterator
from typing import TypeVar
from serena.util.file_system import find_all_non_ignored_files
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
T = TypeVar("T")
@@ -27,40 +27,42 @@ def iter_subclasses(
yield from iter_subclasses(subclass, recursive, inclusion_predicate)
def determine_programming_language_composition(repo_path: str, languages: list[Language] | None = None) -> dict[Language, float]:
def compute_language_server_support_composition(
repo_path: str, ls_ids: list[LanguageServerId] | None = None
) -> dict[LanguageServerId, float]:
"""
Determine the programming language composition of a repository.
Determine the composition of a repository in terms of the language servers that can be used to analyze it.
Percentages are computed relative to the number of files that match at least
one supported language, not the total file count. This prevents files that
one supported language server, not the total file count. This prevents files that
belong to no supported language (images, plain text, licenses, lock files, etc.)
from diluting language percentages in repositories where such files dominate.
:param repo_path: path to the repository to analyze
:param languages: the list of languages to consider; if None, use default languages
:return: dictionary mapping languages to percentages of recognised source files
matching the respective language (denominator = files matched by at least one language)
:param ls_ids: the list of language servers to consider; if None, use default (non-experimental ones)
:return: dictionary mapping language servers to percentages of recognised source files
(denominator = files matched by at least one language server)
"""
if languages is None:
languages = list(Language.iter_all(include_experimental=False))
if ls_ids is None:
ls_ids = list(LanguageServerId.iter_all(include_experimental=False))
all_files = find_all_non_ignored_files(repo_path)
if not all_files:
return {}
matchers = {lang: lang.get_source_fn_matcher() for lang in languages}
matchers = {lang: lang.get_source_fn_matcher() for lang in ls_ids}
# count files per language in a single pass over the files
language_counts: dict[Language, int] = {}
ls_file_counts: dict[LanguageServerId, int] = {}
recognised_files = 0
for file_path in all_files:
# Use just the filename for matching, not the full path
filename = os.path.basename(file_path)
matched_any = False
for lang, matcher in matchers.items():
for ls_id, matcher in matchers.items():
if matcher.is_relevant_filename(filename):
language_counts[lang] = language_counts.get(lang, 0) + 1
ls_file_counts[ls_id] = ls_file_counts.get(ls_id, 0) + 1
matched_any = True
if matched_any:
recognised_files += 1
@@ -69,4 +71,4 @@ def determine_programming_language_composition(repo_path: str, languages: list[L
return {}
# convert to percentages relative to recognised source files only
return {lang: round(count / recognised_files * 100, 2) for lang, count in language_counts.items()}
return {ls_id: round(count / recognised_files * 100, 2) for ls_id, count in ls_file_counts.items()}
@@ -18,7 +18,7 @@ from overrides import override
from solidlsp import ls_types
from solidlsp.language_servers.common import quote_windows_path
from solidlsp.ls import DocumentSymbols, LSPFileBuffer, RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
@@ -240,7 +240,7 @@ class ALLanguageServer(SolidLanguageServer):
log.warning(f"AL_EXTENSION_PATH set but directory not found: {env_path}")
# Check the resolved-version download location (versioned for non-INITIAL, legacy "al-extension" for INITIAL)
al_settings = solidlsp_settings.get_ls_specific_settings(Language.AL)
al_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.AL)
al_extension_version = al_settings.get("al_extension_version", DEFAULT_AL_EXTENSION_VERSION)
default_path = os.path.join(cls.ls_resources_dir(solidlsp_settings), _al_extension_dirname(al_extension_version), "extension")
if os.path.exists(default_path):
@@ -265,7 +265,7 @@ class ALLanguageServer(SolidLanguageServer):
Path to installed extension or None if download failed
"""
al_settings = solidlsp_settings.get_ls_specific_settings(Language.AL)
al_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.AL)
al_extension_version = al_settings.get("al_extension_version", DEFAULT_AL_EXTENSION_VERSION)
al_extension_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), _al_extension_dirname(al_extension_version))
al_extension_url = (
@@ -69,7 +69,7 @@ from solidlsp.language_servers.typescript_language_server import (
)
from solidlsp.language_servers.vscode_html_language_server import VsCodeHtmlLanguageServer
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_config import FilenameMatcher, LanguageServerConfig, LanguageServerId
from solidlsp.lsp_protocol_handler.lsp_types import DocumentSymbol, SymbolInformation
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -97,13 +97,13 @@ class AngularTypeScriptServer(TypeScriptLanguageServer):
@classmethod
@override
def get_language_enum_instance(cls) -> Language:
return Language.TYPESCRIPT
def get_language_server_id(cls) -> LanguageServerId:
return LanguageServerId.TYPESCRIPT
def get_source_fn_matcher(self) -> FilenameMatcher:
# Use the Angular matcher so .html template files aren't filtered out of
# reference / search results when the companion is asked about them.
return Language.ANGULAR.get_source_fn_matcher()
return LanguageServerId.ANGULAR.get_source_fn_matcher()
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
"""Dependency provider that returns a pre-resolved executable path.
@@ -293,8 +293,8 @@ class AngularLanguageServer(SolidLanguageServer):
assert shutil.which("node") is not None, "node is not installed or isn't in PATH. Please install NodeJS and try again."
assert shutil.which("npm") is not None, "npm is not installed or isn't in PATH. Please install npm and try again."
ng_settings = solidlsp_settings.get_ls_specific_settings(Language.ANGULAR)
ts_settings = solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT)
ng_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.ANGULAR)
ts_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.TYPESCRIPT)
ls_version = ng_settings.get("angular_language_server_version", DEFAULT_ANGULAR_LANGUAGE_SERVER_VERSION)
svc_version = ng_settings.get("angular_language_service_version", DEFAULT_ANGULAR_LANGUAGE_SERVICE_VERSION)
ts_version = ng_settings.get("typescript_version", ts_settings.get("typescript_version", DEFAULT_TYPESCRIPT_VERSION))
@@ -368,7 +368,7 @@ class AngularLanguageServer(SolidLanguageServer):
def _start_typescript_server(self) -> None:
try:
ts_config = LanguageServerConfig(code_language=Language.TYPESCRIPT, trace_lsp_communication=False)
ts_config = LanguageServerConfig(ls_id=LanguageServerId.TYPESCRIPT, trace_lsp_communication=False)
log.info("Creating companion AngularTypeScriptServer")
self._ts_server = AngularTypeScriptServer(
config=ts_config,
@@ -413,7 +413,7 @@ class AngularLanguageServer(SolidLanguageServer):
non-fatal: we log and fall back to returning an empty list.
"""
try:
html_config = LanguageServerConfig(code_language=Language.HTML, trace_lsp_communication=False)
html_config = LanguageServerConfig(ls_id=LanguageServerId.HTML, trace_lsp_communication=False)
log.info("Creating companion VsCodeHtmlLanguageServer")
self._html_server = VsCodeHtmlLanguageServer(
config=html_config,
@@ -32,7 +32,7 @@ class BasedPyrightLanguageServer(SolidLanguageServer):
config,
repository_root_path,
None,
str(config.code_language),
str(config.ls_id),
solidlsp_settings,
)
@@ -8,7 +8,7 @@ from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
from ..ls_config import Language, LanguageServerConfig
from ..ls_config import LanguageServerConfig, LanguageServerId
from .common import RuntimeDependency, RuntimeDependencyCollection
log = logging.getLogger(__name__)
@@ -73,7 +73,7 @@ class DartLanguageServer(SolidLanguageServer):
@classmethod
def _setup_runtime_dependencies(cls, solidlsp_settings: SolidLSPSettings) -> str:
dart_settings = solidlsp_settings.get_ls_specific_settings(Language.DART)
dart_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.DART)
dart_sdk_version = dart_settings.get("dart_sdk_version", DEFAULT_DART_SDK_VERSION)
deps = RuntimeDependencyCollection(
[
@@ -10,7 +10,7 @@ from typing import Any
from overrides import override
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_utils import FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -77,7 +77,7 @@ class ElixirTools(SolidLanguageServer):
Setup runtime dependencies for Expert.
Downloads the Expert binary for the current platform and returns the path to the executable.
"""
elixir_settings = solidlsp_settings.get_ls_specific_settings(Language.ELIXIR)
elixir_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.ELIXIR)
expert_version = elixir_settings.get("expert_version", EXPERT_VERSION)
# Check if Elixir is available first
elixir_version = cls._get_elixir_version()
@@ -12,7 +12,7 @@ from sensai.util.logging import LogTime
from solidlsp import ls_types
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -73,7 +73,7 @@ class ElmLanguageServer(SolidLanguageServer):
"""
Setup runtime dependencies for Elm Language Server and return the command to start the server.
"""
elm_config = solidlsp_settings.get_ls_specific_settings(Language.ELM)
elm_config = solidlsp_settings.get_ls_specific_settings(LanguageServerId.ELM)
elm_language_server_version = elm_config.get("elm_language_server_version", DEFAULT_ELM_LANGUAGE_SERVER_VERSION)
elm_compiler_version = elm_config.get("elm_compiler_version", DEFAULT_ELM_COMPILER_VERSION)
npm_registry = elm_config.get("npm_registry")
@@ -13,7 +13,7 @@ from overrides import override
from serena.util.dotnet import DotNETUtil
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -71,7 +71,7 @@ class FSharpLanguageServer(SolidLanguageServer):
"""
Setup runtime dependencies for F# Language Server and return the command to start the server.
"""
fsharp_settings = solidlsp_settings.get_ls_specific_settings(Language.FSHARP)
fsharp_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.FSHARP)
fsautocomplete_version = fsharp_settings.get("fsautocomplete_version", DEFAULT_FSAUTOCOMPLETE_VERSION)
dotnet_exe = DotNETUtil("8.0", allow_higher_version=True).get_dotnet_path_or_raise()
@@ -81,7 +81,7 @@ class GodotLanguageServer(SolidLanguageServer):
self._conn_info = TCPConnectionInfo(host="127.0.0.1", port=port)
return TCPLanguageServer(
connection_info=self._conn_info,
language=self.language,
ls_id=self.ls_id,
determine_log_level=self._determine_log_level,
logger=logging_fn,
request_timeout=request_timeout,
@@ -8,7 +8,7 @@ import os
import shlex
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_utils import FileUtils, PlatformUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -82,7 +82,7 @@ class GroovyLanguageServer(SolidLanguageServer):
ls_jar_options = []
if solidlsp_settings.ls_specific_settings:
groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY)
groovy_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.GROOVY)
jar_options_str = groovy_settings.get("ls_jar_options", "")
if jar_options_str:
ls_jar_options = shlex.split(jar_options_str)
@@ -111,7 +111,7 @@ class GroovyLanguageServer(SolidLanguageServer):
Setup runtime dependencies for Groovy Language Server and return paths.
"""
platform_id = PlatformUtils.get_platform_id()
groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY)
groovy_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.GROOVY)
vscode_java_version = groovy_settings.get("vscode_java_version", DEFAULT_VSCODE_JAVA_VERSION)
vscode_java_tag = f"v{vscode_java_version.rsplit('-', 1)[0]}"
@@ -125,7 +125,7 @@ class GroovyLanguageServer(SolidLanguageServer):
java_path = None
if solidlsp_settings and solidlsp_settings.ls_specific_settings:
groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY)
groovy_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.GROOVY)
custom_java_home = groovy_settings.get("ls_java_home_path")
if custom_java_home:
log.info(f"Using custom Java home path from configuration: {custom_java_home}")
@@ -226,7 +226,7 @@ class GroovyLanguageServer(SolidLanguageServer):
Find Groovy Language Server JAR file
"""
if solidlsp_settings and solidlsp_settings.ls_specific_settings:
groovy_settings = solidlsp_settings.get_ls_specific_settings(Language.GROOVY)
groovy_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.GROOVY)
config_jar_path = groovy_settings.get("ls_jar_path")
if config_jar_path and os.path.exists(config_jar_path):
log.info(f"Using Groovy LS JAR from configuration: {config_jar_path}")
@@ -115,7 +115,7 @@ class Intelephense(SolidLanguageServer):
# such that Serena's symbol tools treat the added extensions as PHP sources as well (#1710)
file_filter = self._custom_settings.get("file_filter")
if file_filter:
self.language.get_source_fn_matcher().add_extensions(*file_filter)
self.ls_id.get_source_fn_matcher().add_extensions(*file_filter)
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
@@ -201,7 +201,7 @@ class Intelephense(SolidLanguageServer):
# the initialize request: Intelephense reads configuration exclusively from
# workspace/didChangeConfiguration (its initializationOptions only cover storagePath,
# globalStoragePath, clearCache and licenceKey).
associations = [f"*{ext}" for ext in self.language.get_source_fn_matcher().file_extensions]
associations = [f"*{ext}" for ext in self.ls_id.get_source_fn_matcher().file_extensions]
intelephense_config: DidChangeConfigurationParams = {"settings": {"intelephense": {"files": {"associations": associations}}}}
log.info(f"Sending workspace/didChangeConfiguration with file associations: {associations}")
self.server.notify.workspace_did_change_configuration(intelephense_config)
+3 -3
View File
@@ -15,7 +15,7 @@ from pathlib import Path
from overrides import override
from solidlsp.ls import RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -91,7 +91,7 @@ class LuaLanguageServer(SolidLanguageServer):
]
if solidlsp_settings is not None:
lua_settings = solidlsp_settings.get_ls_specific_settings(Language.LUA)
lua_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.LUA)
lua_ls_version = lua_settings.get("lua_language_server_version", DEFAULT_LUA_LS_VERSION)
ls_resource_dir = _lua_ls_install_dir(LuaLanguageServer.ls_resources_dir(solidlsp_settings), lua_ls_version)
possible_paths.extend(
@@ -118,7 +118,7 @@ class LuaLanguageServer(SolidLanguageServer):
@staticmethod
def _download_lua_ls(solidlsp_settings: SolidLSPSettings) -> str:
"""Download and install lua-language-server if not present."""
lua_settings = solidlsp_settings.get_ls_specific_settings(Language.LUA)
lua_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.LUA)
lua_ls_version = lua_settings.get("lua_language_server_version", DEFAULT_LUA_LS_VERSION)
system = platform.system()
machine = platform.machine().lower()
+2 -2
View File
@@ -11,7 +11,7 @@ from collections.abc import Iterable
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_utils import DotnetVersion, FileUtils, PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -152,7 +152,7 @@ class OmniSharp(SolidLanguageServer):
with open(os.path.join(os.path.dirname(__file__), "omnisharp", "runtime_dependencies.json"), encoding="utf-8") as f:
d = json.load(f)
del d["_description"]
omnisharp_settings = solidlsp_settings.get_ls_specific_settings(Language.CSHARP_OMNISHARP)
omnisharp_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.CSHARP_OMNISHARP)
omnisharp_version = omnisharp_settings.get("omnisharp_version", DEFAULT_OMNISHARP_VERSION)
razor_omnisharp_version = omnisharp_settings.get("razor_omnisharp_version", DEFAULT_RAZOR_OMNISHARP_VERSION)
for dependency in d["runtimeDependencies"]:
@@ -62,7 +62,7 @@ import zipfile
from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, quote_windows_path
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -131,9 +131,9 @@ class PascalLanguageServer(SolidLanguageServer):
proc_env: dict[str, str] = {}
# Read from ls_specific_settings["pascal"]
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
pascal_settings = solidlsp_settings.get_ls_specific_settings(Language.PASCAL)
pascal_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.PASCAL)
# pp: Path to FPC compiler driver (must be fpc.exe, NOT ppc386.exe/ppcx64.exe)
# CodeTools queries fpc.exe for configuration via "fpc -iV", "fpc -iTO", etc.
@@ -638,7 +638,7 @@ class PascalLanguageServer(SolidLanguageServer):
str: The command to start the pasls server
"""
pascal_settings = solidlsp_settings.get_ls_specific_settings(Language.PASCAL)
pascal_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.PASCAL)
pasls_version = pascal_settings.get("pasls_version", PASLS_VERSION)
cls.PASLS_VERSION = pasls_version
cls.PASLS_RELEASES_URL = f"https://github.com/zen010101/pascal-language-server/releases/download/{pasls_version}"
@@ -12,7 +12,7 @@ from typing import Any
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.lsp_types import DidChangeConfigurationParams
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -120,7 +120,7 @@ class PerlLanguageServer(SolidLanguageServer):
``ignore_dirs``); falls back to the defaults otherwise. Extracted as a pure function so the
configuration plumbing can be unit-tested without starting the language server.
"""
perl_settings = solidlsp_settings.get_ls_specific_settings(Language.PERL)
perl_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.PERL)
file_filter = perl_settings.get("file_filter", list(_DEFAULT_FILE_FILTER))
ignore_dirs = perl_settings.get("ignore_dirs", list(_DEFAULT_IGNORE_DIRS))
return file_filter, ignore_dirs
@@ -135,7 +135,7 @@ class PerlLanguageServer(SolidLanguageServer):
language composition detection. Without this, ``find_symbol`` would not surface symbols in
files whose extensions were added to ``file_filter`` (#1449).
"""
Language.PERL.get_source_fn_matcher().add_extensions(*file_filter)
LanguageServerId.PERL.get_source_fn_matcher().add_extensions(*file_filter)
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
# Setup runtime dependencies before initializing
+1 -3
View File
@@ -12,7 +12,7 @@ import subprocess
from overrides import override
from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import FileUtils
from solidlsp.settings import SolidLSPSettings
@@ -109,8 +109,6 @@ class PhpactorServer(SolidLanguageServer):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
super().__init__(config, repository_root_path, None, "php", solidlsp_settings)
# Override internal language enum for correct file matching
self.language = Language.PHP_PHPACTOR
self._ignored_dirnames = {"node_modules", "cache"}
if self._custom_settings.get("ignore_vendor", True):
+1 -4
View File
@@ -17,7 +17,7 @@ from solidlsp.ls import (
LSPFileBuffer,
SolidLanguageServer,
)
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler import lsp_types as protocol_lsp_types
from solidlsp.lsp_protocol_handler.lsp_types import Definition, DefinitionParams, LocationLink
@@ -165,9 +165,6 @@ class PHPantomServer(SolidLanguageServer):
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
super().__init__(config, repository_root_path, None, "php", solidlsp_settings)
self.request_id = 0
self.language = Language.PHP_PHPANTOM
self._ignored_dirnames = {"node_modules", "cache"}
if self._custom_settings.get("ignore_vendor", True):
self._ignored_dirnames.add("vendor")
@@ -23,7 +23,7 @@ from overrides import override
from solidlsp import ls_types
from solidlsp.ls import LSPConstants, RawDocumentSymbol, SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import FileUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
@@ -119,7 +119,7 @@ class PowerShellLanguageServer(SolidLanguageServer):
@classmethod
def _get_pses_path(cls, solidlsp_settings: SolidLSPSettings) -> str | None:
"""Get the path to PowerShell Editor Services installation."""
ps_settings = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL)
ps_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.POWERSHELL)
pses_version = ps_settings.get("pses_version", DEFAULT_PSES_VERSION)
install_dir = _pses_install_dir(cls.ls_resources_dir(solidlsp_settings), pses_version)
start_script = install_dir / "PowerShellEditorServices" / "Start-EditorServices.ps1"
@@ -132,7 +132,7 @@ class PowerShellLanguageServer(SolidLanguageServer):
@classmethod
def _download_pses(cls, solidlsp_settings: SolidLSPSettings) -> str:
"""Download and install PowerShell Editor Services."""
ps_settings = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL)
ps_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.POWERSHELL)
pses_version = ps_settings.get("pses_version", DEFAULT_PSES_VERSION)
download_url = (
f"https://github.com/PowerShell/PowerShellEditorServices/releases/download/v{pses_version}/PowerShellEditorServices.zip"
@@ -184,7 +184,7 @@ class PowerShellLanguageServer(SolidLanguageServer):
# The bundled modules path is the directory containing PowerShellEditorServices
bundled_modules_path = str(Path(pses_path).parent)
psscriptanalyzer_version = solidlsp_settings.get_ls_specific_settings(Language.POWERSHELL).get(
psscriptanalyzer_version = solidlsp_settings.get_ls_specific_settings(LanguageServerId.POWERSHELL).get(
"psscriptanalyzer_version", PSSCRIPTANALYZER_VERSION
)
psscriptanalyzer_path = Path(bundled_modules_path) / "PSScriptAnalyzer" / psscriptanalyzer_version
@@ -51,7 +51,7 @@ class PyreflyLanguageServer(SolidLanguageServer):
config,
repository_root_path,
None,
str(config.code_language),
str(config.ls_id),
solidlsp_settings,
)
self._ensure_workspace_pyrefly_config()
+2 -2
View File
@@ -18,7 +18,7 @@ import threading
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.lsp_protocol_handler.lsp_types import InitializeResult
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -107,7 +107,7 @@ class RubyLsp(SolidLanguageServer):
Setup runtime dependencies for ruby-lsp and return the command list to start the server.
Installation strategy: Bundler project > global ruby-lsp > gem install ruby-lsp at the pinned version
"""
ls_specific_settings = solidlsp_settings.get_ls_specific_settings(Language.RUBY)
ls_specific_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.RUBY)
ruby_lsp_version = ls_specific_settings.get("ruby_lsp_version", RUBY_LSP_VERSION)
# Detect Ruby version manager environment
# Using the version manager's exec wrapper ensures commands run with the correct Ruby version
@@ -52,7 +52,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object
- on_stale_lock: StaleLockMode
- log_multi_instance_notice: bool
"""
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
defaults: dict[str, object] = {
"metals_version": DEFAULT_METALS_VERSION,
@@ -64,7 +64,7 @@ def _get_scala_settings(solidlsp_settings: SolidLSPSettings) -> dict[str, object
if not solidlsp_settings.ls_specific_settings:
return defaults
scala_settings = solidlsp_settings.get_ls_specific_settings(Language.SCALA)
scala_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.SCALA)
# Parse stale lock mode with validation
on_stale_lock_str = scala_settings.get("on_stale_lock", DEFAULT_ON_STALE_LOCK)
@@ -120,7 +120,7 @@ class ScalaLanguageServer(SolidLanguageServer):
config,
repository_root_path,
ProcessLaunchInfo(cmd=scala_lsp_executable_path, cwd=repository_root_path),
config.code_language.value,
config.ls_id.value,
solidlsp_settings,
)
@@ -40,11 +40,6 @@ class Solargraph(SolidLanguageServer):
"ruby",
solidlsp_settings,
)
# Override internal language enum for file matching (excludes .erb files)
# while keeping LSP languageId as "ruby" for protocol compliance
from solidlsp.ls_config import Language
self.language = Language.RUBY_SOLARGRAPH
self.analysis_complete = threading.Event()
self.service_ready_event = threading.Event()
self.initialize_searcher_command_available = threading.Event()
@@ -27,7 +27,7 @@ from solidlsp.ls import (
LSPFileBuffer,
SolidLanguageServer,
)
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_config import FilenameMatcher, LanguageServerConfig, LanguageServerId
from solidlsp.settings import SolidLSPSettings
log = logging.getLogger(__name__)
@@ -100,14 +100,14 @@ class SvelteTypeScriptServer(TypeScriptLanguageServer):
@classmethod
@override
def get_language_enum_instance(cls) -> Language:
def get_language_server_id(cls) -> LanguageServerId:
"""Return TYPESCRIPT; companion uses the TypeScript LS infrastructure."""
return Language.TYPESCRIPT
return LanguageServerId.TYPESCRIPT
@override
def get_source_fn_matcher(self) -> FilenameMatcher:
# include .svelte so references returned by the plugin are not filtered out
return Language.SVELTE.get_source_fn_matcher()
return LanguageServerId.SVELTE.get_source_fn_matcher()
@override
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
@@ -273,7 +273,7 @@ class SvelteLanguageServer(SolidLanguageServer):
@override
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
ts_settings = self._solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT)
ts_settings = self._solidlsp_settings.get_ls_specific_settings(LanguageServerId.TYPESCRIPT)
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, ts_settings)
def __init__(self, config: LanguageServerConfig, repo_path: str, solidlsp_settings: SolidLSPSettings):
@@ -337,7 +337,7 @@ class SvelteLanguageServer(SolidLanguageServer):
def _get_companion_indexing_timeout(self) -> float:
""":return: maximum seconds to wait for companion TS indexing after opening Svelte files."""
ts_settings = self._solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT)
ts_settings = self._solidlsp_settings.get_ls_specific_settings(LanguageServerId.TYPESCRIPT)
timeout = self._custom_settings.get(
"indexing_timeout",
ts_settings.get("indexing_timeout", SvelteTypeScriptServer.INDEXING_PROGRESS_TIMEOUT),
@@ -417,7 +417,7 @@ class SvelteLanguageServer(SolidLanguageServer):
"""Spawn the companion :class:`SvelteTypeScriptServer`, wait for ready, then index .svelte files."""
try:
ts_config = LanguageServerConfig(
code_language=Language.TYPESCRIPT,
ls_id=LanguageServerId.TYPESCRIPT,
trace_lsp_communication=False,
)
log.info("Creating companion SvelteTypeScriptServer")
@@ -5,7 +5,7 @@ import shutil
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_utils import PlatformUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -124,7 +124,7 @@ class TerraformLS(SolidLanguageServer):
Downloads and installs terraform-ls if not already present.
"""
cls._ensure_tf_command_available()
terraform_settings = solidlsp_settings.get_ls_specific_settings(Language.TERRAFORM)
terraform_settings = solidlsp_settings.get_ls_specific_settings(LanguageServerId.TERRAFORM)
terraform_ls_version = terraform_settings.get("terraform_ls_version", DEFAULT_TERRAFORM_LS_VERSION)
platform_id = PlatformUtils.get_platform_id()
deps = RuntimeDependencyCollection(
+1 -1
View File
@@ -34,7 +34,7 @@ class TyLanguageServer(SolidLanguageServer):
config,
repository_root_path,
None,
str(config.code_language),
str(config.ls_id),
solidlsp_settings,
)
@@ -12,7 +12,7 @@ import threading
from overrides import override
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_utils import PlatformId, PlatformUtils
from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
from solidlsp.settings import SolidLSPSettings
@@ -87,7 +87,7 @@ class VtsLanguageServer(SolidLanguageServer):
PlatformId.WIN_arm64,
]
assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for vtsls at the moment"
vts_config = solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT_VTS)
vts_config = solidlsp_settings.get_ls_specific_settings(LanguageServerId.TYPESCRIPT_VTS)
vtsls_version = vts_config.get("vtsls_version", DEFAULT_VTSLS_VERSION)
npm_registry = vts_config.get("npm_registry")
@@ -22,7 +22,7 @@ from solidlsp.language_servers.typescript_language_server import (
prefer_non_node_modules_definition,
)
from solidlsp.ls import LanguageServerDependencyProvider, LSPFileBuffer, SolidLanguageServer
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_config import FilenameMatcher, LanguageServerConfig, LanguageServerId
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import Location
from solidlsp.ls_utils import PathUtils
@@ -39,19 +39,19 @@ class VueTypeScriptServer(TypeScriptLanguageServer):
@classmethod
@override
def get_language_enum_instance(cls) -> Language:
def get_language_server_id(cls) -> LanguageServerId:
"""Return TYPESCRIPT since this is a TypeScript language server variant.
Note: VueTypeScriptServer is a companion server that uses TypeScript's language server
with the Vue TypeScript plugin. It reports as TYPESCRIPT to maintain compatibility
with the TypeScript language server infrastructure.
"""
return Language.TYPESCRIPT
return LanguageServerId.TYPESCRIPT
def get_source_fn_matcher(self) -> FilenameMatcher:
# must override with Vue-specific matcher to ensure .vue files are included (as they can be discovered via references,
# for instance; otherwise, we may find references in .vue files but then filter the results out, because .vue files are ignored.)
return Language.VUE.get_source_fn_matcher()
return LanguageServerId.VUE.get_source_fn_matcher()
class DependencyProvider(TypeScriptLanguageServer.DependencyProvider):
"""Dependency provider that returns a pre-resolved executable path.
@@ -536,10 +536,10 @@ class VueLanguageServer(SolidLanguageServer):
assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
# Get TypeScript version settings from TypeScript language server settings
typescript_config = solidlsp_settings.get_ls_specific_settings(Language.TYPESCRIPT)
typescript_config = solidlsp_settings.get_ls_specific_settings(LanguageServerId.TYPESCRIPT)
typescript_version = typescript_config.get("typescript_version", "5.9.3")
typescript_language_server_version = typescript_config.get("typescript_language_server_version", "5.1.3")
vue_config = solidlsp_settings.get_ls_specific_settings(Language.VUE)
vue_config = solidlsp_settings.get_ls_specific_settings(LanguageServerId.VUE)
vue_language_server_version = vue_config.get("vue_language_server_version", "3.1.5")
npm_registry = vue_config.get("npm_registry", typescript_config.get("npm_registry"))
@@ -659,7 +659,7 @@ class VueLanguageServer(SolidLanguageServer):
vue_ts_plugin_path = os.path.join(self._vue_ls_dir, "node_modules", "@vue", "typescript-plugin")
ts_config = LanguageServerConfig(
code_language=Language.TYPESCRIPT,
ls_id=LanguageServerId.TYPESCRIPT,
trace_lsp_communication=False,
)
+13 -13
View File
@@ -31,7 +31,7 @@ from solidlsp.dependency_provider import (
LanguageServerDependencyProviderUvx,
)
from solidlsp.initialize_params import DefaultInitializeParamsBuilder, InitializeParamsBuilder
from solidlsp.ls_config import FilenameMatcher, Language, LanguageServerConfig
from solidlsp.ls_config import FilenameMatcher, LanguageServerConfig, LanguageServerId
from solidlsp.ls_exceptions import InvalidTextLocationError, SolidLSPException
from solidlsp.ls_process import LanguageServerInterface, StdioLanguageServer
from solidlsp.ls_types import UnifiedSymbolInformation
@@ -383,8 +383,8 @@ class SolidLanguageServer(ABC):
return logging.INFO
@classmethod
def get_language_enum_instance(cls) -> Language:
return Language.from_ls_class(cls)
def get_language_server_id(cls) -> LanguageServerId:
return LanguageServerId.from_ls_class(cls)
@classmethod
def supports_implementation_request(cls) -> bool:
@@ -442,7 +442,7 @@ class SolidLanguageServer(ABC):
# Ensure repository_root_path is absolute to avoid issues with file URIs
repository_root_path = os.path.abspath(repository_root_path)
ls_class = config.code_language.get_ls_class()
ls_class = config.ls_id.get_ls_class()
# All language server implementations are required to use the same signature of the constructor
# (which differs from the signature of the base class constructor).
ls = ls_class(config, repository_root_path, solidlsp_settings)
@@ -481,13 +481,13 @@ class SolidLanguageServer(ABC):
"""
self.config = config
self._solidlsp_settings = solidlsp_settings
lang = self.get_language_enum_instance()
self._custom_settings = solidlsp_settings.get_ls_specific_settings(lang)
ls_id = self.get_language_server_id()
self._custom_settings = solidlsp_settings.get_ls_specific_settings(ls_id)
"""
the (user-provided) language server-specific settings
"""
self._ls_resources_dir = self.ls_resources_dir(solidlsp_settings)
log.debug(f"Custom config (LS-specific settings) for {lang}: {self._custom_settings}")
log.debug(f"Custom config (LS-specific settings) for {ls_id}: {self._custom_settings}")
self._encoding = config.encoding
self.repository_root_path: str = repository_root_path
@@ -500,15 +500,15 @@ class SolidLanguageServer(ABC):
default language identifier to be passed to the language server in `textDocument/didOpen` notifications.
"""
self.open_file_buffers: dict[str, LSPFileBuffer] = {}
self.language = self.get_language_enum_instance()
self.ls_id = self.get_language_server_id()
"""
identifies the language server (not to be confused with the language id passed to the language server)
identifies the language server (not to be confused with the language_id passed to the language server)
"""
# The source filename matcher is a @cache'd per-language singleton. A previous project may
# have extended it (e.g. Perl's file_filter adding .cgi); reset it here so every activation
# starts from the language's default extensions, then language-server subclasses re-apply
# their own settings during the rest of __init__.
self.language.get_source_fn_matcher().reset()
self.ls_id.get_source_fn_matcher().reset()
self._published_diagnostics: dict[str, list[ls_types.Diagnostic]] = {}
self._published_diagnostics_generation_by_uri: dict[str, int] = {}
self._published_diagnostics_generation = 0
@@ -611,7 +611,7 @@ class SolidLanguageServer(ABC):
log.debug(f"Creating language server instance with {language_id=} and {process_launch_info}")
return StdioLanguageServer(
process_launch_info,
language=self.language,
ls_id=self.ls_id,
determine_log_level=self._determine_log_level,
logger=logging_fn,
start_independent_lsp_process=self.config.start_independent_lsp_process,
@@ -1167,7 +1167,7 @@ class SolidLanguageServer(ABC):
are understood by this language server or are discovered as containing sources indirectly, e.g. via references
"""
# By default, use the matcher of the language
return self.language.get_source_fn_matcher()
return self.ls_id.get_source_fn_matcher()
def is_ignored_path(self, relative_path: str, ignore_unsupported_files: bool = True) -> bool:
"""
@@ -3131,7 +3131,7 @@ class SolidLanguageServer(ABC):
:return: self for method chaining
"""
log.info(f"Starting language server with language {self.language_server.language} for {self.language_server.repository_root_path}")
log.info(f"Starting language server {self.language_server.ls_id} for {self.language_server.repository_root_path}")
self.server_started = True
self._start_server()
return self
+2 -2
View File
@@ -87,7 +87,7 @@ class FilenameMatcher:
return False
class Language(str, Enum):
class LanguageServerId(str, Enum):
"""
Enumeration of language servers supported by SolidLSP.
"""
@@ -895,7 +895,7 @@ class LanguageServerConfig:
Configuration parameters for a language server instance
"""
code_language: Language
ls_id: LanguageServerId
"""
defines the language server to use
"""
+3 -3
View File
@@ -2,7 +2,7 @@
This module contains the exceptions raised by the framework.
"""
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
class SolidLSPException(Exception):
@@ -28,14 +28,14 @@ class SolidLSPException(Exception):
return isinstance(self.cause, LanguageServerTerminatedException)
def get_affected_language(self) -> Language | None:
def get_affected_language(self) -> LanguageServerId | None:
"""
:return: the affected language for the case where the exception is caused by the language server having terminated
"""
from .ls_process import LanguageServerTerminatedException
if isinstance(self.cause, LanguageServerTerminatedException):
return self.cause.language
return self.cause.ls_id
return None
def __str__(self) -> str:
+24 -24
View File
@@ -14,7 +14,7 @@ from typing import IO, Any, AnyStr, cast
from sensai.util.string import ToStringMixin
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_request import LanguageServerRequest
from solidlsp.lsp_protocol_handler.lsp_requests import LspNotification
@@ -53,10 +53,10 @@ class LanguageServerTerminatedException(Exception):
Exception raised when the language server process has terminated unexpectedly.
"""
def __init__(self, message: str, language: Language, cause: Exception | None = None) -> None:
def __init__(self, message: str, ls_id: LanguageServerId, cause: Exception | None = None) -> None:
super().__init__(message)
self.message = message
self.language = language
self.ls_id = ls_id
self.cause = cause
def __str__(self) -> str:
@@ -116,18 +116,18 @@ class LanguageServerInterface(ABC):
def __init__(
self,
language: Language,
ls_id: LanguageServerId,
determine_log_level: Callable[[str], int],
logger: Callable[[str, str, StringDict | str], None] | None = None,
request_timeout: float | None = None,
) -> None:
"""
:param language: the language
:param ls_id: the language server identifier
:param determine_log_level: a function for log lines read from stderr, which determines the log level
:param logger: the trace logger function
:param request_timeout: the timeout, in seconds, for all requests sent to the language server. If None, no timeout will be applied.
"""
self.language = language
self.ls_id = ls_id
self._determine_log_level = determine_log_level
self.send = LanguageServerRequest(self)
"""
@@ -486,7 +486,7 @@ class StdioLanguageServer(LanguageServerInterface):
def __init__(
self,
process_launch_info: ProcessLaunchInfo,
language: Language,
ls_id: LanguageServerId,
determine_log_level: Callable[[str], int],
logger: Callable[[str, str, StringDict | str], None] | None = None,
start_independent_lsp_process: bool = True,
@@ -494,13 +494,13 @@ class StdioLanguageServer(LanguageServerInterface):
) -> None:
"""
:param process_launch_info: the information required to launch the language server process
:param language: the language
:param ls_id: the language
:param determine_log_level: a function for log lines read from stderr, which determines the log level
:param logger: the trace logger function
:param start_independent_lsp_process: whether to start the language server process in an independent process group
:param request_timeout: the timeout, in seconds, for all requests sent to the language server. If None, no timeout will be applied.
"""
super().__init__(language, determine_log_level, logger, request_timeout)
super().__init__(ls_id, determine_log_level, logger, request_timeout)
self.process_launch_info = process_launch_info
self.process: subprocess.Popen[bytes] | None = None
@@ -547,12 +547,12 @@ class StdioLanguageServer(LanguageServerInterface):
# start threads to read stdout and stderr of the process
threading.Thread(
target=self._read_ls_process_stdout,
name=f"LSP-stdout-reader:{self.language.value}",
name=f"LSP-stdout-reader:{self.ls_id.value}",
daemon=True,
).start()
threading.Thread(
target=self._read_ls_process_stderr,
name=f"LSP-stderr-reader:{self.language.value}",
name=f"LSP-stderr-reader:{self.ls_id.value}",
daemon=True,
).start()
@@ -571,7 +571,7 @@ class StdioLanguageServer(LanguageServerInterface):
# Ignore errors here, we are proceeding to terminate anyway.
# terminate the process
subprocess_util.terminate_process_tree_with_kill_fallback(
self.process, terminate_timeout=timeout, process_name=f"LS[{self.language.value}]"
self.process, terminate_timeout=timeout, process_name=f"LS[{self.ls_id.value}]"
)
finally:
self.process = None
@@ -594,7 +594,7 @@ class StdioLanguageServer(LanguageServerInterface):
if process.poll() is not None:
raise LanguageServerTerminatedException(
f"Process terminated while trying to read response (read {len(data)} of {num_bytes} bytes before termination)",
language=self.language,
ls_id=self.ls_id,
)
# Process still running but no data available yet, retry after a short delay
time.sleep(0.01)
@@ -631,15 +631,15 @@ class StdioLanguageServer(LanguageServerInterface):
except LanguageServerTerminatedException as e:
exception = e
except (BrokenPipeError, ConnectionResetError) as e:
exception = LanguageServerTerminatedException("Language server process terminated while reading stdout", self.language, cause=e)
exception = LanguageServerTerminatedException("Language server process terminated while reading stdout", self.ls_id, cause=e)
except Exception as e:
exception = LanguageServerTerminatedException(
"Unexpected error while reading stdout from language server process", self.language, cause=e
"Unexpected error while reading stdout from language server process", self.ls_id, cause=e
)
log.info("Language server stdout reader thread has terminated")
if not self._is_stopping:
if exception is None:
exception = LanguageServerTerminatedException("Language server stdout read process terminated unexpectedly", self.language)
exception = LanguageServerTerminatedException("Language server stdout read process terminated unexpectedly", self.ls_id)
log.error(str(exception))
self._cancel_pending_requests(exception)
@@ -704,12 +704,12 @@ class TCPLanguageServer(LanguageServerInterface):
def __init__(
self,
connection_info: TCPConnectionInfo,
language: Language,
ls_id: LanguageServerId,
determine_log_level: Callable[[str], int],
logger: Callable[[str, str, StringDict | str], None] | None = None,
request_timeout: float | None = None,
) -> None:
super().__init__(language, determine_log_level, logger, request_timeout)
super().__init__(ls_id, determine_log_level, logger, request_timeout)
self._connection_info = connection_info
self._sock: socket.socket | None = None
self._file: Any = None # socket.makefile("rb") - buffered reader
@@ -747,7 +747,7 @@ class TCPLanguageServer(LanguageServerInterface):
threading.Thread(
target=self._read_loop,
name=f"LSP-tcp-reader:{self.language.value}",
name=f"LSP-tcp-reader:{self.ls_id.value}",
daemon=True,
).start()
@@ -788,7 +788,7 @@ class TCPLanguageServer(LanguageServerInterface):
log.error("Failed to write to TCP language server: %s", e)
self._sock = None
self._file = None
self._cancel_pending_requests(LanguageServerTerminatedException("TCP send error", self.language, cause=e))
self._cancel_pending_requests(LanguageServerTerminatedException("TCP send error", self.ls_id, cause=e))
def _read_loop(self) -> None:
"""Read Content-Length-framed LSP messages from the TCP socket and dispatch them."""
@@ -802,7 +802,7 @@ class TCPLanguageServer(LanguageServerInterface):
line = f.readline()
except OSError as exc:
if not self._is_stopping:
exception = LanguageServerTerminatedException("TCP read error", self.language, cause=exc)
exception = LanguageServerTerminatedException("TCP read error", self.ls_id, cause=exc)
break
if not line:
break
@@ -824,17 +824,17 @@ class TCPLanguageServer(LanguageServerInterface):
body = f.read(num_bytes)
except OSError as exc:
if not self._is_stopping:
exception = LanguageServerTerminatedException("TCP read error", self.language, cause=exc)
exception = LanguageServerTerminatedException("TCP read error", self.ls_id, cause=exc)
break
if len(body) < num_bytes:
break
self._handle_body(body)
except Exception as exc:
exception = LanguageServerTerminatedException("Unexpected error in TCP language server read loop", self.language, cause=exc)
exception = LanguageServerTerminatedException("Unexpected error in TCP language server read loop", self.ls_id, cause=exc)
log.info("TCP language server read loop has terminated")
if not self._is_stopping:
if exception is None:
exception = LanguageServerTerminatedException("TCP language server read loop terminated unexpectedly", self.language)
exception = LanguageServerTerminatedException("TCP language server read loop terminated unexpectedly", self.ls_id)
log.error(str(exception))
self._cancel_pending_requests(exception)
# Clear the socket so is_running() returns False, allowing _ensure_functional_ls
+11 -7
View File
@@ -11,7 +11,7 @@ from typing import TYPE_CHECKING, Any
from sensai.util.string import ToStringMixin
if TYPE_CHECKING:
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
log = logging.getLogger(__name__)
@@ -34,7 +34,7 @@ class SolidLSPSettings:
For instance, if this is "/home/user/myproject/.solidlsp",
then Solid-LSP will store project-specific data (e.g. caches) in that directory.
"""
ls_specific_settings: dict["Language", dict[str, Any]] = field(default_factory=dict)
ls_specific_settings: dict["LanguageServerId", dict[str, Any]] = field(default_factory=dict)
"""
Advanced configuration option allowing to configure language server implementation specific options.
Have a look at the docstring of the constructors of the corresponding LS implementations within solidlsp to see which options are available.
@@ -50,6 +50,10 @@ class SolidLSPSettings:
return os.path.join(str(self.solidlsp_dir), "language_servers", "static")
class CustomLSSettings(ToStringMixin):
"""
Represents custom (user-specified) settings for a specific language server.
"""
def __init__(self, settings: dict[str, Any] | None) -> None:
self.settings = settings or {}
@@ -69,11 +73,11 @@ class SolidLSPSettings:
value = default_value
return value
def get_ls_specific_settings(self, language: "Language") -> CustomLSSettings:
def get_ls_specific_settings(self, ls_id: "LanguageServerId") -> CustomLSSettings:
"""
Get the language server specific settings for the given language.
Gets the custom settings for the given language server
:param language: The programming language.
:return: A dictionary of settings for the language server.
:param ls_id: the language server identifier for which to retrieve settings
:return: a dictionary of settings for the language server
"""
return self.CustomLSSettings(self.ls_specific_settings.get(language))
return self.CustomLSSettings(self.ls_specific_settings.get(ls_id))
+113 -113
View File
@@ -19,7 +19,7 @@ from serena.constants import SERENA_MANAGED_DIR_NAME
from serena.project import Project
from serena.util.file_system import GitignoreParser
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.settings import SolidLSPSettings
from .solidlsp.clojure import is_clojure_cli_available
@@ -39,47 +39,47 @@ def resources_dir() -> Path:
class LanguageParamRequest:
param: Language
param: LanguageServerId
_LANGUAGE_REPO_ALIASES: dict[Language, Language] = {
Language.CPP_CCLS: Language.CPP,
Language.PHP_PHPACTOR: Language.PHP,
Language.PHP_PHPANTOM: Language.PHP,
Language.PYTHON_JEDI: Language.PYTHON,
Language.PYTHON_BASEDPYRIGHT: Language.PYTHON,
Language.PYTHON_TY: Language.PYTHON,
Language.RUBY_SOLARGRAPH: Language.RUBY,
Language.PYTHON_TY: Language.PYTHON,
Language.PYTHON_PYREFLY: Language.PYTHON,
_LANGUAGE_REPO_ALIASES: dict[LanguageServerId, LanguageServerId] = {
LanguageServerId.CPP_CCLS: LanguageServerId.CPP,
LanguageServerId.PHP_PHPACTOR: LanguageServerId.PHP,
LanguageServerId.PHP_PHPANTOM: LanguageServerId.PHP,
LanguageServerId.PYTHON_JEDI: LanguageServerId.PYTHON,
LanguageServerId.PYTHON_BASEDPYRIGHT: LanguageServerId.PYTHON,
LanguageServerId.PYTHON_TY: LanguageServerId.PYTHON,
LanguageServerId.RUBY_SOLARGRAPH: LanguageServerId.RUBY,
LanguageServerId.PYTHON_TY: LanguageServerId.PYTHON,
LanguageServerId.PYTHON_PYREFLY: LanguageServerId.PYTHON,
}
PYTHON_LANGUAGE_BACKENDS = [Language.PYTHON, Language.PYTHON_TY, Language.PYTHON_BASEDPYRIGHT]
PYTHON_LANGUAGE_BACKENDS = [LanguageServerId.PYTHON, LanguageServerId.PYTHON_TY, LanguageServerId.PYTHON_BASEDPYRIGHT]
def get_repo_path(language: Language) -> Path:
def get_repo_path(language: LanguageServerId) -> Path:
repo_language = _LANGUAGE_REPO_ALIASES.get(language, language)
return Path(__file__).parent / "resources" / "repos" / repo_language / "test_repo"
def _create_ls(
language: Language,
ls_id: LanguageServerId,
repo_path: str | None = None,
ignored_paths: list[str] | None = None,
trace_lsp_communication: bool = False,
ls_specific_settings: dict[Language, dict[str, Any]] | None = None,
ls_specific_settings: dict[LanguageServerId, dict[str, Any]] | None = None,
workspace_folders: list[str] | None = None,
additional_workspace_folders: list[str] | None = None,
solidlsp_dir: Path | None = None,
) -> SolidLanguageServer:
ignored_paths = ignored_paths or []
if repo_path is None:
repo_path = str(get_repo_path(language))
repo_path = str(get_repo_path(ls_id))
gitignore_parser = GitignoreParser(str(repo_path))
for spec in gitignore_parser.get_ignore_specs():
ignored_paths.extend(spec.patterns)
config = LanguageServerConfig(
code_language=language,
ls_id=ls_id,
ignored_paths=ignored_paths,
trace_lsp_communication=trace_lsp_communication,
workspace_folders=workspace_folders or ["."],
@@ -100,17 +100,17 @@ def _create_ls(
@contextmanager
def start_ls_context(
language: Language,
ls_id: LanguageServerId,
repo_path: str | None = None,
ignored_paths: list[str] | None = None,
trace_lsp_communication: bool = False,
ls_specific_settings: dict[Language, dict[str, Any]] | None = None,
ls_specific_settings: dict[LanguageServerId, dict[str, Any]] | None = None,
workspace_folders: list[str] | None = None,
additional_workspace_folders: list[str] | None = None,
solidlsp_dir: Path | None = None,
) -> Iterator[SolidLanguageServer]:
ls = _create_ls(
language,
ls_id,
repo_path,
ignored_paths,
trace_lsp_communication,
@@ -119,14 +119,14 @@ def start_ls_context(
additional_workspace_folders,
solidlsp_dir,
)
log.info(f"Starting language server for {language} {repo_path}")
log.info(f"Starting language server for {ls_id} {repo_path}")
with ls.start_server_context():
yield ls
@contextmanager
def start_default_ls_context(language: Language) -> Iterator[SolidLanguageServer]:
with start_ls_context(language) as ls:
def start_default_ls_context(ls_id: LanguageServerId) -> Iterator[SolidLanguageServer]:
with start_ls_context(ls_id) as ls:
yield ls
@@ -134,8 +134,8 @@ def create_default_serena_config():
return SerenaConfig().with_headless_mode_overrides()
def _create_default_project(language: Language, repo_root_override: str | None = None) -> Project:
repo_path = str(get_repo_path(language)) if repo_root_override is None else repo_root_override
def _create_default_project(ls_id: LanguageServerId, repo_root_override: str | None = None) -> Project:
repo_path = str(get_repo_path(ls_id)) if repo_root_override is None else repo_root_override
return Project.load(repo_path, serena_config=create_default_serena_config())
@@ -193,9 +193,9 @@ def language_server(request: LanguageParamRequest):
@contextmanager
def project_context(language: Language, repo_root_override: str | None = None) -> Iterator[Project]:
def project_context(ls_id: LanguageServerId, repo_root_override: str | None = None) -> Iterator[Project]:
"""Context manager that creates a Project for the specified language and ensures proper cleanup."""
project = _create_default_project(language, repo_root_override)
project = _create_default_project(ls_id, repo_root_override)
try:
yield project
finally:
@@ -233,16 +233,16 @@ def project(request: LanguageParamRequest, repo_root_override: str | None = None
@contextmanager
def project_with_ls_context(language: Language, repo_root_override: str | None = None) -> Iterator[Project]:
def project_with_ls_context(ls_id: LanguageServerId, repo_root_override: str | None = None) -> Iterator[Project]:
"""Context manager that creates a Project with an active language server for the specified language."""
with project_context(language, repo_root_override) as project:
with project_context(ls_id, repo_root_override) as project:
project.create_language_server_manager()
yield project
@contextmanager
def agent_for_project_context(language: Language, repo_root_override: str | None = None) -> Iterator[SerenaAgent]:
project_root = str(get_repo_path(language)) if repo_root_override is None else repo_root_override
def agent_for_project_context(ls_id: LanguageServerId, repo_root_override: str | None = None) -> Iterator[SerenaAgent]:
project_root = str(get_repo_path(ls_id)) if repo_root_override is None else repo_root_override
agent = SerenaAgent(project=project_root, serena_config=create_default_serena_config())
# wait for agent to be ready
@@ -273,50 +273,50 @@ is_macos = platform.system() == "Darwin"
is_linux = platform.system() == "Linux"
_LANGUAGE_PYTEST_MARKERS: dict[Language, list[MarkDecorator | Mark]] = {
Language.ADA: [pytest.mark.ada],
Language.CLOJURE: [pytest.mark.clojure],
Language.CPP: [pytest.mark.cpp],
Language.CPP_CCLS: [pytest.mark.cpp],
Language.CUE: [pytest.mark.cue],
Language.CSHARP: [pytest.mark.csharp],
Language.FSHARP: [pytest.mark.fsharp],
Language.GO: [pytest.mark.go],
Language.HAXE: [pytest.mark.haxe],
Language.JAVA: [pytest.mark.java],
Language.KOTLIN: [pytest.mark.kotlin],
Language.LEAN4: [pytest.mark.lean4],
Language.LATEX: [pytest.mark.latex],
Language.MSL: [pytest.mark.msl],
Language.PHP: [pytest.mark.php],
Language.PHP_PHPACTOR: [pytest.mark.php],
Language.PHP_PHPANTOM: [pytest.mark.php],
Language.POWERSHELL: [pytest.mark.powershell],
Language.PYTHON: [pytest.mark.python],
Language.PYTHON_JEDI: [pytest.mark.python],
Language.PYTHON_TY: [pytest.mark.python],
Language.PYTHON_PYREFLY: [pytest.mark.python],
Language.PYTHON_BASEDPYRIGHT: [pytest.mark.python],
Language.RUST: [pytest.mark.rust],
Language.TYPESCRIPT: [pytest.mark.typescript],
Language.BSL: [pytest.mark.bsl],
Language.SVELTE: [pytest.mark.svelte],
Language.ANGULAR: [pytest.mark.angular],
Language.HTML: [pytest.mark.html],
Language.SCSS: [pytest.mark.scss],
_LANGUAGE_PYTEST_MARKERS: dict[LanguageServerId, list[MarkDecorator | Mark]] = {
LanguageServerId.ADA: [pytest.mark.ada],
LanguageServerId.CLOJURE: [pytest.mark.clojure],
LanguageServerId.CPP: [pytest.mark.cpp],
LanguageServerId.CPP_CCLS: [pytest.mark.cpp],
LanguageServerId.CUE: [pytest.mark.cue],
LanguageServerId.CSHARP: [pytest.mark.csharp],
LanguageServerId.FSHARP: [pytest.mark.fsharp],
LanguageServerId.GO: [pytest.mark.go],
LanguageServerId.HAXE: [pytest.mark.haxe],
LanguageServerId.JAVA: [pytest.mark.java],
LanguageServerId.KOTLIN: [pytest.mark.kotlin],
LanguageServerId.LEAN4: [pytest.mark.lean4],
LanguageServerId.LATEX: [pytest.mark.latex],
LanguageServerId.MSL: [pytest.mark.msl],
LanguageServerId.PHP: [pytest.mark.php],
LanguageServerId.PHP_PHPACTOR: [pytest.mark.php],
LanguageServerId.PHP_PHPANTOM: [pytest.mark.php],
LanguageServerId.POWERSHELL: [pytest.mark.powershell],
LanguageServerId.PYTHON: [pytest.mark.python],
LanguageServerId.PYTHON_JEDI: [pytest.mark.python],
LanguageServerId.PYTHON_TY: [pytest.mark.python],
LanguageServerId.PYTHON_PYREFLY: [pytest.mark.python],
LanguageServerId.PYTHON_BASEDPYRIGHT: [pytest.mark.python],
LanguageServerId.RUST: [pytest.mark.rust],
LanguageServerId.TYPESCRIPT: [pytest.mark.typescript],
LanguageServerId.BSL: [pytest.mark.bsl],
LanguageServerId.SVELTE: [pytest.mark.svelte],
LanguageServerId.ANGULAR: [pytest.mark.angular],
LanguageServerId.HTML: [pytest.mark.html],
LanguageServerId.SCSS: [pytest.mark.scss],
}
def get_pytest_markers(language: Language) -> list[MarkDecorator | Mark]:
"""Pytest markers for a language.
def get_pytest_markers(ls_id: LanguageServerId) -> list[MarkDecorator | Mark]:
"""Pytest markers for a language server.
Returns the primary language marker plus the central enablement skip derived from
Returns the primary language server marker plus the central enablement skip derived from
``language_tests_enabled()`` -- so per-language availability/reliability lives in exactly one
place (``_determine_disabled_languages``) instead of being duplicated per marker or per test file.
"""
return [
*_LANGUAGE_PYTEST_MARKERS[language],
pytest.mark.skipif(not language_tests_enabled(language), reason=f"{language.value} tests are disabled in this environment"),
*_LANGUAGE_PYTEST_MARKERS[ls_id],
pytest.mark.skipif(not language_server_tests_enabled(ls_id), reason=f"{ls_id.value} tests are disabled in this environment"),
]
@@ -395,9 +395,9 @@ def _is_ocaml_lsp_available() -> bool:
return False
def _determine_disabled_languages() -> list[Language]:
def _determine_disabled_language_servers() -> list[LanguageServerId]:
"""
Determine which language tests are disabled in the current environment.
Determine which language server tests are disabled in the current environment.
Every language falls into exactly ONE of the categories below; a language that is not appended
here is **category 4 (enabled everywhere)**, e.g. python, typescript, go, java, kotlin-locally.
@@ -412,112 +412,112 @@ def _determine_disabled_languages() -> list[Language]:
4. ENABLED EVERYWHERE -- not listed in this function at all.
5. DISABLED ONLY ON CI (resource/stability reasons) even though the precondition holds locally.
"""
result: list[Language] = []
result: list[LanguageServerId] = []
# === 1. Always disabled (flaky / broken everywhere) ===
result.append(Language.BSL) # 1C:Enterprise; niche and the tests are slow and flaky
result.append(Language.FSHARP) # F# language server is currently unreliable
result.append(LanguageServerId.BSL) # 1C:Enterprise; niche and the tests are slow and flaky
result.append(LanguageServerId.FSHARP) # F# language server is currently unreliable
# === 2. Disabled off-CI if the precondition is missing; expected to be present on CI ===
if _sh.which("terraform") is None and not is_ci:
result.append(Language.TERRAFORM)
result.append(LanguageServerId.TERRAFORM)
if _sh.which("regal") is None and not is_ci:
result.append(Language.REGO)
result.append(LanguageServerId.REGO)
if _sh.which("elm") is None and not is_ci:
result.append(Language.ELM)
result.append(LanguageServerId.ELM)
# qmlls is installed (standalone build; see pytest.yml) only on the Ubuntu other-langs CI batch. It is
# expected there, so a missing binary on Linux CI is NOT skipped here -- the test runs and fails loudly,
# catching a CI setup regression. On Windows/macOS CI (never installed) and off-CI without the binary it skips.
if (_sh.which("qmlls6") is None and _sh.which("qmlls") is None) and not (is_ci and is_linux):
result.append(Language.QML)
result.append(LanguageServerId.QML)
# === 3. Disabled wherever the precondition is missing (including on CI) ===
# 3a. Platform precondition: these language servers have no native Windows support.
if is_windows:
result.append(Language.ANSIBLE) # ansible-language-server has no native Windows support
result.append(LanguageServerId.ANSIBLE) # ansible-language-server has no native Windows support
if not is_macos:
result.append(Language.SWIFT) # swiftly toolchain is only set up on the macOS native batch
result.append(LanguageServerId.SWIFT) # swiftly toolchain is only set up on the macOS native batch
# 3b. Toolchain / language-server availability (the LS/compiler must be on PATH or installed).
if _sh.which("clangd") is None:
result.append(Language.CPP)
result.append(LanguageServerId.CPP)
if _sh.which("ccls") is None or is_windows: # no recent ccls binary is available for Windows
result.append(Language.CPP_CCLS)
result.append(LanguageServerId.CPP_CCLS)
if _sh.which("php") is None:
result.append(Language.PHP_PHPACTOR)
result.append(Language.PHP_PHPANTOM)
result.append(LanguageServerId.PHP_PHPACTOR)
result.append(LanguageServerId.PHP_PHPANTOM)
if not is_clojure_cli_available():
result.append(Language.CLOJURE)
result.append(LanguageServerId.CLOJURE)
if _sh.which("verible-verilog-ls") is None:
result.append(Language.SYSTEMVERILOG)
result.append(LanguageServerId.SYSTEMVERILOG)
if not _is_matlab_available():
result.append(Language.MATLAB)
result.append(LanguageServerId.MATLAB)
if ERLANG_LS_UNAVAILABLE: # no Erlang-OTP / no rebar3 / Windows -- see test/solidlsp/erlang
result.append(Language.ERLANG)
result.append(LanguageServerId.ERLANG)
if EXPERT_UNAVAILABLE: # Elixir not installed -- see test/solidlsp/elixir
result.append(Language.ELIXIR)
result.append(LanguageServerId.ELIXIR)
if _sh.which("lean") is None:
result.append(Language.LEAN4)
result.append(LanguageServerId.LEAN4)
if _sh.which("crystalline") is None:
result.append(Language.CRYSTAL)
result.append(LanguageServerId.CRYSTAL)
if _sh.which("julia") is None: # LanguageServer.jl is auto-installed by the LS when julia is present
result.append(Language.JULIA)
result.append(LanguageServerId.JULIA)
if _sh.which("nixd") is None:
result.append(Language.NIX)
result.append(LanguageServerId.NIX)
if _sh.which("haskell-language-server-wrapper") is None:
result.append(Language.HASKELL)
result.append(LanguageServerId.HASKELL)
if not _is_r_language_server_available(): # `which("R")` isn't enough -- needs the languageserver package
result.append(Language.R)
result.append(LanguageServerId.R)
if not _is_ocaml_lsp_available(): # opam alone isn't enough -- needs the ocaml-lsp-server package
result.append(Language.OCAML)
result.append(LanguageServerId.OCAML)
if not _is_perl_language_server_available(): # perl ships with the OS; the LS module is the real signal
result.append(Language.PERL)
result.append(LanguageServerId.PERL)
# === 4. Enabled everywhere: every language NOT listed in this function (python, go, java, ...) ===
# === 5. Disabled only on CI (works locally; too unstable/costly on the CI runners) ===
if is_ci:
result.append(Language.KOTLIN) # IntelliJ-based Kotlin LSP crashes on JVM restart under CI memory limits
result.append(LanguageServerId.KOTLIN) # IntelliJ-based Kotlin LSP crashes on JVM restart under CI memory limits
return result
_disabled_languages = _determine_disabled_languages()
_disabled_language_servers = _determine_disabled_language_servers()
def language_tests_enabled(language: Language) -> bool:
def language_server_tests_enabled(ls_id: LanguageServerId) -> bool:
"""
Check if tests for the given language are enabled in the current environment.
Check if tests for the given language server are enabled in the current environment.
:param language: the language to check
:param ls_id: the language server to check
:return: True if tests for the language are enabled, False otherwise
"""
return language not in _disabled_languages
return ls_id not in _disabled_language_servers
def language_supports_implementation(language: Language) -> bool:
def ls_supports_implementation(language: LanguageServerId) -> bool:
return language.supports_implementation_request()
def languages_supporting_implementation(*languages: Language) -> list[Language]:
return [language for language in languages if language_supports_implementation(language)]
def language_servers_supporting_implementation(*languages: LanguageServerId) -> list[LanguageServerId]:
return [language for language in languages if ls_supports_implementation(language)]
_VERIFIED_IMPLEMENTATION_LANGUAGES = {
Language.ANGULAR,
Language.CSHARP,
Language.GO,
Language.JAVA,
Language.RUST,
Language.TYPESCRIPT,
LanguageServerId.ANGULAR,
LanguageServerId.CSHARP,
LanguageServerId.GO,
LanguageServerId.JAVA,
LanguageServerId.RUST,
LanguageServerId.TYPESCRIPT,
}
def language_has_verified_implementation_support(language: Language) -> bool:
def ls_has_verified_implementation_support(language: LanguageServerId) -> bool:
"""
True only for languages where the server advertises implementation support and
the repo fixtures contain a verified working go-to-implementation scenario.
"""
return language in _VERIFIED_IMPLEMENTATION_LANGUAGES and language_supports_implementation(language)
return language in _VERIFIED_IMPLEMENTATION_LANGUAGES and ls_supports_implementation(language)
def find_identifier_position(file_path: Path, identifier: str) -> tuple[int, int] | None:
@@ -5,7 +5,7 @@ from pathlib import Path
from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig
from serena.project import Project
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
def _create_test_project(
@@ -16,7 +16,7 @@ def _create_test_project(
"""Helper to create a Project with the given ignored paths configuration."""
config = ProjectConfig(
project_name="test_project",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
ignored_paths=project_ignored_paths or [],
ignore_all_files_in_gitignore=False,
)
@@ -145,7 +145,7 @@ class TestRegisteredProjectGlobalIgnoredPaths:
"""RegisteredProject.get_project_instance() passes global_ignored_paths to Project."""
config = ProjectConfig(
project_name="test_project",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
ignored_paths=[],
ignore_all_files_in_gitignore=False,
)
@@ -161,7 +161,7 @@ class TestRegisteredProjectGlobalIgnoredPaths:
"""RegisteredProject without global_ignored_paths defaults to empty."""
config = ProjectConfig(
project_name="test_project",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
ignored_paths=[],
ignore_all_files_in_gitignore=False,
)
@@ -193,7 +193,7 @@ class TestRegisteredProjectGlobalIgnoredPaths:
"""RegisteredProject.from_project_instance() threads global_ignored_paths to Project."""
config = ProjectConfig(
project_name="test_project",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
ignored_paths=[],
ignore_all_files_in_gitignore=False,
)
@@ -233,7 +233,7 @@ class TestGlobalIgnoredPathsWithGitignore:
"""Global patterns, project patterns, and .gitignore patterns are all applied together."""
config = ProjectConfig(
project_name="test_project",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
ignored_paths=["build"],
ignore_all_files_in_gitignore=True,
)
+19 -19
View File
@@ -18,7 +18,7 @@ from serena.config.serena_config import (
)
from serena.constants import PROJECT_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME
from serena.project import MemoryManager, Project
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.conftest import create_default_serena_config
@@ -62,7 +62,7 @@ class TestProjectConfigAutogenerate:
# Verify the configuration
assert config.project_name == self.project_path.name
assert config.languages == [Language.PYTHON]
assert config.languages == [LanguageServerId.PYTHON]
def test_autogenerate_with_python_files_and_custom_ls_priorities(self):
"""Test successful autogeneration with Python source files, using custom language server priorities."""
@@ -71,14 +71,14 @@ class TestProjectConfigAutogenerate:
python_file.write_text("def hello():\n print('Hello, world!')\n")
serena_config = deepcopy(self.serena_config)
serena_config.ls_priorities = {Language.PYTHON_TY.value: 3}
serena_config.ls_priorities = {LanguageServerId.PYTHON_TY.value: 3}
# Run autogenerate
config = ProjectConfig.autogenerate(self.project_path, serena_config, save_to_disk=False)
# Verify the configuration
assert config.project_name == self.project_path.name
assert config.languages == [Language.PYTHON_TY]
assert config.languages == [LanguageServerId.PYTHON_TY]
def test_autogenerate_with_js_files(self):
"""Test successful autogeneration with JavaScript source files."""
@@ -88,7 +88,7 @@ class TestProjectConfigAutogenerate:
# Run autogenerate - should pick Python as dominant
config = ProjectConfig.autogenerate(self.project_path, self.serena_config, save_to_disk=False)
assert config.languages == [Language.TYPESCRIPT]
assert config.languages == [LanguageServerId.TYPESCRIPT]
def test_autogenerate_with_multiple_languages(self):
"""Test autogeneration picks dominant language when multiple are present."""
@@ -100,7 +100,7 @@ class TestProjectConfigAutogenerate:
# Run autogenerate - should pick Python as dominant
config = ProjectConfig.autogenerate(self.project_path, self.serena_config, save_to_disk=False)
assert config.languages == [Language.PYTHON]
assert config.languages == [LanguageServerId.PYTHON]
def test_autogenerate_saves_to_disk(self):
"""Test that autogenerate can save the configuration to disk."""
@@ -116,7 +116,7 @@ class TestProjectConfigAutogenerate:
assert config_path.exists()
# Verify the content
assert config.languages == [Language.GO]
assert config.languages == [LanguageServerId.GO]
def test_autogenerate_nonexistent_path(self):
"""Test that autogenerate raises FileNotFoundError for non-existent path."""
@@ -153,7 +153,7 @@ class TestProjectConfigAutogenerate:
config = ProjectConfig.autogenerate(self.project_path, self.serena_config, project_name=custom_name, save_to_disk=False)
assert config.project_name == custom_name
assert config.languages == [Language.TYPESCRIPT]
assert config.languages == [LanguageServerId.TYPESCRIPT]
class TestProjectConfig:
@@ -168,14 +168,14 @@ class TestProjectConfigLanguageBackend:
def test_language_backend_defaults_to_none(self):
config = ProjectConfig(
project_name="test",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
)
assert config.language_backend is None
def test_language_backend_can_be_set(self):
config = ProjectConfig(
project_name="test",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
language_backend=LanguageBackend.JETBRAINS,
)
assert config.language_backend == LanguageBackend.JETBRAINS
@@ -183,7 +183,7 @@ class TestProjectConfigLanguageBackend:
def test_language_backend_roundtrips_through_yaml(self):
config = ProjectConfig(
project_name="test",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
language_backend=LanguageBackend.JETBRAINS,
)
d = config._to_yaml_dict()
@@ -192,7 +192,7 @@ class TestProjectConfigLanguageBackend:
def test_language_backend_none_roundtrips_through_yaml(self):
config = ProjectConfig(
project_name="test",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
)
d = config._to_yaml_dict()
assert d["language_backend"] is None
@@ -232,7 +232,7 @@ def _make_config_with_project(
project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "python" / "test_repo"),
project_config=ProjectConfig(
project_name=project_name,
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
language_backend=language_backend,
),
serena_config=config,
@@ -286,7 +286,7 @@ class TestEffectiveLanguageBackend:
project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "java" / "test_repo"),
project_config=ProjectConfig(
project_name="jb_proj",
languages=[Language.JAVA],
languages=[LanguageServerId.JAVA],
language_backend=LanguageBackend.JETBRAINS,
),
serena_config=config,
@@ -309,7 +309,7 @@ class TestEffectiveLanguageBackend:
project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "python" / "test_repo"),
project_config=ProjectConfig(
project_name="lsp_proj2",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
language_backend=LanguageBackend.LSP,
),
serena_config=config,
@@ -332,7 +332,7 @@ class TestEffectiveLanguageBackend:
project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "python" / "test_repo"),
project_config=ProjectConfig(
project_name="proj2",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
language_backend=None,
),
serena_config=config,
@@ -432,7 +432,7 @@ class TestProjectSerenaDataFolder:
def _make_project(self, serena_config: "SerenaConfig | None" = None) -> Project:
project_config = ProjectConfig(
project_name="myproject",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
)
project = Project(
project_root=str(self.project_path),
@@ -679,11 +679,11 @@ class TestProjectConfigActivationCommand:
return data
def test_activation_command_defaults_to_none(self):
config = ProjectConfig(project_name="test", languages=[Language.PYTHON])
config = ProjectConfig(project_name="test", languages=[LanguageServerId.PYTHON])
assert config.activation_command is None
def test_activation_command_timeout_default(self):
config = ProjectConfig(project_name="test", languages=[Language.PYTHON])
config = ProjectConfig(project_name="test", languages=[LanguageServerId.PYTHON])
assert config.activation_command_timeout == 180.0
def test_activation_command_parsed_from_dict(self):
+2 -2
View File
@@ -8,7 +8,7 @@ import pytest
from serena.config.serena_config import ProjectConfig, SerenaConfig
from serena.project import Project
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
def _make_project(
@@ -25,7 +25,7 @@ def _make_project(
)
project_config = ProjectConfig(
project_name="test-activation",
languages=[Language.PYTHON],
languages=[LanguageServerId.PYTHON],
activation_command=activation_command,
activation_command_timeout=activation_command_timeout,
)
+7 -7
View File
@@ -2,7 +2,7 @@ from collections.abc import Callable
from types import SimpleNamespace
from serena.dashboard import SerenaDashboardAPI
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
class _DummyMemoryLogHandler:
@@ -28,7 +28,7 @@ class _DummyAgent:
return self._project
def _make_dashboard(project_languages: list[Language] | None) -> SerenaDashboardAPI:
def _make_dashboard(project_languages: list[LanguageServerId] | None) -> SerenaDashboardAPI:
project = None
if project_languages is not None:
project = SimpleNamespace(project_config=SimpleNamespace(languages=project_languages))
@@ -39,15 +39,15 @@ def _make_dashboard(project_languages: list[Language] | None) -> SerenaDashboard
def test_available_languages_include_experimental_when_no_active_project():
dashboard = _make_dashboard(project_languages=None)
response = dashboard._get_available_languages()
expected = sorted(lang.value for lang in Language.iter_all(include_experimental=True))
expected = sorted(lang.value for lang in LanguageServerId.iter_all(include_experimental=True))
assert response.languages == expected
def test_available_languages_exclude_project_languages():
dashboard = _make_dashboard(project_languages=[Language.PYTHON, Language.MARKDOWN])
dashboard = _make_dashboard(project_languages=[LanguageServerId.PYTHON, LanguageServerId.MARKDOWN])
response = dashboard._get_available_languages()
available = set(response.languages)
assert Language.PYTHON.value not in available
assert Language.MARKDOWN.value not in available
assert LanguageServerId.PYTHON.value not in available
assert LanguageServerId.MARKDOWN.value not in available
# ensure experimental languages remain available for selection
assert Language.ANSIBLE.value in available
assert LanguageServerId.ANSIBLE.value in available
+3 -3
View File
@@ -14,7 +14,7 @@ import pytest
from serena.agent import SerenaAgent
from serena.project import Project
from serena.tools import FindReferencingSymbolsTool
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.conftest import agent_for_project_context, get_repo_path
pytestmark = pytest.mark.python
@@ -73,10 +73,10 @@ class FileSystemSyncTestCase:
def run(self, tmp_path):
# Work on an isolated copy so we can freely create/edit/delete files under the project root.
repo_root = tmp_path / "repo"
shutil.copytree(get_repo_path(Language.PYTHON), repo_root)
shutil.copytree(get_repo_path(LanguageServerId.PYTHON), repo_root)
caller_abs = repo_root / self._CALLER_REL_PATH
with agent_for_project_context(Language.PYTHON, str(repo_root)) as agent:
with agent_for_project_context(LanguageServerId.PYTHON, str(repo_root)) as agent:
project = agent.get_active_project_or_raise()
# Warm the reference index, then establish the freshness baseline (first poll never notifies).
+126 -121
View File
@@ -32,24 +32,24 @@ from serena.tools import (
SafeDeleteSymbol,
Tool,
)
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from test.conftest import (
find_identifier_pos,
get_pytest_markers,
get_repo_path,
language_tests_enabled,
language_server_tests_enabled,
)
from test.serena.config.test_context_mode import GROK_EXCLUDED_TOOLS
@dataclass
class BaseCase:
language: Language
ls_id: LanguageServerId
id: str
def to_pytest_param(self, *marks: MarkDecorator | Mark) -> ParameterSet:
return pytest.param(self.language, self, marks=[*get_pytest_markers(self.language), *marks], id=self.id)
return pytest.param(self.ls_id, self, marks=[*get_pytest_markers(self.ls_id), *marks], id=self.id)
@dataclass
@@ -139,7 +139,6 @@ class SafeDeleteCase(BaseCase):
@dataclass
class DiagnosticCase(BaseCase):
language: Language
relative_path: str
name_path1: str
name_path2: str | None
@@ -189,8 +188,8 @@ class DiagnosticCase(BaseCase):
DIAGNOSTIC_CASES = [
DiagnosticCase(
language=Language.PYTHON,
id=f"{Language.PYTHON.value}_missing_user",
ls_id=LanguageServerId.PYTHON,
id=f"{LanguageServerId.PYTHON.value}_missing_user",
relative_path=os.path.join("test_repo", "diagnostics_sample.py"),
name_path1="broken_factory",
name_path2="broken_consumer",
@@ -198,8 +197,8 @@ DIAGNOSTIC_CASES = [
message_fragment2="undefined_name",
).to_pytest_param(),
DiagnosticCase(
language=Language.CLOJURE,
id=f"{Language.CLOJURE.value}_missing-greeting",
ls_id=LanguageServerId.CLOJURE,
id=f"{LanguageServerId.CLOJURE.value}_missing-greeting",
relative_path=os.path.join("src", "test_app", "diagnostics_sample.clj"),
name_path1="broken-factory",
name_path2="broken-consumer",
@@ -207,8 +206,8 @@ DIAGNOSTIC_CASES = [
message_fragment2="missing-consumer-value",
).to_pytest_param(),
DiagnosticCase(
language=Language.GO,
id=f"{Language.GO.value}_missingGreeting",
ls_id=LanguageServerId.GO,
id=f"{LanguageServerId.GO.value}_missingGreeting",
relative_path="diagnostics_sample.go",
name_path1="brokenFactory",
name_path2="brokenConsumer",
@@ -216,8 +215,8 @@ DIAGNOSTIC_CASES = [
message_fragment2="missingConsumerValue",
).to_pytest_param(),
DiagnosticCase(
language=Language.TYPESCRIPT,
id=f"{Language.TYPESCRIPT.value}_missingGreeting",
ls_id=LanguageServerId.TYPESCRIPT,
id=f"{LanguageServerId.TYPESCRIPT.value}_missingGreeting",
relative_path="diagnostics_sample.ts",
name_path1="brokenFactory",
name_path2="brokenConsumer",
@@ -229,7 +228,7 @@ DIAGNOSTIC_CASES = [
FIND_DEFINING_SYMBOL_CASES = [
FindDefiningSymbolCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="python_user_in_services",
relative_path=os.path.join("test_repo", "services.py"),
identifier="User",
@@ -239,7 +238,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="models.py",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.PYTHON_TY,
ls_id=LanguageServerId.PYTHON_TY,
id="python_ty_user_in_services",
relative_path=os.path.join("test_repo", "services.py"),
identifier="User",
@@ -249,7 +248,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="models.py",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.GO,
ls_id=LanguageServerId.GO,
id="go_helper_in_main",
relative_path="main.go",
identifier="Helper",
@@ -259,7 +258,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="main.go",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.JAVA,
ls_id=LanguageServerId.JAVA,
id="java_model_in_main",
relative_path=os.path.join("src", "main", "java", "test_repo", "Main.java"),
identifier="Model",
@@ -269,7 +268,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="Model.java",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.KOTLIN,
ls_id=LanguageServerId.KOTLIN,
id="kotlin_model_in_main",
relative_path=os.path.join("src", "main", "kotlin", "test_repo", "Main.kt"),
identifier="Model",
@@ -279,7 +278,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="Model.kt",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.RUST,
ls_id=LanguageServerId.RUST,
id="rust_format_greeting",
relative_path=os.path.join("src", "main.rs"),
identifier="format_greeting",
@@ -289,7 +288,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="lib.rs",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.PHP,
ls_id=LanguageServerId.PHP,
id="php_helper_function",
relative_path="index.php",
identifier="helperFunction",
@@ -299,7 +298,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="helper.php",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.CLOJURE,
ls_id=LanguageServerId.CLOJURE,
id="clojure_multiply_in_utils",
relative_path=os.path.join("src", "test_app", "utils.clj"),
identifier="multiply",
@@ -309,7 +308,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file=os.path.join("src", "test_app", "core.clj"),
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.CSHARP,
ls_id=LanguageServerId.CSHARP,
id="csharp_add_in_program",
relative_path="Program.cs",
identifier="Add",
@@ -319,7 +318,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="Program.cs",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.POWERSHELL,
ls_id=LanguageServerId.POWERSHELL,
id="powershell_convert_to_uppercase",
relative_path="main.ps1",
identifier="Convert-ToUpperCase",
@@ -329,7 +328,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="utils.ps1",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.CPP,
ls_id=LanguageServerId.CPP,
id="cpp_add_in_a",
relative_path="a.cpp",
identifier="add",
@@ -339,7 +338,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="b.cpp",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.LEAN4,
ls_id=LanguageServerId.LEAN4,
id="lean_add_in_main",
relative_path="Main.lean",
identifier="add",
@@ -349,7 +348,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="Helper.lean",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.TYPESCRIPT,
ls_id=LanguageServerId.TYPESCRIPT,
id="typescript_helper_function",
relative_path="index.ts",
identifier="helperFunction",
@@ -359,7 +358,7 @@ FIND_DEFINING_SYMBOL_CASES = [
expected_definition_file="index.ts",
).to_pytest_param(),
FindDefiningSymbolCase(
language=Language.FSHARP,
ls_id=LanguageServerId.FSHARP,
id="fsharp_add_in_program",
relative_path="Program.fs",
identifier="add",
@@ -374,7 +373,7 @@ FIND_DEFINING_SYMBOL_CASES = [
FIND_DEFINING_SYMBOL_REGEX_CASES = [
RegexDefiningSymbolCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="python_import_user",
relative_path=os.path.join("test_repo", "services.py"),
regex=r"from \.models import Item, (User)",
@@ -383,7 +382,7 @@ FIND_DEFINING_SYMBOL_REGEX_CASES = [
expected_definition_file="models.py",
).to_pytest_param(),
RegexDefiningSymbolCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="python_create_user_call",
relative_path=os.path.join("test_repo", "services.py"),
regex=r"=\s+(User)\(",
@@ -392,7 +391,7 @@ FIND_DEFINING_SYMBOL_REGEX_CASES = [
expected_definition_file="models.py",
).to_pytest_param(),
RegexDefiningSymbolCase(
language=Language.PYTHON_TY,
ls_id=LanguageServerId.PYTHON_TY,
id="python_ty_create_user_call",
relative_path=os.path.join("test_repo", "services.py"),
regex=r"=\s+(User)\(",
@@ -401,7 +400,7 @@ FIND_DEFINING_SYMBOL_REGEX_CASES = [
expected_definition_file="models.py",
).to_pytest_param(),
RegexDefiningSymbolCase(
language=Language.GO,
ls_id=LanguageServerId.GO,
id="go_greeter_var",
relative_path="main.go",
regex=r"var greeter (Greeter) =",
@@ -413,7 +412,7 @@ FIND_DEFINING_SYMBOL_REGEX_CASES = [
FIND_IMPLEMENTATION_CASES = [
FindImplementationCase(
language=Language.CSHARP,
ls_id=LanguageServerId.CSHARP,
id="csharp_greeter_format",
symbol_name="IGreeter/FormatGreeting",
definition_file=os.path.join("Services", "IGreeter.cs"),
@@ -421,7 +420,7 @@ FIND_IMPLEMENTATION_CASES = [
expected_symbol_name="FormatGreeting",
).to_pytest_param(),
FindImplementationCase(
language=Language.GO,
ls_id=LanguageServerId.GO,
id="go_greeter_format",
symbol_name="Greeter/FormatGreeting",
definition_file="main.go",
@@ -429,7 +428,7 @@ FIND_IMPLEMENTATION_CASES = [
expected_symbol_name="FormatGreeting",
).to_pytest_param(),
FindImplementationCase(
language=Language.JAVA,
ls_id=LanguageServerId.JAVA,
id="java_greeter_format",
symbol_name="Greeter/formatGreeting",
definition_file=os.path.join("src", "main", "java", "test_repo", "Greeter.java"),
@@ -437,7 +436,7 @@ FIND_IMPLEMENTATION_CASES = [
expected_symbol_name="formatGreeting",
).to_pytest_param(),
FindImplementationCase(
language=Language.RUST,
ls_id=LanguageServerId.RUST,
id="rust_greeter_format",
symbol_name="Greeter/format_greeting",
definition_file=os.path.join("src", "lib.rs"),
@@ -445,7 +444,7 @@ FIND_IMPLEMENTATION_CASES = [
expected_symbol_name="format_greeting",
).to_pytest_param(),
FindImplementationCase(
language=Language.TYPESCRIPT,
ls_id=LanguageServerId.TYPESCRIPT,
id="typescript_greeter_format",
symbol_name="Greeter/formatGreeting",
definition_file="formatters.ts",
@@ -457,153 +456,157 @@ FIND_IMPLEMENTATION_CASES = [
FIND_SYMBOL_REFERENCES_CASES = [
FindSymbolCase(
language=Language.PYTHON, id="python_user_class", symbol_name="User", expected_kind="Class", expected_file="models.py"
ls_id=LanguageServerId.PYTHON, id="python_user_class", symbol_name="User", expected_kind="Class", expected_file="models.py"
).to_pytest_param(),
FindSymbolCase(
language=Language.GO, id="go_helper_function", symbol_name="Helper", expected_kind="Function", expected_file="main.go"
ls_id=LanguageServerId.GO, id="go_helper_function", symbol_name="Helper", expected_kind="Function", expected_file="main.go"
).to_pytest_param(),
FindSymbolCase(
language=Language.JAVA, id="java_model_class", symbol_name="Model", expected_kind="Class", expected_file="Model.java"
ls_id=LanguageServerId.JAVA, id="java_model_class", symbol_name="Model", expected_kind="Class", expected_file="Model.java"
).to_pytest_param(),
FindSymbolCase(
language=Language.KOTLIN, id="kotlin_model_struct", symbol_name="Model", expected_kind="Struct", expected_file="Model.kt"
ls_id=LanguageServerId.KOTLIN, id="kotlin_model_struct", symbol_name="Model", expected_kind="Struct", expected_file="Model.kt"
).to_pytest_param(),
FindSymbolCase(
language=Language.TYPESCRIPT,
ls_id=LanguageServerId.TYPESCRIPT,
id="typescript_demo_class",
symbol_name="DemoClass",
expected_kind="Class",
expected_file="index.ts",
).to_pytest_param(),
FindSymbolCase(
language=Language.PHP,
ls_id=LanguageServerId.PHP,
id="php_helper_function",
symbol_name="helperFunction",
expected_kind="Function",
expected_file="helper.php",
).to_pytest_param(),
FindSymbolCase(
language=Language.CLOJURE,
ls_id=LanguageServerId.CLOJURE,
id="clojure_greet_function",
symbol_name="greet",
expected_kind="Function",
expected_file=os.path.join("src", "test_app", "core.clj"),
).to_pytest_param(),
FindSymbolCase(
language=Language.CSHARP,
ls_id=LanguageServerId.CSHARP,
id="csharp_calculator_class",
symbol_name="Calculator",
expected_kind="Class",
expected_file="Program.cs",
).to_pytest_param(),
FindSymbolCase(
language=Language.POWERSHELL,
ls_id=LanguageServerId.POWERSHELL,
id="powershell_greet_user",
symbol_name="Greet-User",
expected_kind="Function",
expected_file="main.ps1",
).to_pytest_param(),
FindSymbolCase(
language=Language.CPP, id="cpp_add_function", symbol_name="add", expected_kind="Function", expected_file="b.cpp"
ls_id=LanguageServerId.CPP, id="cpp_add_function", symbol_name="add", expected_kind="Function", expected_file="b.cpp"
).to_pytest_param(),
FindSymbolCase(
language=Language.LEAN4, id="lean_add_method", symbol_name="add", expected_kind="Method", expected_file="Helper.lean"
ls_id=LanguageServerId.LEAN4, id="lean_add_method", symbol_name="add", expected_kind="Method", expected_file="Helper.lean"
).to_pytest_param(),
FindSymbolCase(
language=Language.FSHARP,
ls_id=LanguageServerId.FSHARP,
id="fsharp_calculator_module",
symbol_name="Calculator",
expected_kind="Module",
expected_file="Calculator.fs",
).to_pytest_param(pytest.mark.xfail(reason="F# language server is unreliable")),
FindSymbolCase(
language=Language.RUST, id="rust_add_function", symbol_name="add", expected_kind="Function", expected_file="lib.rs"
ls_id=LanguageServerId.RUST, id="rust_add_function", symbol_name="add", expected_kind="Function", expected_file="lib.rs"
).to_pytest_param(),
FindSymbolCase(
language=Language.LATEX, id="latex_methods_section", symbol_name="Methods", expected_kind="Module", expected_file="main.tex"
ls_id=LanguageServerId.LATEX, id="latex_methods_section", symbol_name="Methods", expected_kind="Module", expected_file="main.tex"
).to_pytest_param(),
]
FIND_REFERENCE_CASES = [
FindReferenceCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="python_user_refs",
symbol_name="User",
definition_file=os.path.join("test_repo", "models.py"),
reference_file=os.path.join("test_repo", "services.py"),
).to_pytest_param(),
FindReferenceCase(
language=Language.GO, id="go_helper_refs", symbol_name="Helper", definition_file="main.go", reference_file="main.go"
ls_id=LanguageServerId.GO, id="go_helper_refs", symbol_name="Helper", definition_file="main.go", reference_file="main.go"
).to_pytest_param(),
FindReferenceCase(
language=Language.JAVA,
ls_id=LanguageServerId.JAVA,
id="java_model_refs",
symbol_name="Model",
definition_file=os.path.join("src", "main", "java", "test_repo", "Model.java"),
reference_file=os.path.join("src", "main", "java", "test_repo", "Main.java"),
).to_pytest_param(),
FindReferenceCase(
language=Language.KOTLIN,
ls_id=LanguageServerId.KOTLIN,
id="kotlin_model_refs",
symbol_name="Model",
definition_file=os.path.join("src", "main", "kotlin", "test_repo", "Model.kt"),
reference_file=os.path.join("src", "main", "kotlin", "test_repo", "Main.kt"),
).to_pytest_param(),
FindReferenceCase(
language=Language.RUST,
ls_id=LanguageServerId.RUST,
id="rust_add_refs",
symbol_name="add",
definition_file=os.path.join("src", "lib.rs"),
reference_file=os.path.join("src", "main.rs"),
).to_pytest_param(),
FindReferenceCase(
language=Language.PHP,
ls_id=LanguageServerId.PHP,
id="php_helper_refs",
symbol_name="helperFunction",
definition_file="helper.php",
reference_file="index.php",
).to_pytest_param(),
FindReferenceCase(
language=Language.CLOJURE,
ls_id=LanguageServerId.CLOJURE,
id="clojure_multiply_refs",
symbol_name="multiply",
definition_file=os.path.join("src", "test_app", "core.clj"),
reference_file=os.path.join("src", "test_app", "utils.clj"),
).to_pytest_param(),
FindReferenceCase(
language=Language.CSHARP,
ls_id=LanguageServerId.CSHARP,
id="csharp_calculator_refs",
symbol_name="Calculator",
definition_file="Program.cs",
reference_file="Program.cs",
).to_pytest_param(),
FindReferenceCase(
language=Language.POWERSHELL,
ls_id=LanguageServerId.POWERSHELL,
id="powershell_greet_user_refs",
symbol_name="Greet-User",
definition_file="main.ps1",
reference_file="main.ps1",
).to_pytest_param(),
FindReferenceCase(
language=Language.CPP, id="cpp_add_refs", symbol_name="add", definition_file="b.cpp", reference_file="a.cpp"
ls_id=LanguageServerId.CPP, id="cpp_add_refs", symbol_name="add", definition_file="b.cpp", reference_file="a.cpp"
).to_pytest_param(),
FindReferenceCase(
language=Language.LEAN4, id="lean_add_refs", symbol_name="add", definition_file="Helper.lean", reference_file="Main.lean"
ls_id=LanguageServerId.LEAN4, id="lean_add_refs", symbol_name="add", definition_file="Helper.lean", reference_file="Main.lean"
).to_pytest_param(),
FindReferenceCase(
language=Language.TYPESCRIPT,
ls_id=LanguageServerId.TYPESCRIPT,
id="typescript_helper_refs",
symbol_name="helperFunction",
definition_file="index.ts",
reference_file="use_helper.ts",
).to_pytest_param(pytest.mark.xfail(False, reason="TypeScript language server is unreliable")),
FindReferenceCase(
language=Language.FSHARP, id="fsharp_add_refs", symbol_name="add", definition_file="Calculator.fs", reference_file="Program.fs"
ls_id=LanguageServerId.FSHARP,
id="fsharp_add_refs",
symbol_name="add",
definition_file="Calculator.fs",
reference_file="Program.fs",
).to_pytest_param(
pytest.mark.xfail(reason="F# language server is unreliable"), # See issue #1040
),
FindReferenceCase(
language=Language.LATEX,
ls_id=LanguageServerId.LATEX,
id="latex_background_refs",
symbol_name="Background",
definition_file="sections/background.tex",
@@ -613,7 +616,7 @@ FIND_REFERENCE_CASES = [
FIND_DEFINING_SYMBOL_REGEX_ERROR_CASES = [
RegexDefiningSymbolErrorCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="python_regex_multiple_matches",
relative_path=os.path.join("test_repo", "services.py"),
regex=r"(User)",
@@ -621,7 +624,7 @@ FIND_DEFINING_SYMBOL_REGEX_ERROR_CASES = [
error_fragment="Match must be unique",
).to_pytest_param(),
RegexDefiningSymbolErrorCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="python_regex_missing_group",
relative_path=os.path.join("test_repo", "services.py"),
regex=r"self.users.get\(id\)",
@@ -632,7 +635,7 @@ FIND_DEFINING_SYMBOL_REGEX_ERROR_CASES = [
FIND_SYMBOL_NAME_PATH_CASES = [
FindSymbolNamePathCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="nested_class_exact",
name_path="OuterClass/NestedClass",
substring_matching=False,
@@ -641,7 +644,7 @@ FIND_SYMBOL_NAME_PATH_CASES = [
expected_file=os.path.join("test_repo", "nested.py"),
).to_pytest_param(),
FindSymbolNamePathCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="nested_method_exact",
name_path="OuterClass/NestedClass/find_me",
substring_matching=False,
@@ -650,7 +653,7 @@ FIND_SYMBOL_NAME_PATH_CASES = [
expected_file=os.path.join("test_repo", "nested.py"),
).to_pytest_param(),
FindSymbolNamePathCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="nested_class_substring",
name_path="OuterClass/NestedCl",
substring_matching=True,
@@ -659,7 +662,7 @@ FIND_SYMBOL_NAME_PATH_CASES = [
expected_file=os.path.join("test_repo", "nested.py"),
).to_pytest_param(),
FindSymbolNamePathCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="nested_method_substring",
name_path="OuterClass/NestedClass/find_m",
substring_matching=True,
@@ -668,7 +671,7 @@ FIND_SYMBOL_NAME_PATH_CASES = [
expected_file=os.path.join("test_repo", "nested.py"),
).to_pytest_param(),
FindSymbolNamePathCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="outer_class_absolute",
name_path="/OuterClass",
substring_matching=False,
@@ -677,7 +680,7 @@ FIND_SYMBOL_NAME_PATH_CASES = [
expected_file=os.path.join("test_repo", "nested.py"),
).to_pytest_param(),
FindSymbolNamePathCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="nested_method_absolute_substring",
name_path="/OuterClass/NestedClass/find_m",
substring_matching=True,
@@ -688,21 +691,21 @@ FIND_SYMBOL_NAME_PATH_CASES = [
]
FIND_SYMBOL_NAME_PATH_NO_MATCH_CASES = [
FindSymbolNoMatchCase(language=Language.PYTHON, id="nested_class_not_top_level", name_path="/NestedClass").to_pytest_param(),
FindSymbolNoMatchCase(ls_id=LanguageServerId.PYTHON, id="nested_class_not_top_level", name_path="/NestedClass").to_pytest_param(),
FindSymbolNoMatchCase(
language=Language.PYTHON, id="nested_class_missing_parent", name_path="/NoSuchParent/NestedClass"
ls_id=LanguageServerId.PYTHON, id="nested_class_missing_parent", name_path="/NoSuchParent/NestedClass"
).to_pytest_param(),
]
FIND_SYMBOL_OVERLOADED_FUNCTION_CASES = [
FindSymbolOverloadedCase(
language=Language.JAVA, id="java_overloaded_get_name", name_path="Model/getName", num_expected=2
ls_id=LanguageServerId.JAVA, id="java_overloaded_get_name", name_path="Model/getName", num_expected=2
).to_pytest_param(),
]
NON_UNIQUE_SYMBOL_REFERENCE_ERROR_CASES = [
NonUniqueSymbolReferenceCase(
language=Language.JAVA,
ls_id=LanguageServerId.JAVA,
id="java_overloaded_get_name",
name_path="Model/getName",
relative_path=os.path.join("src", "main", "java", "test_repo", "Model.java"),
@@ -711,25 +714,25 @@ NON_UNIQUE_SYMBOL_REFERENCE_ERROR_CASES = [
SAFE_DELETE_BLOCKED_CASES = [
SafeDeleteCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="python_user",
name_path="User",
relative_path=os.path.join("test_repo", "models.py"),
).to_pytest_param(),
SafeDeleteCase(
language=Language.JAVA,
ls_id=LanguageServerId.JAVA,
id="java_model",
name_path="Model",
relative_path=os.path.join("src", "main", "java", "test_repo", "Model.java"),
).to_pytest_param(),
SafeDeleteCase(
language=Language.KOTLIN,
ls_id=LanguageServerId.KOTLIN,
id="kotlin_model",
name_path="Model",
relative_path=os.path.join("src", "main", "kotlin", "test_repo", "Model.kt"),
).to_pytest_param(),
SafeDeleteCase(
language=Language.TYPESCRIPT,
ls_id=LanguageServerId.TYPESCRIPT,
id="typescript_helper_function",
name_path="helperFunction",
relative_path="index.ts",
@@ -738,25 +741,25 @@ SAFE_DELETE_BLOCKED_CASES = [
SAFE_DELETE_SUCCEEDS_CASES = [
SafeDeleteCase(
language=Language.PYTHON,
ls_id=LanguageServerId.PYTHON,
id="python_timer",
name_path="Timer",
relative_path=os.path.join("test_repo", "utils.py"),
).to_pytest_param(),
SafeDeleteCase(
language=Language.JAVA,
ls_id=LanguageServerId.JAVA,
id="java_model_user",
name_path="ModelUser",
relative_path=os.path.join("src", "main", "java", "test_repo", "ModelUser.java"),
).to_pytest_param(),
SafeDeleteCase(
language=Language.KOTLIN,
ls_id=LanguageServerId.KOTLIN,
id="kotlin_model_user",
name_path="ModelUser",
relative_path=os.path.join("src", "main", "kotlin", "test_repo", "ModelUser.kt"),
).to_pytest_param(),
SafeDeleteCase(
language=Language.TYPESCRIPT,
ls_id=LanguageServerId.TYPESCRIPT,
id="typescript_unused_standalone_function",
name_path="unusedStandaloneFunction",
relative_path="index.ts",
@@ -771,23 +774,23 @@ def serena_config():
# Create test projects for all supported languages
test_projects = []
for language in [
Language.PYTHON,
Language.PYTHON_TY,
Language.GO,
Language.JAVA,
Language.KOTLIN,
Language.RUST,
Language.TYPESCRIPT,
Language.PHP,
Language.CSHARP,
Language.CLOJURE,
Language.FSHARP,
Language.POWERSHELL,
Language.CPP,
Language.HAXE,
Language.LEAN4,
Language.MSL,
Language.LATEX,
LanguageServerId.PYTHON,
LanguageServerId.PYTHON_TY,
LanguageServerId.GO,
LanguageServerId.JAVA,
LanguageServerId.KOTLIN,
LanguageServerId.RUST,
LanguageServerId.TYPESCRIPT,
LanguageServerId.PHP,
LanguageServerId.CSHARP,
LanguageServerId.CLOJURE,
LanguageServerId.FSHARP,
LanguageServerId.POWERSHELL,
LanguageServerId.CPP,
LanguageServerId.HAXE,
LanguageServerId.LEAN4,
LanguageServerId.MSL,
LanguageServerId.LATEX,
]:
repo_path = get_repo_path(language)
if repo_path.exists():
@@ -845,8 +848,8 @@ def project_file_modification_context(serena_agent: SerenaAgent, relative_path:
@pytest.fixture
def serena_agent(request: pytest.FixtureRequest, serena_config) -> Iterator[SerenaAgent]:
language = Language(request.param)
if not language_tests_enabled(language):
language = LanguageServerId(request.param)
if not language_server_tests_enabled(language):
pytest.skip(f"Tests for language {language} are not enabled.")
project_name = f"test_repo_{language}"
@@ -865,7 +868,7 @@ def serena_agent(request: pytest.FixtureRequest, serena_config) -> Iterator[Sere
class TestSerenaAgent:
@pytest.mark.parametrize(
"project",
[None, str(get_repo_path(Language.PYTHON)), "non_existent_path"],
[None, str(get_repo_path(LanguageServerId.PYTHON)), "non_existent_path"],
ids=["no_project", "python_project_path", "invalid_project_path"],
)
def test_agent_instantiation(self, project: str | None):
@@ -880,7 +883,7 @@ class TestSerenaAgent:
SerenaAgent(project=project, serena_config=serena_config)
@pytest.mark.python
@pytest.mark.skipif(not language_tests_enabled(Language.PYTHON), reason="python tests are disabled in this environment")
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.PYTHON), reason="python tests are disabled in this environment")
def test_grok_context_restricts_toolset_and_prompt(self, serena_config):
agent = SerenaAgent(
project="test_repo_python",
@@ -911,7 +914,7 @@ class TestSerenaAgent:
symbol: dict,
expected_name: str | None = None,
) -> None:
if serena_agent.get_active_lsp_languages() == [Language.KOTLIN]:
if serena_agent.get_active_language_server_ids() == [LanguageServerId.KOTLIN]:
# kotlin LS doesn't seem to provide hover info right now, at least for the struct we test this on
return
@@ -924,12 +927,12 @@ class TestSerenaAgent:
if expected_name is not None:
assert expected_name in symbol_info, (
f"[{serena_agent.get_active_lsp_languages()[0]}] Expected symbol info to contain symbol name "
f"[{serena_agent.get_active_language_server_ids()[0]}] Expected symbol info to contain symbol name "
f"{expected_name}. Info: {symbol_info}"
)
# special additional test for Java, since Eclipse returns hover in a complex format and we want to make sure to get it right
if symbol["kind"] == SymbolKind.Class.name and serena_agent.get_active_lsp_languages() == [Language.JAVA]:
if symbol["kind"] == SymbolKind.Class.name and serena_agent.get_active_language_server_ids() == [LanguageServerId.JAVA]:
assert "A simple model class" in symbol_info, f"Java class docstring not found in symbol info: {symbol}"
@pytest.mark.parametrize("serena_agent,case", FIND_SYMBOL_REFERENCES_CASES, indirect=["serena_agent"])
@@ -1030,7 +1033,7 @@ class TestSerenaAgent:
diagnostic_case.assert_matches(full_file_diagnostics)
# testing diagnostics in range by removing second symbol
project_root = get_repo_path(diagnostic_case.language)
project_root = get_repo_path(diagnostic_case.ls_id)
pos1 = find_identifier_pos(project_root / diagnostic_case.relative_path, diagnostic_case.symbol1_id_str)
pos2 = find_identifier_pos(project_root / diagnostic_case.relative_path, cast(str, diagnostic_case.symbol2_id_str))
assert pos1 is not None
@@ -1141,7 +1144,7 @@ class TestSerenaAgent:
@pytest.mark.parametrize(
"serena_agent",
[
pytest.param(Language.TYPESCRIPT, marks=get_pytest_markers(Language.TYPESCRIPT), id="typescript_unique_regex"),
pytest.param(LanguageServerId.TYPESCRIPT, marks=get_pytest_markers(LanguageServerId.TYPESCRIPT), id="typescript_unique_regex"),
],
indirect=["serena_agent"],
)
@@ -1163,7 +1166,7 @@ class TestSerenaAgent:
@pytest.mark.parametrize(
"serena_agent",
[
pytest.param(Language.TYPESCRIPT, marks=get_pytest_markers(Language.TYPESCRIPT), id="typescript_backslashes"),
pytest.param(LanguageServerId.TYPESCRIPT, marks=get_pytest_markers(LanguageServerId.TYPESCRIPT), id="typescript_backslashes"),
],
indirect=["serena_agent"],
)
@@ -1191,7 +1194,7 @@ class TestSerenaAgent:
@pytest.mark.parametrize(
"serena_agent",
[
pytest.param(Language.PYTHON, marks=get_pytest_markers(Language.PYTHON), id="python_replace_in_files"),
pytest.param(LanguageServerId.PYTHON, marks=get_pytest_markers(LanguageServerId.PYTHON), id="python_replace_in_files"),
],
indirect=["serena_agent"],
)
@@ -1221,7 +1224,7 @@ class TestSerenaAgent:
@pytest.mark.parametrize(
"serena_agent",
[
pytest.param(Language.PYTHON, marks=get_pytest_markers(Language.PYTHON), id="python_replace_in_files_guard"),
pytest.param(LanguageServerId.PYTHON, marks=get_pytest_markers(LanguageServerId.PYTHON), id="python_replace_in_files_guard"),
],
indirect=["serena_agent"],
)
@@ -1242,8 +1245,8 @@ class TestSerenaAgent:
@pytest.mark.parametrize(
"serena_agent",
[
pytest.param(Language.PYTHON, marks=get_pytest_markers(Language.PYTHON), id="python_services"),
pytest.param(Language.PYTHON_TY, marks=get_pytest_markers(Language.PYTHON_TY), id="python_ty_services"),
pytest.param(LanguageServerId.PYTHON, marks=get_pytest_markers(LanguageServerId.PYTHON), id="python_services"),
pytest.param(LanguageServerId.PYTHON_TY, marks=get_pytest_markers(LanguageServerId.PYTHON_TY), id="python_ty_services"),
],
indirect=["serena_agent"],
)
@@ -1273,8 +1276,8 @@ class TestSerenaAgent:
@pytest.mark.parametrize(
"serena_agent",
[
pytest.param(Language.PYTHON, marks=get_pytest_markers(Language.PYTHON), id="python_container_body"),
pytest.param(Language.PYTHON_TY, marks=get_pytest_markers(Language.PYTHON_TY), id="python_ty_container_body"),
pytest.param(LanguageServerId.PYTHON, marks=get_pytest_markers(LanguageServerId.PYTHON), id="python_container_body"),
pytest.param(LanguageServerId.PYTHON_TY, marks=get_pytest_markers(LanguageServerId.PYTHON_TY), id="python_ty_container_body"),
],
indirect=["serena_agent"],
)
@@ -1306,7 +1309,9 @@ class TestSerenaAgent:
@pytest.mark.parametrize(
"serena_agent",
[
pytest.param(Language.TYPESCRIPT, marks=get_pytest_markers(Language.TYPESCRIPT), id="typescript_ambiguous_regex"),
pytest.param(
LanguageServerId.TYPESCRIPT, marks=get_pytest_markers(LanguageServerId.TYPESCRIPT), id="typescript_ambiguous_regex"
),
],
indirect=["serena_agent"],
)
@@ -1375,7 +1380,7 @@ class TestPromptProvision:
else:
assert match is None, f"Expected no project activation message in result:\n{result}"
@pytest.mark.parametrize("serena_agent", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("serena_agent", [LanguageServerId.PYTHON], indirect=True)
def test_initial_instructions_provide_project_activation_message_once_per_session(self, serena_agent: SerenaAgent) -> None:
"""
Tests that the project activation message is provided on the first call to InitialInstructionsTool for a session,
@@ -1394,7 +1399,7 @@ class TestPromptProvision:
result3 = self._call_tool(serena_agent, InitialInstructionsTool, session_id=session1)
self._assert_activation_message(result3, project_name, present=False)
@pytest.mark.parametrize("serena_agent", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("serena_agent", [LanguageServerId.PYTHON], indirect=True)
def test_dynamically_activated_mode_is_provided_once_per_session(self, serena_agent: SerenaAgent) -> None:
"""
Tests that when a new project is activated within a session that has a different mode configuration (e.g. no-onboarding),
@@ -1437,7 +1442,7 @@ class TestPromptProvision:
# the initial instructions for the new session must also include the activation message for the project
self._assert_activation_message(result4, project_name2, present=True)
@pytest.mark.parametrize("serena_agent", [Language.PYTHON], indirect=True)
@pytest.mark.parametrize("serena_agent", [LanguageServerId.PYTHON], indirect=True)
def test_activate_project_tool_always_returns_activation_message(self, serena_agent: SerenaAgent) -> None:
project_name = "test_repo_python"
session = "session1"
+42 -42
View File
@@ -22,9 +22,9 @@ from overrides import overrides
from syrupy import SnapshotAssertion
from serena.code_editor import CodeEditor, LanguageServerCodeEditor
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from src.serena.symbol import LanguageServerSymbolRetriever
from test.conftest import get_repo_path, language_tests_enabled, project_with_ls_context
from test.conftest import get_repo_path, language_server_tests_enabled, project_with_ls_context
pytestmark = pytest.mark.snapshot
@@ -177,14 +177,14 @@ class CodeDiff:
class EditingTest(ABC):
def __init__(self, language: Language, rel_path: str):
def __init__(self, ls_id: LanguageServerId, rel_path: str):
"""
:param language: the language
:param ls_id: the language server to use
:param rel_path: the relative path of the edited file
"""
self.rel_path = rel_path
self.language = language
self.original_repo_path = get_repo_path(language)
self.ls_id = ls_id
self.original_repo_path = get_repo_path(ls_id)
self.repo_path: Path | None = None
@contextmanager
@@ -199,8 +199,8 @@ class EditingTest(ABC):
# wait for a long time here
if os.name == "nt":
time.sleep(0.1)
log.info(f"Creating language server for {self.language} {self.rel_path}")
with project_with_ls_context(self.language, str(self.repo_path)) as project:
log.info(f"Creating language server for {self.ls_id} {self.rel_path}")
with project_with_ls_context(self.ls_id, str(self.repo_path)) as project:
yield LanguageServerSymbolRetriever(project)
finally:
# prevent deadlock on Windows due to lingering file locks
@@ -244,8 +244,8 @@ TYPESCRIPT_TEST_FILE = "index.ts"
class DeleteSymbolTest(EditingTest):
def __init__(self, language: Language, rel_path: str, deleted_symbol: str):
super().__init__(language, rel_path)
def __init__(self, ls_id: LanguageServerId, rel_path: str, deleted_symbol: str):
super().__init__(ls_id, rel_path)
self.deleted_symbol = deleted_symbol
self.rel_path = rel_path
@@ -258,7 +258,7 @@ class DeleteSymbolTest(EditingTest):
[
pytest.param(
DeleteSymbolTest(
Language.PYTHON,
LanguageServerId.PYTHON,
PYTHON_TEST_REL_FILE_PATH,
"VariableContainer",
),
@@ -266,7 +266,7 @@ class DeleteSymbolTest(EditingTest):
),
pytest.param(
DeleteSymbolTest(
Language.TYPESCRIPT,
LanguageServerId.TYPESCRIPT,
TYPESCRIPT_TEST_FILE,
"DemoClass",
),
@@ -307,9 +307,9 @@ NEW_TYPESCRIPT_FUNCTION_AFTER = """function newFunctionAfterClass(): void {
class InsertInRelToSymbolTest(EditingTest):
def __init__(
self, language: Language, rel_path: str, symbol_name: str, new_content: str, mode: Literal["before", "after"] | None = None
self, ls_id: LanguageServerId, rel_path: str, symbol_name: str, new_content: str, mode: Literal["before", "after"] | None = None
):
super().__init__(language, rel_path)
super().__init__(ls_id, rel_path)
self.symbol_name = symbol_name
self.new_content = new_content
self.mode: Literal["before", "after"] | None = mode
@@ -331,7 +331,7 @@ class InsertInRelToSymbolTest(EditingTest):
[
pytest.param(
InsertInRelToSymbolTest(
Language.PYTHON,
LanguageServerId.PYTHON,
PYTHON_TEST_REL_FILE_PATH,
"use_module_variables",
NEW_PYTHON_FUNCTION,
@@ -340,7 +340,7 @@ class InsertInRelToSymbolTest(EditingTest):
),
pytest.param(
InsertInRelToSymbolTest(
Language.TYPESCRIPT,
LanguageServerId.TYPESCRIPT,
TYPESCRIPT_TEST_FILE,
"DemoClass",
NEW_TYPESCRIPT_FUNCTION_AFTER,
@@ -349,7 +349,7 @@ class InsertInRelToSymbolTest(EditingTest):
),
pytest.param(
InsertInRelToSymbolTest(
Language.TYPESCRIPT,
LanguageServerId.TYPESCRIPT,
TYPESCRIPT_TEST_FILE,
"helperFunction",
NEW_TYPESCRIPT_FUNCTION,
@@ -366,7 +366,7 @@ def test_insert_in_rel_to_symbol(test_case: InsertInRelToSymbolTest, mode: Liter
@pytest.mark.python
def test_insert_python_class_before(snapshot: SnapshotAssertion):
InsertInRelToSymbolTest(
Language.PYTHON,
LanguageServerId.PYTHON,
PYTHON_TEST_REL_FILE_PATH,
"VariableDataclass",
NEW_PYTHON_CLASS_WITH_TRAILING_NEWLINES,
@@ -377,7 +377,7 @@ def test_insert_python_class_before(snapshot: SnapshotAssertion):
@pytest.mark.python
def test_insert_python_class_after(snapshot: SnapshotAssertion):
InsertInRelToSymbolTest(
Language.PYTHON,
LanguageServerId.PYTHON,
PYTHON_TEST_REL_FILE_PATH,
"VariableDataclass",
NEW_PYTHON_CLASS_WITH_LEADING_NEWLINES,
@@ -399,8 +399,8 @@ TYPESCRIPT_REPLACED_BODY = """function printValue() {
class ReplaceBodyTest(EditingTest):
def __init__(self, language: Language, rel_path: str, symbol_name: str, new_body: str):
super().__init__(language, rel_path)
def __init__(self, ls_id: LanguageServerId, rel_path: str, symbol_name: str, new_body: str):
super().__init__(ls_id, rel_path)
self.symbol_name = symbol_name
self.new_body = new_body
@@ -413,7 +413,7 @@ class ReplaceBodyTest(EditingTest):
[
pytest.param(
ReplaceBodyTest(
Language.PYTHON,
LanguageServerId.PYTHON,
PYTHON_TEST_REL_FILE_PATH,
"VariableContainer/modify_instance_var",
PYTHON_REPLACED_BODY,
@@ -422,7 +422,7 @@ class ReplaceBodyTest(EditingTest):
),
pytest.param(
ReplaceBodyTest(
Language.TYPESCRIPT,
LanguageServerId.TYPESCRIPT,
TYPESCRIPT_TEST_FILE,
"DemoClass/printValue",
TYPESCRIPT_REPLACED_BODY,
@@ -442,8 +442,8 @@ NIX_ATTR_REPLACEMENT = """c = 3;"""
class NixAttrReplacementTest(EditingTest):
"""Test for replacing individual attributes in Nix that should NOT result in double semicolons."""
def __init__(self, language: Language, rel_path: str, symbol_name: str, new_body: str):
super().__init__(language, rel_path)
def __init__(self, ls_id: LanguageServerId, rel_path: str, symbol_name: str, new_body: str):
super().__init__(ls_id, rel_path)
self.symbol_name = symbol_name
self.new_body = new_body
@@ -452,7 +452,7 @@ class NixAttrReplacementTest(EditingTest):
@pytest.mark.nix
@pytest.mark.skipif(not language_tests_enabled(Language.NIX), reason="Nix tests are disabled (nixd not available)")
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.NIX), reason="Nix tests are disabled (nixd not available)")
def test_nix_symbol_replacement_no_double_semicolon(snapshot: SnapshotAssertion):
"""
Test that replacing a Nix attribute does not result in double semicolons.
@@ -467,7 +467,7 @@ def test_nix_symbol_replacement_no_double_semicolon(snapshot: SnapshotAssertion)
logic should prevent double semicolons.
"""
test_case = NixAttrReplacementTest(
Language.NIX,
LanguageServerId.NIX,
"default.nix",
"testUser", # Simple attrset with multiple key-value pairs
NIX_ATTR_REPLACEMENT,
@@ -486,8 +486,8 @@ class GoDeclReplacementTest(EditingTest):
in a duplicated leading keyword (e.g. ``type type NamedInt``).
"""
def __init__(self, language: Language, rel_path: str, symbol_name: str, new_body: str):
super().__init__(language, rel_path)
def __init__(self, ls_id: LanguageServerId, rel_path: str, symbol_name: str, new_body: str):
super().__init__(ls_id, rel_path)
self.symbol_name = symbol_name
self.new_body = new_body
@@ -523,7 +523,7 @@ def test_go_symbol_replacement_no_double_keyword(snapshot: SnapshotAssertion):
range extension prevents the duplicated keyword.
"""
test_case = GoDeclReplacementTest(
Language.GO,
LanguageServerId.GO,
"symbol_body.go",
"NamedInt",
GO_DECL_REPLACEMENT,
@@ -532,8 +532,8 @@ def test_go_symbol_replacement_no_double_keyword(snapshot: SnapshotAssertion):
class RenameSymbolTest(EditingTest):
def __init__(self, language: Language, rel_path: str, symbol_name: str, new_name: str):
super().__init__(language, rel_path)
def __init__(self, ls_id: LanguageServerId, rel_path: str, symbol_name: str, new_name: str):
super().__init__(ls_id, rel_path)
self.symbol_name = symbol_name
self.new_name = new_name
@@ -550,7 +550,7 @@ class RenameSymbolTest(EditingTest):
@pytest.mark.python
def test_rename_symbol(snapshot: SnapshotAssertion):
test_case = RenameSymbolTest(
Language.PYTHON,
LanguageServerId.PYTHON,
PYTHON_TEST_REL_FILE_PATH,
"typed_module_var",
"renamed_typed_module_var",
@@ -564,15 +564,15 @@ class InsertAndDeleteSymbolTest(EditingTest):
in no change to the file content.
"""
def __init__(self, language: Language, rel_path: str, rel_symbol: str, deleted_symbol: str, insertion: str):
def __init__(self, ls_id: LanguageServerId, rel_path: str, rel_symbol: str, deleted_symbol: str, insertion: str):
"""
:param language: specifies the language server to use
:param ls_id: specifies the language server to use
:param rel_path: relative path of the file
:param rel_symbol: symbol after which the insertion is made
:param deleted_symbol: the symbol to be deleted after the insertion
:param insertion: text which inserts the symbol `deleted_symbol`
"""
super().__init__(language, rel_path)
super().__init__(ls_id, rel_path)
self.rel_symbol = rel_symbol
self.deleted_symbol = deleted_symbol
self.rel_path = rel_path
@@ -592,7 +592,7 @@ def test_insert_and_delete_no_change():
"""
insertion = " def inserted_function():\n pass"
test_case = InsertAndDeleteSymbolTest(
Language.PYTHON,
LanguageServerId.PYTHON,
PYTHON_TEST_REL_FILE_PATH,
"VariableContainer/modify_instance_var",
"VariableContainer/inserted_function",
@@ -617,7 +617,7 @@ NEW_VUE_HANDLER = """const handleDoubleClick = () => {
[
pytest.param(
DeleteSymbolTest(
Language.VUE,
LanguageServerId.VUE,
VUE_TEST_FILE,
"handleMouseEnter",
),
@@ -635,7 +635,7 @@ def test_delete_symbol_vue(test_case: DeleteSymbolTest, snapshot: SnapshotAssert
[
pytest.param(
InsertInRelToSymbolTest(
Language.VUE,
LanguageServerId.VUE,
VUE_TEST_FILE,
"handleClick",
NEW_VUE_HANDLER,
@@ -666,7 +666,7 @@ VUE_REPLACED_HANDLECLICK_BODY = """const handleClick = () => {
[
pytest.param(
ReplaceBodyTest(
Language.VUE,
LanguageServerId.VUE,
VUE_TEST_FILE,
"handleClick",
VUE_REPLACED_HANDLECLICK_BODY,
@@ -687,7 +687,7 @@ VUE_REPLACED_PRESSCOUNT_BODY = """const pressCount = ref(100)"""
[
pytest.param(
ReplaceBodyTest(
Language.VUE,
LanguageServerId.VUE,
VUE_TEST_FILE,
"pressCount",
VUE_REPLACED_PRESSCOUNT_BODY,
@@ -726,7 +726,7 @@ VUE_STORE_REPLACED_CLEAR_BODY = """function clear() {
[
pytest.param(
ReplaceBodyTest(
Language.VUE,
LanguageServerId.VUE,
VUE_STORE_FILE,
"clear",
VUE_STORE_REPLACED_CLEAR_BODY,
+12 -14
View File
@@ -1,9 +1,7 @@
"""Unit tests for :func:`serena.util.inspection.determine_programming_language_composition`."""
from pathlib import Path
from serena.util.inspection import determine_programming_language_composition
from solidlsp.ls_config import Language
from serena.util.inspection import compute_language_server_support_composition
from solidlsp.ls_config import LanguageServerId
def _touch(directory: Path, *names: str) -> None:
@@ -11,11 +9,11 @@ def _touch(directory: Path, *names: str) -> None:
(directory / name).write_text("content", encoding="utf-8")
class TestDetermineProgrammingLanguageComposition:
class TestComputeLanguageServerSupportComposition:
def test_single_language_repo(self, tmp_path: Path) -> None:
_touch(tmp_path, "a.py", "b.py", "c.py")
composition = determine_programming_language_composition(str(tmp_path))
assert composition[Language.PYTHON] == 100.0
composition = compute_language_server_support_composition(str(tmp_path))
assert composition[LanguageServerId.PYTHON] == 100.0
def test_unrecognised_files_do_not_dilute_percentages(self, tmp_path: Path) -> None:
# 2 source files vs many files that belong to no supported language
@@ -23,23 +21,23 @@ class TestDetermineProgrammingLanguageComposition:
_touch(tmp_path, *[f"note_{i}.txt" for i in range(20)])
_touch(tmp_path, "logo.png", "LICENSE", "data.csv")
composition = determine_programming_language_composition(str(tmp_path))
composition = compute_language_server_support_composition(str(tmp_path))
# previously: 2 / 25 files = 8% — now the denominator is recognised source files only
assert composition[Language.PYTHON] == 100.0
assert composition[LanguageServerId.PYTHON] == 100.0
def test_mixed_language_percentages_relative_to_recognised_files(self, tmp_path: Path) -> None:
_touch(tmp_path, "a.py", "b.py", "c.py", "d.go")
_touch(tmp_path, *[f"asset_{i}.dat" for i in range(50)])
composition = determine_programming_language_composition(str(tmp_path))
composition = compute_language_server_support_composition(str(tmp_path))
assert composition[Language.PYTHON] == 75.0
assert composition[Language.GO] == 25.0
assert composition[LanguageServerId.PYTHON] == 75.0
assert composition[LanguageServerId.GO] == 25.0
def test_repo_without_recognised_files(self, tmp_path: Path) -> None:
_touch(tmp_path, "readme.txt", "logo.png")
assert determine_programming_language_composition(str(tmp_path)) == {}
assert compute_language_server_support_composition(str(tmp_path)) == {}
def test_empty_repo(self, tmp_path: Path) -> None:
assert determine_programming_language_composition(str(tmp_path)) == {}
assert compute_language_server_support_composition(str(tmp_path)) == {}
+15 -15
View File
@@ -3,7 +3,7 @@ from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@@ -24,14 +24,14 @@ class TestAdaLanguageServer:
obviously test-affecting.
"""
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ADA], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ADA], indirect=True)
def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
# main.adb (1-indexed source / 0-indexed LSP):
# 5/4: Greeting : constant String := Helper.Greet ("Ada");
@@ -47,8 +47,8 @@ class TestAdaLanguageServer:
# `Greeting` is declared on LSP line 4; ALS points at the identifier (column 3).
assert loc["range"]["start"]["line"] == 4
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ADA], indirect=True)
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
# main.adb LSP line 4 / column 40 sits on the `G` of `Helper.Greet`:
# ` Greeting : constant String := Helper.Greet ("Ada");`
@@ -65,8 +65,8 @@ class TestAdaLanguageServer:
assert loc["range"]["start"]["line"] == 4
assert loc["range"]["start"]["character"] == 12
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ADA], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
# Click on `Helper` in `with Helper;` (main.adb LSP line 1, column 5).
# Serena's request_references uses includeDeclaration=False, so we need a symbol with
@@ -80,8 +80,8 @@ class TestAdaLanguageServer:
# The qualifier usage on LSP line 4 of main.adb must appear among the references.
assert 4 in ref_lines_in_main, f"Expected reference on line 4 of main.adb, got {ref_lines_in_main}"
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ADA], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
# Click on the `G` of `Greet` in the spec:
# helper.ads LSP line 4: ` function Greet (Name : String) return String;`
@@ -94,7 +94,7 @@ class TestAdaLanguageServer:
# The call site in main.adb must appear in the references.
assert "main.adb" in ref_files, f"Expected reference in main.adb, got {ref_files}"
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
from solidlsp.ls_utils import SymbolUtils
@@ -103,7 +103,7 @@ class TestAdaLanguageServer:
assert SymbolUtils.symbol_tree_contains_name(symbols, "Greet"), "Greet subprogram not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main procedure not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
def test_document_symbols_helper(self, language_server: SolidLanguageServer) -> None:
doc_symbols = language_server.request_document_symbols(str(Path("src") / "helper.ads"))
all_symbols, _ = doc_symbols.get_all_symbols_and_roots()
@@ -111,7 +111,7 @@ class TestAdaLanguageServer:
assert "Helper" in names, f"Helper package not found in helper.ads document symbols. Found: {names}"
assert "Greet" in names, f"Greet not found in helper.ads document symbols. Found: {names}"
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
def test_document_symbols_hierarchical_structure(self, language_server: SolidLanguageServer) -> None:
"""ALS must return hierarchical DocumentSymbol[] with subprograms nested under their package."""
all_symbols, root_symbols = language_server.request_document_symbols(str(Path("src") / "helper.ads")).get_all_symbols_and_roots()
@@ -129,7 +129,7 @@ class TestAdaLanguageServer:
# Greet must NOT appear at root level — that would indicate the flat fallback format.
assert "Greet" not in root_names, f"Greet should be a child of Helper, not at root level. Roots: {root_names}"
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None:
# ALS surfaces a few Ada-specific synthetic groupings as symbols:
# - "With clauses" — namespace-kind group containing all `with` statements in a unit
+2 -2
View File
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.ada
class TestAdaDiagnostics:
@pytest.mark.parametrize("language_server", [Language.ADA], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ADA], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+27 -27
View File
@@ -5,13 +5,13 @@ import pytest
from serena.symbol import LanguageServerSymbol
from solidlsp import SolidLanguageServer
from solidlsp.language_servers.al_language_server import ALLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import SymbolUtils
from test.conftest import language_tests_enabled
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
pytestmark = [pytest.mark.al, pytest.mark.skipif(not language_tests_enabled(Language.AL), reason="AL tests are disabled")]
pytestmark = [pytest.mark.al, pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.AL), reason="AL tests are disabled")]
class TestExtractALDisplayName:
@@ -62,7 +62,7 @@ class TestExtractALDisplayName:
@pytest.mark.al
class TestALLanguageServer:
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_symbol_names_are_normalized(self, language_server: SolidLanguageServer) -> None:
"""Test that AL symbol names are normalized (metadata stripped)."""
file_path = os.path.join("src", "Tables", "Customer.Table.al")
@@ -79,7 +79,7 @@ class TestALLanguageServer:
# Name should be just "TEST Customer", not "Table 50000 'TEST Customer'"
assert customer_table["name"] == "TEST Customer", f"Expected normalized name 'TEST Customer', got '{customer_table['name']}'"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_find_symbol_exact_match(self, language_server: SolidLanguageServer) -> None:
"""Test that find_symbol can match AL symbols by normalized name without substring_matching."""
file_path = os.path.join("src", "Tables", "Customer.Table.al")
@@ -96,7 +96,7 @@ class TestALLanguageServer:
pytest.fail("Could not find 'TEST Customer' symbol by exact name match")
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_find_codeunit_exact_match(self, language_server: SolidLanguageServer) -> None:
"""Test finding a codeunit by its normalized name."""
file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al")
@@ -112,7 +112,7 @@ class TestALLanguageServer:
pytest.fail("Could not find 'CustomerMgt' symbol by exact name match")
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test that AL Language Server can find symbols in the test repository with normalized names."""
symbols = language_server.request_full_symbol_tree()
@@ -136,7 +136,7 @@ class TestALLanguageServer:
# Check for interface symbol
assert SymbolUtils.symbol_tree_contains_name(symbols, "IPaymentProcessor"), "IPaymentProcessor interface not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_find_table_fields(self, language_server: SolidLanguageServer) -> None:
"""Test that AL Language Server can find fields within a table."""
file_path = os.path.join("src", "Tables", "Customer.Table.al")
@@ -169,7 +169,7 @@ class TestALLanguageServer:
assert any("Name" in name for name in field_names), f"Name field not found. Fields: {field_names}"
assert any("Balance" in name for name in field_names), f"Balance field not found. Fields: {field_names}"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_find_procedures(self, language_server: SolidLanguageServer) -> None:
"""Test that AL Language Server can find procedures in codeunits."""
file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al")
@@ -191,7 +191,7 @@ class TestALLanguageServer:
assert any("CreateCustomer" in name for name in procedure_names), "CreateCustomer procedure not found"
assert any("TestNoSeries" in name for name in procedure_names), "TestNoSeries procedure not found"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test that AL Language Server can find references to symbols."""
# Find references to the Customer table from the CustomerMgt codeunit
@@ -220,7 +220,7 @@ class TestALLanguageServer:
"Customer table should be referenced in CustomerCard.Page.al"
)
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_cross_file_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test that AL Language Server can handle cross-file symbol relationships."""
# Get all symbols to verify cross-file visibility
@@ -290,7 +290,7 @@ class TestALHoverInjection:
return hover, None
return None, None
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_table_injects_full_name(self, language_server: SolidLanguageServer) -> None:
"""Test that hovering over a Table symbol shows the full object name with ID."""
file_path = os.path.join("src", "Tables", "Customer.Table.al")
@@ -300,7 +300,7 @@ class TestALHoverInjection:
assert value is not None, "Hover should have content"
assert '**Table 50000 "TEST Customer"**' in value, f"Hover should contain full Table name with ID. Got: {value[:200]}"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_page_injects_full_name(self, language_server: SolidLanguageServer) -> None:
"""Test that hovering over a Page symbol shows the full object name with ID."""
file_path = os.path.join("src", "Pages", "CustomerCard.Page.al")
@@ -310,7 +310,7 @@ class TestALHoverInjection:
assert value is not None, "Hover should have content"
assert '**Page 50001 "TEST Customer Card"**' in value, f"Hover should contain full Page name with ID. Got: {value[:200]}"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_codeunit_injects_full_name(self, language_server: SolidLanguageServer) -> None:
"""Test that hovering over a Codeunit symbol shows the full object name with ID."""
file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al")
@@ -320,7 +320,7 @@ class TestALHoverInjection:
assert value is not None, "Hover should have content"
assert "**Codeunit 50000 CustomerMgt**" in value, f"Hover should contain full Codeunit name with ID. Got: {value[:200]}"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_enum_injects_full_name(self, language_server: SolidLanguageServer) -> None:
"""Test that hovering over an Enum symbol shows the full object name with ID."""
file_path = os.path.join("src", "Enums", "CustomerType.Enum.al")
@@ -330,7 +330,7 @@ class TestALHoverInjection:
assert value is not None, "Hover should have content"
assert "**Enum 50000 CustomerType**" in value, f"Hover should contain full Enum name with ID. Got: {value[:200]}"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_interface_injects_full_name(self, language_server: SolidLanguageServer) -> None:
"""Test that hovering over an Interface symbol shows the full object name (no ID for interfaces)."""
file_path = os.path.join("src", "Interfaces", "IPaymentProcessor.Interface.al")
@@ -340,7 +340,7 @@ class TestALHoverInjection:
assert value is not None, "Hover should have content"
assert "**Interface IPaymentProcessor**" in value, f"Hover should contain full Interface name. Got: {value[:200]}"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_procedure_no_injection(self, language_server: SolidLanguageServer) -> None:
"""Test that hovering over a procedure does NOT inject object name (procedures are not normalized)."""
file_path = os.path.join("src", "Codeunits", "CustomerMgt.Codeunit.al")
@@ -353,7 +353,7 @@ class TestALHoverInjection:
# But should contain procedure info
assert "CreateCustomer" in value, f"Hover should contain procedure name. Got: {value[:200]}"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_field_no_injection(self, language_server: SolidLanguageServer) -> None:
"""Test that hovering over a field does NOT inject object name (fields are not normalized)."""
file_path = os.path.join("src", "Tables", "Customer.Table.al")
@@ -380,7 +380,7 @@ class TestALHoverInjection:
pytest.fail("Could not find a field to test hover on")
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_multiple_objects_correct_injection(self, language_server: SolidLanguageServer) -> None:
"""Test that multiple AL objects each get their correct full name injected."""
test_cases = [
@@ -398,7 +398,7 @@ class TestALHoverInjection:
f"Hover for {symbol_name} should contain '{expected_full_name}'. Got: {value[:200]}"
)
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_contains_separator_after_injection(self, language_server: SolidLanguageServer) -> None:
"""Test that injected hover has a separator between injected name and original content."""
file_path = os.path.join("src", "Tables", "Customer.Table.al")
@@ -413,7 +413,7 @@ class TestALHoverInjection:
separator_pos = value.find("---")
assert separator_pos > bold_end, "Separator should come after the injected name"
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_preserves_original_content(self, language_server: SolidLanguageServer) -> None:
"""Test that the original hover content is preserved after the injected name."""
file_path = os.path.join("src", "Tables", "Customer.Table.al")
@@ -430,7 +430,7 @@ class TestALHoverInjection:
class TestALPathNormalization:
"""Tests for path normalization in hover injection cache."""
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_with_forward_slash_path(self, language_server: SolidLanguageServer) -> None:
"""Test that hover injection works with forward slash paths."""
file_path = "src/Tables/Customer.Table.al"
@@ -451,7 +451,7 @@ class TestALPathNormalization:
pytest.fail("Could not find TEST Customer symbol")
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_with_backslash_path(self, language_server: SolidLanguageServer) -> None:
"""Test that hover injection works with backslash paths (Windows style)."""
file_path = "src\\Tables\\Customer.Table.al"
@@ -472,7 +472,7 @@ class TestALPathNormalization:
pytest.fail("Could not find TEST Customer symbol")
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_with_mixed_path_formats_symbols_backslash_hover_forward(self, language_server: SolidLanguageServer) -> None:
"""Test hover works when symbols requested with backslash but hover with forward slash."""
file_path_backslash = "src\\Tables\\Customer.Table.al"
@@ -499,7 +499,7 @@ class TestALPathNormalization:
pytest.fail("Could not find TEST Customer symbol")
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_with_mixed_path_formats_symbols_forward_hover_backslash(self, language_server: SolidLanguageServer) -> None:
"""Test hover works when symbols requested with forward slash but hover with backslash."""
file_path_forward = "src/Tables/Customer.Table.al"
@@ -526,7 +526,7 @@ class TestALPathNormalization:
pytest.fail("Could not find TEST Customer symbol")
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_hover_caching_multiple_files_different_path_formats(self, language_server: SolidLanguageServer) -> None:
"""Test that hover injection cache works correctly across multiple files with different path formats."""
test_cases = [
@@ -559,7 +559,7 @@ class TestALPathNormalization:
)
break
@pytest.mark.parametrize("language_server", [Language.AL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.AL], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
+20 -20
View File
@@ -17,19 +17,19 @@ import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.conftest import read_repo_file, request_all_symbols
@pytest.mark.angular
class TestAngularLanguageServerBasics:
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ANGULAR], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_component_class_symbols(self, language_server: SolidLanguageServer) -> None:
"""The Angular LS exposes the component class methods/fields via tsserver."""
all_symbols, _ = language_server.request_document_symbols("src/app/app.component.ts").get_all_symbols_and_roots()
@@ -37,14 +37,14 @@ class TestAngularLanguageServerBasics:
for expected in ("AppComponent", "title", "userName", "items", "greeting", "setName"):
assert expected in names, f"Expected '{expected}' in component symbols: {names}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_service_class_symbols(self, language_server: SolidLanguageServer) -> None:
all_symbols, _ = language_server.request_document_symbols("src/app/greeting.service.ts").get_all_symbols_and_roots()
names = [s["name"] for s in all_symbols]
for expected in ("GreetingService", "greet", "defaultName"):
assert expected in names, f"Expected '{expected}' in service symbols: {names}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_full_symbol_tree_includes_all_files(self, language_server: SolidLanguageServer) -> None:
all_symbols = request_all_symbols(language_server)
relative_paths = {s.get("location", {}).get("relativePath") for s in all_symbols}
@@ -59,7 +59,7 @@ class TestAngularLanguageServerBasics:
):
assert f in relative_paths, f"Expected {f} to appear in symbol tree, got {relative_paths}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_template_definition_to_component_method(self, language_server: SolidLanguageServer) -> None:
"""Resolve `greeting()` interpolation in the template to its component method.
@@ -79,7 +79,7 @@ class TestAngularLanguageServerBasics:
f"Expected definition to resolve into app.component.ts, got URIs: {target_uris}"
)
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_plain_html_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""DocumentSymbol on plain index.html should come back from the HTML companion.
@@ -94,7 +94,7 @@ class TestAngularLanguageServerBasics:
for expected in ("html", "head", "body", "app-root"):
assert expected in names, f"Expected '{expected}' in plain-HTML symbol list, got: {names}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_template_html_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""DocumentSymbol on an Angular template must return the HTML element tree.
@@ -108,7 +108,7 @@ class TestAngularLanguageServerBasics:
for expected in ("section", "h1", "p", "input", "ul", "app-item-card"):
assert expected in names, f"Expected '{expected}' in template HTML symbol list, got: {names}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_ts_method_references_include_template_usage(self, language_server: SolidLanguageServer) -> None:
"""References on a .ts component method must include its template callers.
@@ -134,7 +134,7 @@ class TestAngularLanguageServerBasics:
class TestAngularHover:
"""Hover routing — .ts goes to the companion tsserver, .html goes to ngserver."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_hover_on_ts_method(self, language_server: SolidLanguageServer) -> None:
"""Hover on a .ts method declaration is routed through the companion TS server
and must yield a non-empty MarkupContent describing the method signature.
@@ -153,7 +153,7 @@ class TestAngularHover:
text = contents["value"] if isinstance(contents, dict) else str(contents)
assert "setName" in text, f"Expected setName in hover text, got: {text}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_hover_on_template_method_call(self, language_server: SolidLanguageServer) -> None:
"""Hover on a method call inside an Angular template ({{ greeting() }}) goes
through ngserver and must yield Angular-aware type info.
@@ -174,7 +174,7 @@ class TestAngularHover:
class TestAngularDefinitionRouting:
"""Cross-file definition for the binding flavours not covered by basic tests."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_definition_from_property_binding(self, language_server: SolidLanguageServer) -> None:
"""Property binding ``[value]="userName()"`` must resolve to the component's
``userName`` signal field declaration in app.component.ts.
@@ -193,7 +193,7 @@ class TestAngularDefinitionRouting:
f"Expected definition to resolve into app.component.ts, got URIs: {target_uris}"
)
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_definition_from_event_binding(self, language_server: SolidLanguageServer) -> None:
"""Event binding ``(input)="setName(...)"`` must resolve to the component's
``setName`` method declaration in app.component.ts.
@@ -211,7 +211,7 @@ class TestAngularDefinitionRouting:
f"Expected definition to resolve into app.component.ts, got URIs: {target_uris}"
)
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_definition_service_import_in_component(self, language_server: SolidLanguageServer) -> None:
"""The ``GreetingService`` symbol used in the constructor parameter list
of AppComponent must resolve to greeting.service.ts via the companion TS server.
@@ -231,7 +231,7 @@ class TestAngularDefinitionRouting:
f"Expected definition to resolve into greeting.service.ts, got URIs: {target_uris}"
)
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_definition_from_child_component_selector(self, language_server: SolidLanguageServer) -> None:
"""The ``<app-item-card>`` element in the parent template must resolve to the
ItemCardComponent class declaration in item-card.component.ts.
@@ -258,7 +258,7 @@ class TestAngularDefinitionRouting:
class TestAngularRename:
"""Rename routing returns a WorkspaceEdit without applying it (safe for fixtures)."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_rename_method_returns_edits_for_ts_and_template(self, language_server: SolidLanguageServer) -> None:
"""Renaming the ``setName`` method from its .ts declaration must return a
WorkspaceEdit that touches both app.component.ts (declaration + any TS calls)
@@ -289,7 +289,7 @@ class TestAngularRename:
class TestAngularSymbolStructure:
"""Hierarchical symbol structure — class symbols must contain method/field children."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_component_class_has_methods_and_fields_as_children(self, language_server: SolidLanguageServer) -> None:
"""``request_document_symbols`` should return AppComponent as a class symbol
whose children include its methods (``greeting``, ``setName``) and signal
@@ -306,7 +306,7 @@ class TestAngularSymbolStructure:
for expected in ("greeting", "setName", "title", "userName", "items"):
assert expected in child_names, f"Expected '{expected}' as child of AppComponent, got: {child_names}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_pipe_class_in_symbol_tree(self, language_server: SolidLanguageServer) -> None:
"""A custom pipe (``ExclaimPipe``) declared in exclaim.pipe.ts must appear in
the workspace symbol tree alongside the components and the service.
@@ -321,7 +321,7 @@ class TestAngularSymbolStructure:
class TestAngularImplementations:
"""``textDocument/implementation`` is routed through tsserver via the Angular plugin."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_find_implementations_of_interface_method(self, language_server: SolidLanguageServer) -> None:
"""``Greeter.greet`` is implemented by ``GreetingService``; ``request_implementation``
invoked at the interface declaration must point at the service.
@@ -12,13 +12,13 @@ Two paths are exercised:
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.angular
class TestAngularDiagnostics:
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_component_class_diagnostics(self, language_server: SolidLanguageServer) -> None:
"""The component's ``count: number = 'not-a-number'`` must be flagged by tsserver."""
assert_file_diagnostics(
@@ -28,7 +28,7 @@ class TestAngularDiagnostics:
min_count=1,
)
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_template_diagnostics(self, language_server: SolidLanguageServer) -> None:
"""The template's ``{{ undefinedSignal() }}`` must be flagged by ngserver.
@@ -21,7 +21,7 @@ import time
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_exceptions import SolidLSPException
from test.conftest import _create_ls
@@ -37,7 +37,7 @@ TEMPLATE_FILE = os.path.join("src", "app", "app.component.html")
class TestAngularInvalidPositionsOnTs:
"""Negative / out-of-bounds line and column on .ts files (TS-companion route)."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_negative_line_number_containing_symbol(self, language_server: SolidLanguageServer) -> None:
"""``request_containing_symbol`` short-circuits before reaching the LS:
a negative line returns None.
@@ -45,19 +45,19 @@ class TestAngularInvalidPositionsOnTs:
result = language_server.request_containing_symbol(TS_FILE, -1, 0)
assert result is None, f"Expected None for negative line, got: {result!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_negative_character_number_containing_symbol(self, language_server: SolidLanguageServer) -> None:
result = language_server.request_containing_symbol(TS_FILE, 5, -1)
assert result is None, f"Expected None for negative character, got: {result!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_line_number_beyond_file_length(self, language_server: SolidLanguageServer) -> None:
"""The wrapper code raises ``IndexError`` before reaching the LS."""
with pytest.raises(IndexError) as exc_info:
language_server.request_containing_symbol(TS_FILE, 99999, 0)
assert "list index out of range" in str(exc_info.value), f"Expected 'list index out of range' error, got: {exc_info.value}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_character_beyond_line_length_returns_enclosing_class(self, language_server: SolidLanguageServer) -> None:
"""Character far beyond end-of-line is clamped by the LSP. Line 5 of
app.component.ts is inside ``export class AppComponent { ... }``, so the
@@ -69,7 +69,7 @@ class TestAngularInvalidPositionsOnTs:
assert isinstance(result, dict), f"Expected dict (containing class), got: {result!r}"
assert result.get("name") == "AppComponent", f"Expected containing symbol 'AppComponent', got: {result.get('name')!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_references_at_negative_line_returns_empty(self, language_server: SolidLanguageServer) -> None:
"""The Angular LS's TS companion returns ``[]`` for negative-line
``request_references`` — it does **not** raise. (Plain Vue tests
@@ -79,7 +79,7 @@ class TestAngularInvalidPositionsOnTs:
result = language_server.request_references(TS_FILE, -1, 0)
assert result == [], f"Expected [], got: {result!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_definition_at_negative_position_returns_empty(self, language_server: SolidLanguageServer) -> None:
"""Same as references: TS companion returns ``[]``, does not raise."""
result = language_server.request_definition(TS_FILE, -1, 0)
@@ -93,7 +93,7 @@ class TestAngularInvalidPositionsOnTs:
class TestAngularInvalidPositionsOnTemplate:
"""Negative / out-of-bounds positions on .html files (ngserver route)."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_negative_line_definition_raises(self, language_server: SolidLanguageServer) -> None:
"""Ngserver does **not** swallow malformed positions: it surfaces a
``Debug Failure. False expression.`` error from its underlying
@@ -105,7 +105,7 @@ class TestAngularInvalidPositionsOnTemplate:
f"Unexpected exception message: {exc_info.value}"
)
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_character_beyond_line_definition_raises(self, language_server: SolidLanguageServer) -> None:
"""Same path as negative line — ngserver raises Debug Failure."""
with pytest.raises(SolidLSPException) as exc_info:
@@ -118,7 +118,7 @@ class TestAngularInvalidPositionsOnTemplate:
class TestAngularNonExistentFiles:
"""Requests against files that don't exist must raise FileNotFoundError consistently."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_nonexistent_ts_file_raises(self, language_server: SolidLanguageServer) -> None:
nonexistent = os.path.join("src", "app", "does-not-exist.component.ts")
with pytest.raises(FileNotFoundError):
@@ -128,7 +128,7 @@ class TestAngularNonExistentFiles:
with pytest.raises(FileNotFoundError):
language_server.request_document_symbols(nonexistent)
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_nonexistent_html_file_raises(self, language_server: SolidLanguageServer) -> None:
nonexistent = os.path.join("src", "app", "does-not-exist.component.html")
with pytest.raises(FileNotFoundError):
@@ -140,7 +140,7 @@ class TestAngularNonExistentFiles:
class TestAngularUndefinedSymbols:
"""Symbols that have no callers / definitions / referencing positions."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_definition_at_keyword_position_returns_empty(self, language_server: SolidLanguageServer) -> None:
"""Cursor on the ``import`` keyword (line 0, col 0 of app.component.ts)
has no definition target — the TS companion returns ``[]``.
@@ -148,7 +148,7 @@ class TestAngularUndefinedSymbols:
result = language_server.request_definition(TS_FILE, 0, 0)
assert result == [], f"Expected [] for keyword position, got: {result!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_references_for_local_const_have_few_callers(self, language_server: SolidLanguageServer) -> None:
"""A locally-scoped private field has at most its declaration plus its
in-method use as references. ``defaultName`` on GreetingService is
@@ -168,7 +168,7 @@ class TestAngularUndefinedSymbols:
class TestAngularEdgeCasePositions:
"""Position (0,0), whitespace lines, and other boundary conditions."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_containing_symbol_at_file_start_is_none(self, language_server: SolidLanguageServer) -> None:
"""Line 0 of app.component.ts is an ``import`` statement, outside any
class or function — the TS companion returns ``None`` for containing
@@ -177,19 +177,19 @@ class TestAngularEdgeCasePositions:
result = language_server.request_containing_symbol(TS_FILE, 0, 0)
assert result is None, f"Expected None at (0,0), got: {result!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_references_at_file_start_returns_empty(self, language_server: SolidLanguageServer) -> None:
"""Position (0, 0) is on the ``import`` keyword — no references."""
result = language_server.request_references(TS_FILE, 0, 0)
assert result == [], f"Expected [] at (0,0), got: {result!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_definition_at_file_start_returns_empty(self, language_server: SolidLanguageServer) -> None:
"""Position (0, 0) is on the ``import`` keyword — no definition."""
result = language_server.request_definition(TS_FILE, 0, 0)
assert result == [], f"Expected [] at (0,0), got: {result!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_template_position_no_containing_symbol(self, language_server: SolidLanguageServer) -> None:
"""An Angular template has no class/function containers; the HTML
companion's documentSymbol provides element symbols only.
@@ -198,7 +198,7 @@ class TestAngularEdgeCasePositions:
result = language_server.request_containing_symbol(TEMPLATE_FILE, 1, 4)
assert result is None, f"Expected None inside template, got: {result!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_containing_symbol_inside_class_body_returns_class(self, language_server: SolidLanguageServer) -> None:
"""Lines 5 and 10 of app.component.ts are inside the AppComponent class
body. The TS companion correctly reports AppComponent as the
@@ -209,7 +209,7 @@ class TestAngularEdgeCasePositions:
assert isinstance(result, dict), f"Line {line}: expected dict, got {result!r}"
assert result.get("name") == "AppComponent", f"Line {line}: expected 'AppComponent', got {result.get('name')!r}"
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_containing_symbol_outside_class_body_is_none(self, language_server: SolidLanguageServer) -> None:
"""Line 0 (import) and line 15 (blank/whitespace inside file but
outside any symbol's range, depending on file structure) report no
@@ -223,7 +223,7 @@ class TestAngularEdgeCasePositions:
class TestAngularReferenceEdgeCases:
"""Edge cases for the SolidLanguageServer reference helpers (built on top of LSP)."""
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_referencing_symbols_at_invalid_position_raises(self, language_server: SolidLanguageServer) -> None:
"""Unlike ``request_references`` (which returns ``[]``),
``request_referencing_symbols`` validates more strictly and surfaces
@@ -241,7 +241,7 @@ class TestAngularReferenceEdgeCases:
f"Unexpected exception message: {exc_info.value}"
)
@pytest.mark.parametrize("language_server", [Language.ANGULAR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANGULAR], indirect=True)
def test_defining_symbol_at_invalid_position_returns_none(self, language_server: SolidLanguageServer) -> None:
"""``request_defining_symbol`` short-circuits when no definition is
found and returns None — no exception, even for negative positions.
@@ -328,7 +328,7 @@ class TestAngularStartupCleanup:
import psutil
ls = _create_ls(Language.ANGULAR)
ls = _create_ls(LanguageServerId.ANGULAR)
my_proc = psutil.Process(os.getpid())
children_before = {p.pid for p in my_proc.children(recursive=True)}
+10 -8
View File
@@ -11,24 +11,26 @@ from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@pytest.mark.skipif(not language_tests_enabled(Language.ANSIBLE), reason="Ansible tests are disabled (no native Windows support)")
@pytest.mark.skipif(
not language_server_tests_enabled(LanguageServerId.ANSIBLE), reason="Ansible tests are disabled (no native Windows support)"
)
@pytest.mark.ansible
class TestAnsibleLanguageServerBasics:
"""Test basic Ansible language server functionality."""
@pytest.mark.parametrize("language_server", [Language.ANSIBLE], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.ANSIBLE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANSIBLE], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ANSIBLE], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Language server starts and points to the correct repo."""
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.ANSIBLE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANSIBLE], indirect=True)
def test_hover_on_module_contains_documentation(self, language_server: SolidLanguageServer) -> None:
"""Hover on ansible.builtin.package returns module documentation."""
# playbook.yml line 10 (0-indexed): "ansible.builtin.package:"
@@ -43,7 +45,7 @@ class TestAnsibleLanguageServerBasics:
hover_text = str(hover_value)
assert "package" in hover_text.lower(), f"Hover should mention 'package', got: {hover_text[:300]}"
@pytest.mark.parametrize("language_server", [Language.ANSIBLE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANSIBLE], indirect=True)
def test_completions_contain_module_names(self, language_server: SolidLanguageServer) -> None:
"""Completions at a task keyword position return Ansible module names."""
# playbook.yml line 10 (0-indexed), col 6: inside a task block
@@ -54,7 +56,7 @@ class TestAnsibleLanguageServerBasics:
assert labels, f"Expected completions with completionText, got: {result[:3]}"
@pytest.mark.xfail(reason="Seems like ansible LS lacks basic functionality at the moment, textDocument/documentSymbol doesn't work")
@pytest.mark.parametrize("language_server", [Language.ANSIBLE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ANSIBLE], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
+8 -8
View File
@@ -8,7 +8,7 @@ like request_document_symbols using the bash test repository.
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@@ -16,13 +16,13 @@ from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name,
class TestBashLanguageServerBasics:
"""Test basic functionality of the bash language server."""
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BASH], indirect=True)
def test_bash_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
"""Test that bash language server can be initialized successfully."""
assert language_server is not None
assert language_server.language == Language.BASH
assert language_server.ls_id == LanguageServerId.BASH
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BASH], indirect=True)
def test_bash_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test request_document_symbols for bash files."""
# Test getting symbols from main.sh
@@ -38,7 +38,7 @@ class TestBashLanguageServerBasics:
assert "main" in function_names, "Should find main function"
assert len(function_symbols) >= 3, f"Should find at least 3 functions, found {len(function_symbols)}"
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BASH], indirect=True)
def test_bash_request_document_symbols_with_body(self, language_server: SolidLanguageServer) -> None:
"""Test request_document_symbols with body extraction."""
# Test with include_body=True
@@ -55,7 +55,7 @@ class TestBashLanguageServerBasics:
assert "function greet_user()" in body, "Function body should contain function definition"
assert "case" in body.lower(), "Function body should contain case statement"
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BASH], indirect=True)
def test_bash_utils_functions(self, language_server: SolidLanguageServer) -> None:
"""Test function detection in utils.sh file."""
# Test with utils.sh as well
@@ -81,7 +81,7 @@ class TestBashLanguageServerBasics:
assert len(utils_function_symbols) >= 8, f"Should find at least 8 functions in utils.sh, found {len(utils_function_symbols)}"
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BASH], indirect=True)
def test_bash_function_syntax_patterns(self, language_server: SolidLanguageServer) -> None:
"""Test that LSP detects different bash function syntax patterns correctly."""
# Test main.sh (has both 'function' keyword and traditional syntax)
@@ -121,7 +121,7 @@ class TestBashLanguageServerBasics:
assert len(main_functions) >= 3, f"Should find at least 3 functions in main.sh, found {len(main_functions)}"
assert len(utils_functions) >= 8, f"Should find at least 8 functions in utils.sh, found {len(utils_functions)}"
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BASH], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
+2 -2
View File
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.bash
class TestBashDiagnostics:
@pytest.mark.parametrize("language_server", [Language.BASH], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BASH], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+27 -25
View File
@@ -5,32 +5,34 @@ from unittest import mock
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from solidlsp.settings import SolidLSPSettings
from test.conftest import language_tests_enabled
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
pytestmark = pytest.mark.skipif(not language_tests_enabled(Language.BSL), reason="BSL tests are disabled (niche, slow and flaky)")
pytestmark = pytest.mark.skipif(
not language_server_tests_enabled(LanguageServerId.BSL), reason="BSL tests are disabled (niche, slow and flaky)"
)
@pytest.mark.bsl
class TestBSLLanguageServer:
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.BSL], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Language server starts and attaches to the test repository."""
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
symbols = language_server.request_full_symbol_tree()
assert SymbolUtils.symbol_tree_contains_name(symbols, "ВывестиСообщение"), "ВывестиСообщение not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "ПолучитьПриветствие"), "ПолучитьПриветствие not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "Инициализировать"), "Инициализировать not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
doc_symbols = language_server.request_document_symbols("CommonModule.bsl")
all_symbols, _ = doc_symbols.get_all_symbols_and_roots()
@@ -39,7 +41,7 @@ class TestBSLLanguageServer:
assert "ПолучитьПриветствие" in names, f"ПолучитьПриветствие not found in CommonModule.bsl symbols. Found: {names}"
assert "ВызватьПриветствие" in names, f"ВызватьПриветствие not found in CommonModule.bsl symbols. Found: {names}"
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_full_symbol_tree_within_file(self, language_server: SolidLanguageServer) -> None:
"""Scoping the full-tree request to a single file returns that file's symbols."""
symbols = language_server.request_full_symbol_tree(within_relative_path="ObjectModule.bsl")
@@ -50,7 +52,7 @@ class TestBSLLanguageServer:
"ПолучитьСостояние not found in ObjectModule.bsl symbol tree"
)
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
# CommonModule.bsl (0-indexed):
# line 2: Процедура ВывестиСообщение(Текст) Экспорт <- declaration (name starts at col 10)
@@ -66,7 +68,7 @@ class TestBSLLanguageServer:
matching_lines = [ref["range"]["start"]["line"] for ref in refs if "CommonModule.bsl" in ref.get("relativePath", "")]
assert 12 in matching_lines, f"Expected a reference on line 12 (0-indexed), got lines: {matching_lines}"
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_find_references_to_function_within_file(self, language_server: SolidLanguageServer) -> None:
# CommonModule.bsl (0-indexed):
# line 6: Функция ПолучитьПриветствие(Имя) Экспорт <- declaration (name starts at col 8)
@@ -88,7 +90,7 @@ class TestBSLLanguageServer:
_CROSS_REF_MODULE1 = os.path.join("src", "CommonModules", "ОбщийМодуль1", "Ext", "Module.bsl")
_CROSS_REF_MODULE2 = os.path.join("src", "CommonModules", "ОбщийМодуль2", "Ext", "Module.bsl")
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
"""``request_references`` from the declaration must include the cross-module call-site."""
# ОбщийМодуль1 / Ext / Module.bsl (0-indexed):
@@ -104,7 +106,7 @@ class TestBSLLanguageServer:
call_site_lines = [ref["range"]["start"]["line"] for ref in refs if "ОбщийМодуль2" in ref.get("relativePath", "")]
assert 3 in call_site_lines, f"Expected a reference at ОбщийМодуль2/Module.bsl line 3, got lines: {call_site_lines}"
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_find_definition_across_files(self, language_server: SolidLanguageServer) -> None:
"""``request_definition`` from a cross-module call-site must resolve to the other module."""
# cursor on "ВывестиСообщение" inside
@@ -116,7 +118,7 @@ class TestBSLLanguageServer:
target_lines = [d["range"]["start"]["line"] for d in definitions if "ОбщийМодуль1" in d.get("relativePath", "")]
assert 2 in target_lines, f"Expected the definition to point at ОбщийМодуль1/Module.bsl line 2, got: {target_lines}"
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None:
all_symbols = request_all_symbols(language_server)
malformed = [s for s in all_symbols if has_malformed_name(s)]
@@ -133,15 +135,15 @@ class TestBSLLanguageServer:
def test_bsl_filename_matcher() -> None:
matcher = Language.BSL.get_source_fn_matcher()
matcher = LanguageServerId.BSL.get_source_fn_matcher()
assert matcher.is_relevant_filename("module.bsl")
assert matcher.is_relevant_filename("script.os")
assert not matcher.is_relevant_filename("module.py")
def test_bsl_enum_registration() -> None:
assert Language.BSL.value == "bsl"
assert Language.BSL.get_ls_class().__name__ == "BSLLanguageServer"
assert LanguageServerId.BSL.value == "bsl"
assert LanguageServerId.BSL.get_ls_class().__name__ == "BSLLanguageServer"
def test_bsl_dependency_provider_default_version() -> None:
@@ -153,7 +155,7 @@ def test_bsl_dependency_provider_default_version() -> None:
settings = SolidLSPSettings()
provider = BSLLanguageServer.DependencyProvider(
settings.get_ls_specific_settings(Language.BSL),
settings.get_ls_specific_settings(LanguageServerId.BSL),
"/tmp/ls_resources",
)
@@ -174,9 +176,9 @@ def test_bsl_dependency_provider_custom_version_no_sha() -> None:
from solidlsp.language_servers.common import RuntimeDependencyCollection
settings = SolidLSPSettings()
settings.ls_specific_settings[Language.BSL] = {"bsl_ls_version": "0.28.0"}
settings.ls_specific_settings[LanguageServerId.BSL] = {"bsl_ls_version": "0.28.0"}
provider = BSLLanguageServer.DependencyProvider(
settings.get_ls_specific_settings(Language.BSL),
settings.get_ls_specific_settings(LanguageServerId.BSL),
"/tmp/ls_resources",
)
@@ -214,9 +216,9 @@ def test_bsl_launch_command_uses_ls_path_without_download() -> None:
from solidlsp.language_servers.bsl_language_server import BSLLanguageServer
settings = SolidLSPSettings()
settings.ls_specific_settings[Language.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
settings.ls_specific_settings[LanguageServerId.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
provider = BSLLanguageServer.DependencyProvider(
settings.get_ls_specific_settings(Language.BSL),
settings.get_ls_specific_settings(LanguageServerId.BSL),
"/tmp/ls_resources",
)
@@ -241,9 +243,9 @@ def test_bsl_launch_command_requires_java() -> None:
from solidlsp.language_servers.bsl_language_server import BSLLanguageServer
settings = SolidLSPSettings()
settings.ls_specific_settings[Language.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
settings.ls_specific_settings[LanguageServerId.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
provider = BSLLanguageServer.DependencyProvider(
settings.get_ls_specific_settings(Language.BSL),
settings.get_ls_specific_settings(LanguageServerId.BSL),
"/tmp/ls_resources",
)
@@ -258,9 +260,9 @@ def test_bsl_launch_command_rejects_old_java() -> None:
from solidlsp.language_servers.bsl_language_server import BSL_LS_MIN_JAVA_VERSION, BSLLanguageServer
settings = SolidLSPSettings()
settings.ls_specific_settings[Language.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
settings.ls_specific_settings[LanguageServerId.BSL] = {"ls_path": "/custom/path/bsl-language-server.jar"}
provider = BSLLanguageServer.DependencyProvider(
settings.get_ls_specific_settings(Language.BSL),
settings.get_ls_specific_settings(LanguageServerId.BSL),
"/tmp/ls_resources",
)
+4 -4
View File
@@ -1,15 +1,15 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.bsl
@pytest.mark.skipif(not language_tests_enabled(Language.BSL), reason="BSL tests are disabled")
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.BSL), reason="BSL tests are disabled")
class TestBSLDiagnostics:
@pytest.mark.parametrize("language_server", [Language.BSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.BSL], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
"""bsl-language-server must flag the unterminated string literal in the fixture."""
assert_file_diagnostics(
+17 -17
View File
@@ -2,18 +2,18 @@ import pytest
from serena.project import Project
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
from test.conftest import language_tests_enabled
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
from . import CORE_PATH, UTILS_PATH
@pytest.mark.skipif(not language_tests_enabled(Language.CLOJURE), reason="Clojure tests are disabled")
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.CLOJURE), reason="Clojure tests are disabled")
@pytest.mark.clojure
class TestLanguageServerBasics:
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_basic_definition(self, language_server: SolidLanguageServer):
"""
Test finding definition of 'greet' function call in core.clj
@@ -27,7 +27,7 @@ class TestLanguageServerBasics:
assert definition["relativePath"] == CORE_PATH
assert definition["range"]["start"]["line"] == 2, "Should find the definition of greet function at line 2"
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_cross_file_references(self, language_server: SolidLanguageServer):
"""
Test finding references to 'multiply' function from core.clj
@@ -42,7 +42,7 @@ class TestLanguageServerBasics:
)
assert usage_found, "Should find multiply usage in utils.clj"
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_completions(self, language_server: SolidLanguageServer):
with language_server.open_file(UTILS_PATH):
# After "core/" in calculate-area
@@ -53,7 +53,7 @@ class TestLanguageServerBasics:
completion_texts = [item["completionText"] for item in result]
assert any("multiply" in text for text in completion_texts), "Should find 'multiply' function in completions after 'core/'"
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_document_symbols(self, language_server: SolidLanguageServer):
symbols, _ = language_server.request_document_symbols(CORE_PATH).get_all_symbols_and_roots()
@@ -66,7 +66,7 @@ class TestLanguageServerBasics:
for func_name in expected_functions:
assert func_name in symbol_names, f"Should find {func_name} function in symbols"
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_hover(self, language_server: SolidLanguageServer):
"""Test hover on greet function"""
result = language_server.request_hover(CORE_PATH, 2, 7)
@@ -82,7 +82,7 @@ class TestLanguageServerBasics:
else:
assert False, f"Unexpected contents format: {type(contents)}"
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_workspace_symbols(self, language_server: SolidLanguageServer):
# Search for functions containing "add"
result = language_server.request_workspace_symbol("add")
@@ -93,7 +93,7 @@ class TestLanguageServerBasics:
symbol_names = [symbol["name"] for symbol in result]
assert any("add" in name.lower() for name in symbol_names), f"Should find 'add' function in symbols: {symbol_names}"
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_namespace_functions(self, language_server: SolidLanguageServer):
"""Test definition lookup for core/greet usage in utils.clj"""
# Position of 'greet' in core/greet call
@@ -105,7 +105,7 @@ class TestLanguageServerBasics:
definition = result[0]
assert definition["relativePath"] == CORE_PATH, "Should find the definition of greet in core.clj"
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_request_references_with_content(self, language_server: SolidLanguageServer):
"""Test references to multiply function with content"""
references = language_server.request_references(CORE_PATH, 12, 6)
@@ -130,7 +130,7 @@ class TestLanguageServerBasics:
utils_content = utils_refs[0].to_display_string()
assert "calculate-area" in utils_content
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_request_full_symbol_tree(self, language_server: SolidLanguageServer):
"""Test retrieving the full symbol tree for project overview
We just check that we find some expected symbols.
@@ -172,7 +172,7 @@ class TestLanguageServerBasics:
f"Symbol tree structure:\n{traverse_symbols(result)}"
)
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_request_referencing_symbols(self, language_server: SolidLanguageServer):
"""Test finding symbols that reference a given symbol
Finds references to the 'multiply' function.
@@ -188,10 +188,10 @@ class TestLanguageServerBasics:
assert found_relevant_references, f"Should have found calculate-area referencing multiply, but got: {result}"
@pytest.mark.skipif(not language_tests_enabled(Language.CLOJURE), reason="Clojure tests are disabled")
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.CLOJURE), reason="Clojure tests are disabled")
@pytest.mark.clojure
class TestProjectBasics:
@pytest.mark.parametrize("project", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("project", [LanguageServerId.CLOJURE], indirect=True)
def test_retrieve_content_around_line(self, project: Project):
"""Test retrieving content around specific lines"""
# Test retrieving content around the greet function definition (line 2)
@@ -209,7 +209,7 @@ class TestProjectBasics:
content_str = result.to_display_string()
assert "multiply" in content_str, "Should contain multiply function"
@pytest.mark.parametrize("project", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("project", [LanguageServerId.CLOJURE], indirect=True)
def test_search_files_for_pattern(self, project: Project) -> None:
result = project.search_project_files_for_pattern("defn.*greet")
@@ -225,7 +225,7 @@ class TestProjectBasics:
utils_matches = [match for match in result if match.source_file_path and "utils.clj" in match.source_file_path]
assert len(utils_matches) > 0, "Should find require statement in utils.clj"
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
@@ -3,13 +3,13 @@ import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.clojure
class TestClojureDiagnostics:
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
@@ -19,8 +19,8 @@ import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import read_repo_file
from . import CORE_PATH, TEST_APP_PATH
@@ -29,7 +29,7 @@ EXTRA_PATH = str(TEST_APP_PATH / "extra.clj")
SUBMODULE_CONSUMER_PATH = str(Path("sub_module") / "src" / "sub_module_app" / "consumer.clj")
@pytest.mark.skipif(not language_tests_enabled(Language.CLOJURE), reason="Clojure tests are disabled")
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.CLOJURE), reason="Clojure tests are disabled")
@pytest.mark.clojure
class TestClojureProjectIndexing:
"""Covers the "indexing leaks through results" bug for clojure-lsp.
@@ -41,7 +41,7 @@ class TestClojureProjectIndexing:
indexing of those calls.
"""
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_request_references_includes_unopened_file(self, language_server: SolidLanguageServer) -> None:
# locate the definition of `multiply` in core.clj without hardcoding coords
core_content = read_repo_file(language_server, CORE_PATH)
@@ -65,7 +65,7 @@ class TestClojureProjectIndexing:
f"Expected at least 2 references in extra.clj (double-product and triple-product), got {len(extra_refs)}: {extra_refs}"
)
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_request_references_includes_unopened_sibling_module_file(self, language_server: SolidLanguageServer) -> None:
"""Mirrors the real-world penpot bug repro: ``multiply`` is defined in one
module (root ``src/``) and consumed from a sibling module that has its
@@ -92,7 +92,7 @@ class TestClojureProjectIndexing:
"get_symbols_overview, so reference search returns silently incomplete results."
)
@pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CLOJURE], indirect=True)
def test_request_referencing_symbols_includes_unopened_file(self, language_server: SolidLanguageServer) -> None:
core_content = read_repo_file(language_server, CORE_PATH)
coords = find_text_coordinates(core_content, r"\(defn (multiply)\b", require_unique=True)
+5 -5
View File
@@ -1,14 +1,14 @@
from pathlib import Path
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind, UnifiedSymbolInformation
PYTHON_BACKEND_LANGUAGES = [
Language.PYTHON,
Language.PYTHON_TY,
Language.PYTHON_PYREFLY,
Language.PYTHON_BASEDPYRIGHT,
LanguageServerId.PYTHON,
LanguageServerId.PYTHON_TY,
LanguageServerId.PYTHON_PYREFLY,
LanguageServerId.PYTHON_BASEDPYRIGHT,
]
+2 -2
View File
@@ -2,7 +2,7 @@ import os
import pytest
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.conftest import start_ls_context
@@ -16,7 +16,7 @@ class TestCCLSLanguages:
],
)
def test_get_document_symbols(self, lang: str, unit: str, names: set[str]) -> None:
with start_ls_context(Language.CPP) as ccls:
with start_ls_context(LanguageServerId.CPP) as ccls:
path = os.path.join(lang, unit)
symbols = ccls.request_document_symbols(path).get_all_symbols_and_roots()
symbols = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
+2 -2
View File
@@ -2,7 +2,7 @@ import os
import pytest
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.conftest import start_ls_context
@@ -22,7 +22,7 @@ class TestClangdLanguages:
],
)
def test_get_document_symbols(self, lang: str, unit: str, names: set[str]) -> None:
with start_ls_context(Language.CPP) as clangd:
with start_ls_context(LanguageServerId.CPP) as clangd:
path = os.path.join(lang, unit)
symbols = clangd.request_document_symbols(path).get_all_symbols_and_roots()
symbols = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
+12 -12
View File
@@ -14,18 +14,18 @@ from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from test.conftest import get_repo_path, language_tests_enabled, start_ls_context
from test.conftest import get_repo_path, language_server_tests_enabled, start_ls_context
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
_cpp_servers: list[Language] = [Language.CPP]
if language_tests_enabled(Language.CPP_CCLS):
_cpp_servers.append(Language.CPP_CCLS)
_cpp_servers: list[LanguageServerId] = [LanguageServerId.CPP]
if language_server_tests_enabled(LanguageServerId.CPP_CCLS):
_cpp_servers.append(LanguageServerId.CPP_CCLS)
@pytest.mark.parametrize("language", [Language.CPP, Language.CPP_CCLS])
def test_source_fn_matcher_includes_ino(language: Language) -> None:
@pytest.mark.parametrize("language", [LanguageServerId.CPP, LanguageServerId.CPP_CCLS])
def test_source_fn_matcher_includes_ino(language: LanguageServerId) -> None:
"""Arduino .ino sketches are C++ and must route to the C++ language server.
This is a pure matcher check; it needs no running language server.
@@ -172,7 +172,7 @@ int use_add() {
@pytest.mark.cpp
class TestCppDocumentSymbolCache:
def _copy_cpp_fixture(self, tmp_path: Path) -> Path:
fixture_path = get_repo_path(Language.CPP)
fixture_path = get_repo_path(LanguageServerId.CPP)
target_path = tmp_path / "test_repo"
shutil.copytree(fixture_path, target_path)
return target_path
@@ -180,7 +180,7 @@ class TestCppDocumentSymbolCache:
def test_cache_invalidates_when_clangd_context_changes(self, tmp_path: Path) -> None:
repo_path = self._copy_cpp_fixture(tmp_path)
ls_settings_alt = {
Language.CPP: {
LanguageServerId.CPP: {
"compile_commands_dir": ".serena-alt",
}
}
@@ -201,7 +201,7 @@ class TestCppDocumentSymbolCache:
assert ls._raw_document_symbols_cache_is_modified
assert ls._document_symbols_cache_is_modified
with start_ls_context(Language.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default:
with start_ls_context(LanguageServerId.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default:
_ = ls_default.request_document_symbols(main_cpp)
default_raw_cache_version = ls_default._raw_document_symbols_cache_version()
@@ -212,7 +212,7 @@ class TestCppDocumentSymbolCache:
cache_files = [p for p in cache_dir.rglob("*") if p.is_file()]
assert cache_files, f"Expected SolidLSP to create cache artifacts under {cache_dir}"
with start_ls_context(Language.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again:
with start_ls_context(LanguageServerId.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again:
assert ls_default_again.cache_dir == cache_dir
_assert_caches_loaded_and_clean(ls_default_again)
_ = ls_default_again.request_document_symbols(main_cpp)
@@ -220,7 +220,7 @@ class TestCppDocumentSymbolCache:
assert not ls_default_again._document_symbols_cache_is_modified
with start_ls_context(
Language.CPP,
LanguageServerId.CPP,
repo_path=str(repo_path),
ls_specific_settings=ls_settings_alt,
solidlsp_dir=tmp_path,
+2 -2
View File
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.cpp
class TestCppDiagnostics:
@pytest.mark.parametrize("language_server", [Language.CPP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CPP], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+3 -3
View File
@@ -23,12 +23,12 @@ from collections.abc import Iterator
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from test.conftest import find_identifier_position, get_repo_path, start_ls_context
from test.solidlsp.conftest import document_symbol_names, find_document_symbol
UE_REPO_PATH = get_repo_path(Language.CPP).parent / "ue_test_repo"
UE_REPO_PATH = get_repo_path(LanguageServerId.CPP).parent / "ue_test_repo"
ABILITY_COMPONENT_H = os.path.join("Source", "TestGame", "AbilityComponent.h")
ABILITY_ACTOR_H = os.path.join("Source", "TestGame", "AbilityActor.h")
ABILITY_ACTOR_CPP = os.path.join("Source", "TestGame", "AbilityActor.cpp")
@@ -37,7 +37,7 @@ ABILITY_ACTOR_CPP = os.path.join("Source", "TestGame", "AbilityActor.cpp")
@pytest.fixture(scope="module")
def language_server() -> Iterator[SolidLanguageServer]:
"""Clangd over ue_test_repo; overrides the shared fixture for this module."""
with start_ls_context(Language.CPP, repo_path=str(UE_REPO_PATH)) as ls:
with start_ls_context(LanguageServerId.CPP, repo_path=str(UE_REPO_PATH)) as ls:
yield ls
+11 -9
View File
@@ -14,25 +14,27 @@ import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
pytestmark = [
pytest.mark.crystal,
pytest.mark.skipif(not language_tests_enabled(Language.CRYSTAL), reason="Crystal tests are disabled (crystalline not available)"),
pytest.mark.skipif(
not language_server_tests_enabled(LanguageServerId.CRYSTAL), reason="Crystal tests are disabled (crystalline not available)"
),
]
class TestCrystalDocumentSymbols:
"""Test document symbol retrieval, which works reliably in Crystalline."""
@pytest.mark.parametrize("language_server", [Language.CRYSTAL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CRYSTAL], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer) -> None:
"""Test that the language server starts successfully."""
assert language_server.is_running()
@pytest.mark.parametrize("language_server", [Language.CRYSTAL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CRYSTAL], indirect=True)
def test_document_symbols_main(self, language_server: SolidLanguageServer) -> None:
"""Test that document symbols are returned for the main file."""
file_path = os.path.join("src", "main.cr")
@@ -43,7 +45,7 @@ class TestCrystalDocumentSymbols:
assert "Calculator" in symbol_names, f"Calculator not found in symbols. Found: {symbol_names}"
assert "User" in symbol_names, f"User not found in symbols. Found: {symbol_names}"
@pytest.mark.parametrize("language_server", [Language.CRYSTAL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CRYSTAL], indirect=True)
def test_document_symbols_utils(self, language_server: SolidLanguageServer) -> None:
"""Test that document symbols are returned for the utils file."""
file_path = os.path.join("src", "utils.cr")
@@ -53,7 +55,7 @@ class TestCrystalDocumentSymbols:
symbol_names = [s.get("name") for s in all_symbols if s.get("name")]
assert "Utils" in symbol_names, f"Utils not found in symbols. Found: {symbol_names}"
@pytest.mark.parametrize("language_server", [Language.CRYSTAL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CRYSTAL], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test that the full symbol tree contains expected symbols."""
from solidlsp.ls_utils import SymbolUtils
@@ -63,7 +65,7 @@ class TestCrystalDocumentSymbols:
assert SymbolUtils.symbol_tree_contains_name(symbols, "User"), "User not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "Utils"), "Utils not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.CRYSTAL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CRYSTAL], indirect=True)
def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None:
"""Test that symbol names do not contain unexpected formatting characters."""
all_symbols = request_all_symbols(language_server)
@@ -86,7 +88,7 @@ class TestCrystalDefinition:
module-scoped ``language_server`` fixture ensures we get a fresh server.
"""
@pytest.mark.parametrize("language_server", [Language.CRYSTAL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CRYSTAL], indirect=True)
def test_goto_definition_within_file(self, language_server: SolidLanguageServer) -> None:
"""Test goto_definition for a symbol defined within the same file."""
file_path = os.path.join("src", "main.cr")
@@ -1,18 +1,20 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
from test.solidlsp.util.diagnostics import assert_file_diagnostics
pytestmark = [
pytest.mark.crystal,
pytest.mark.skipif(not language_tests_enabled(Language.CRYSTAL), reason="Crystal tests are disabled (crystalline not available)"),
pytest.mark.skipif(
not language_server_tests_enabled(LanguageServerId.CRYSTAL), reason="Crystal tests are disabled (crystalline not available)"
),
]
class TestCrystalDiagnostics:
@pytest.mark.parametrize("language_server", [Language.CRYSTAL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CRYSTAL], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+14 -14
View File
@@ -10,16 +10,16 @@ from solidlsp.language_servers.csharp_language_server import (
breadth_first_file_scan,
find_solution_or_project_file,
)
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import SymbolUtils
from test.conftest import find_identifier_position, get_repo_path, language_has_verified_implementation_support
from test.conftest import find_identifier_position, get_repo_path, ls_has_verified_implementation_support
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@pytest.mark.csharp
class TestCSharpLanguageServer:
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols in the full symbol tree."""
symbols = language_server.request_full_symbol_tree()
@@ -27,7 +27,7 @@ class TestCSharpLanguageServer:
assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator class not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "Add"), "Add method not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_get_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test getting document symbols from a C# file."""
file_path = os.path.join("Program.cs")
@@ -45,7 +45,7 @@ class TestCSharpLanguageServer:
assert "Program" in class_names
assert "Calculator" in class_names
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test finding references using symbol selection range."""
file_path = os.path.join("Program.cs")
@@ -65,7 +65,7 @@ class TestCSharpLanguageServer:
"Program.cs should reference Add method (tried all positions in selectionRange)"
)
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_nested_namespace_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test getting symbols from nested namespace."""
file_path = os.path.join("Models", "Person.cs")
@@ -89,7 +89,7 @@ class TestCSharpLanguageServer:
assert "ToString" in symbol_names, "ToString method not found"
assert "IsAdult" in symbol_names, "IsAdult method not found"
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_find_referencing_symbols_across_files(self, language_server: SolidLanguageServer) -> None:
"""Test finding references to Calculator.Subtract method across files."""
# First, find the Subtract method in Program.cs
@@ -128,7 +128,7 @@ class TestCSharpLanguageServer:
refs_second_call = language_server.request_references(file_path, sel_start["line"], sel_start["character"] + 1)
assert refs_second_call == refs, "Second call to request_references should return the same results"
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_hover_includes_type_information(self, language_server: SolidLanguageServer) -> None:
"""Test that hover information is available and includes type information."""
file_path = os.path.join("Models", "Person.cs")
@@ -172,11 +172,11 @@ class TestCSharpLanguageServer:
assert "bool" in method_hover_text, f"Hover should include 'bool' return type, got: {method_hover_text}"
assert "IsAdult" in method_hover_text, f"Hover should include 'IsAdult' method name, got: {method_hover_text}"
if language_has_verified_implementation_support(Language.CSHARP):
if ls_has_verified_implementation_support(LanguageServerId.CSHARP):
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_find_implementations(self, language_server: SolidLanguageServer) -> None:
repo_path = get_repo_path(Language.CSHARP)
repo_path = get_repo_path(LanguageServerId.CSHARP)
pos = find_identifier_position(repo_path / "Services" / "IGreeter.cs", "FormatGreeting")
assert pos is not None, "Could not find IGreeter.FormatGreeting in fixture"
@@ -186,9 +186,9 @@ class TestCSharpLanguageServer:
f"Expected ConsoleGreeter.FormatGreeting in implementations, got: {implementations}"
)
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_request_implementing_symbols(self, language_server: SolidLanguageServer) -> None:
repo_path = get_repo_path(Language.CSHARP)
repo_path = get_repo_path(LanguageServerId.CSHARP)
pos = find_identifier_position(repo_path / "Services" / "IGreeter.cs", "FormatGreeting")
assert pos is not None, "Could not find IGreeter.FormatGreeting in fixture"
@@ -327,7 +327,7 @@ class TestCSharpSolutionProjectOpening:
# Verify the file actually exists
assert os.path.exists(result)
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.csharp
class TestCsharpDiagnostics:
@pytest.mark.parametrize("language_server", [Language.CSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CSHARP], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+14 -14
View File
@@ -5,7 +5,7 @@ from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@@ -27,14 +27,14 @@ class TestCueLanguageServer:
``cue lsp v0.16.1`` responses against this repository.
"""
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.CUE], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""The server starts and reports the expected repository root."""
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CUE], indirect=True)
def test_document_symbols_schema(self, language_server: SolidLanguageServer) -> None:
"""``schema.cue`` exposes its top-level definitions with the expected hierarchy."""
# request hierarchical document symbols for schema.cue
@@ -54,7 +54,7 @@ class TestCueLanguageServer:
# fields must not also appear at root level (that would be the flat fallback)
assert "name" not in root_names, f"name should be a child of #Person, not a root. Roots: {root_names}"
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CUE], indirect=True)
def test_full_symbol_tree_contains_cross_file_names(self, language_server: SolidLanguageServer) -> None:
"""The repository-wide symbol tree contains definitions from all three CUE files."""
symbols = language_server.request_full_symbol_tree()
@@ -71,8 +71,8 @@ class TestCueLanguageServer:
):
assert SymbolUtils.symbol_tree_contains_name(symbols, expected), f"{expected} missing from full symbol tree"
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.CUE], indirect=True)
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""``#Person`` used in ``main.cue`` resolves to its definition in ``schema.cue``."""
# main.cue line 3 (0-indexed): "alice: #Person & {"
@@ -85,8 +85,8 @@ class TestCueLanguageServer:
assert target["uri"].endswith("schema.cue")
assert target["range"]["start"] == {"line": 3, "character": 0}
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.CUE], indirect=True)
def test_find_references_across_files_person(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""``#Person`` has references in ``lib.cue`` and ``main.cue`` beyond its declaration."""
schema_path = str(repo_path / "schema.cue")
@@ -103,8 +103,8 @@ class TestCueLanguageServer:
assert ("lib.cue", 5) in ref_pairs, f"Expected lib.cue:5 reference, got {sorted(ref_pairs)}"
assert ("main.cue", 3) in ref_pairs, f"Expected main.cue:3 reference, got {sorted(ref_pairs)}"
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.CUE], indirect=True)
def test_find_references_within_file_build_greeting(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""``#BuildGreeting`` (defined in lib.cue) is referenced from main.cue."""
lib_path = str(repo_path / "lib.cue")
@@ -117,8 +117,8 @@ class TestCueLanguageServer:
# use site in main.cue is line 10: "greetingForAlice: (#BuildGreeting & {for_: alice}).result"
assert ("main.cue", 10) in ref_pairs, f"Expected main.cue:10 reference, got {sorted(ref_pairs)}"
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.CUE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CUE], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.CUE], indirect=True)
def test_find_references_default_locale(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""``defaultLocale`` (schema.cue) is used in main.cue."""
schema_path = str(repo_path / "schema.cue")
@@ -130,7 +130,7 @@ class TestCueLanguageServer:
# use site in main.cue is line 13: "locale: defaultLocale"
assert ("main.cue", 13) in ref_pairs, f"Expected main.cue:13 reference, got {sorted(ref_pairs)}"
@pytest.mark.parametrize("language_server", [Language.CUE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.CUE], indirect=True)
def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None:
"""CUE symbols must have bare names (no whitespace/bracket/paren/comma/colon pollution)."""
# `.` is allowed because the synthetic directory symbol for `cue.mod/` contains a literal
+30 -30
View File
@@ -5,7 +5,7 @@ import pytest
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import SymbolUtils
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@@ -13,16 +13,16 @@ from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name,
@pytest.mark.dart
class TestDartLanguageServer:
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DART], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test that the language server starts and stops successfully."""
# The fixture already handles start and stop
assert language_server.is_running()
assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DART], indirect=True)
def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding definition of a method within the same file."""
# In lib/main.dart:
@@ -43,8 +43,8 @@ class TestDartLanguageServer:
# But language server may return different positions
assert definition_location["range"]["start"]["line"] >= 0
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DART], indirect=True)
def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding definition across different files."""
# Test finding definition of MathHelper class which is in helper.dart
@@ -65,8 +65,8 @@ class TestDartLanguageServer:
assert definition_location["uri"].endswith("helper.dart")
assert definition_location["range"]["start"]["line"] >= 0
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DART], indirect=True)
def test_find_definition_class_method(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding definition of a class method."""
# In lib/main.dart:
@@ -86,8 +86,8 @@ class TestDartLanguageServer:
# Definition of power method should be around line 13 (0-indexed)
assert 12 <= definition_location["range"]["start"]["line"] <= 16
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DART], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding references to a method within the same file."""
main_dart_path = str(repo_path / "lib" / "main.dart")
@@ -105,8 +105,8 @@ class TestDartLanguageServer:
main_dart_references = [ref for ref in references if ref["uri"].endswith("main.dart")]
assert len(main_dart_references) >= 1
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DART], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding references across different files."""
helper_dart_path = str(repo_path / "lib" / "helper.dart")
@@ -121,8 +121,8 @@ class TestDartLanguageServer:
main_dart_references = [ref for ref in references if ref["uri"].endswith("main.dart")]
assert len(main_dart_references) >= 1
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DART], indirect=True)
def test_find_definition_constructor(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding definition of a constructor call."""
main_dart_path = str(repo_path / "lib" / "main.dart")
@@ -139,8 +139,8 @@ class TestDartLanguageServer:
# Definition of Calculator class should be around line 3 (0-indexed)
assert 3 <= definition_location["range"]["start"]["line"] <= 7
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.DART], indirect=True)
def test_find_definition_import(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
"""Test finding definition through imports."""
models_dart_path = str(repo_path / "lib" / "models.dart")
@@ -159,7 +159,7 @@ class TestDartLanguageServer:
# This is acceptable behavior - the important thing is that it found a definition
assert "dart" in definition_location["uri"].lower()
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols in the full symbol tree."""
symbols = language_server.request_full_symbol_tree()
@@ -169,7 +169,7 @@ class TestDartLanguageServer:
assert SymbolUtils.symbol_tree_contains_name(symbols, "MathHelper"), "MathHelper class not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "User"), "User class not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test finding references using symbol selection range."""
file_path = os.path.join("lib", "main.dart")
@@ -202,7 +202,7 @@ class TestDartLanguageServer:
"main.dart should reference add method (tried all positions in selectionRange)"
)
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_request_containing_symbol_method(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a method."""
file_path = os.path.join("lib", "main.dart")
@@ -217,7 +217,7 @@ class TestDartLanguageServer:
body = containing_symbol["body"].get_text()
assert "add" in body or "final result" in body
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a class."""
file_path = os.path.join("lib", "main.dart")
@@ -229,7 +229,7 @@ class TestDartLanguageServer:
assert containing_symbol["name"] == "Calculator"
assert containing_symbol["kind"] == SymbolKind.Class
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol with nested scopes."""
file_path = os.path.join("lib", "main.dart")
@@ -241,7 +241,7 @@ class TestDartLanguageServer:
assert containing_symbol["name"] == "add"
assert containing_symbol["kind"] == SymbolKind.Method
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a variable usage."""
file_path = os.path.join("lib", "main.dart")
@@ -259,7 +259,7 @@ class TestDartLanguageServer:
if defining_symbol.get("name") == "add":
assert defining_symbol.get("kind") == SymbolKind.Method.value
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for an imported class/function."""
file_path = os.path.join("lib", "main.dart")
@@ -272,7 +272,7 @@ class TestDartLanguageServer:
# Could be Function or Method depending on language server interpretation
assert defining_symbol.get("kind") in [SymbolKind.Function.value, SymbolKind.Method.value]
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_request_defining_symbol_class_method(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a static class method."""
file_path = os.path.join("lib", "main.dart")
@@ -284,7 +284,7 @@ class TestDartLanguageServer:
assert defining_symbol.get("name") == "power"
assert defining_symbol.get("kind") == SymbolKind.Method.value
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test getting document symbols from a Dart file."""
file_path = os.path.join("lib", "main.dart")
@@ -311,7 +311,7 @@ class TestDartLanguageServer:
# This is acceptable behavior - the important thing is we found the class
pass
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_request_referencing_symbols_comprehensive(self, language_server: SolidLanguageServer) -> None:
"""Test comprehensive referencing symbols functionality."""
file_path = os.path.join("lib", "main.dart")
@@ -340,7 +340,7 @@ class TestDartLanguageServer:
assert "start" in ref["range"]
assert "end" in ref["range"]
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer) -> None:
"""Test symbol resolution across multiple files."""
helper_file_path = os.path.join("lib", "helper.dart")
@@ -363,7 +363,7 @@ class TestDartLanguageServer:
for ref in main_dart_refs:
assert "range" in ref or "location" in ref
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
@@ -376,7 +376,7 @@ class TestDartLanguageServer:
pytrace=False,
)
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_symbol_body_contains_full_method(self, language_server: SolidLanguageServer) -> None:
"""Test that document symbols return the full method body range, not just the identifier.
+2 -2
View File
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.dart
class TestDartDiagnostics:
@pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.DART], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+12 -9
View File
@@ -10,20 +10,23 @@ import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from test.conftest import language_tests_enabled
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
from test.solidlsp.util.diagnostics import assert_file_diagnostics
# These marks will be applied to all tests in this module
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(not language_tests_enabled(Language.ELIXIR), reason="Elixir tests are disabled")]
pytestmark = [
pytest.mark.elixir,
pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ELIXIR), reason="Elixir tests are disabled"),
]
class TestElixirBasic:
"""Basic Elixir language server functionality tests."""
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_references_function_definition(self, language_server: SolidLanguageServer):
"""Test finding references to a function definition."""
file_path = os.path.join("lib", "models.ex")
@@ -52,7 +55,7 @@ class TestElixirBasic:
found_definition = any(ref["uri"].endswith("models.ex") for ref in references)
assert found_definition, "Should find the function definition"
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_references_create_user_function(self, language_server: SolidLanguageServer):
"""Test finding references to create_user function."""
file_path = os.path.join("lib", "services.ex")
@@ -77,7 +80,7 @@ class TestElixirBasic:
assert references is not None
assert len(references) > 0
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer):
"""Test finding symbols that reference a specific function."""
file_path = os.path.join("lib", "models.ex")
@@ -101,7 +104,7 @@ class TestElixirBasic:
assert referencing_symbols is not None
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_timeout_enumeration_bug(self, language_server: SolidLanguageServer):
"""Test that enumeration doesn't timeout (regression test)."""
# This should complete without timing out
@@ -113,7 +116,7 @@ class TestElixirBasic:
symbols = language_server.request_document_symbols("lib/services.ex").get_all_symbols_and_roots()
assert symbols is not None
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
@@ -139,7 +142,7 @@ class TestElixirBasic:
pytrace=False,
)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
@@ -5,11 +5,14 @@ from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled, start_ls_context
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled, start_ls_context
# These marks will be applied to all tests in this module
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(not language_tests_enabled(Language.ELIXIR), reason="Elixir tests are disabled")]
pytestmark = [
pytest.mark.elixir,
pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ELIXIR), reason="Elixir tests are disabled"),
]
# Skip slow tests in CI - they require multiple Expert instances which is too slow
IN_CI = bool(os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS"))
@@ -26,7 +29,7 @@ def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
Uses session scope to avoid restarting Expert for each test.
"""
ignored_paths = ["scripts", "ignored_dir"]
with start_ls_context(language=Language.ELIXIR, ignored_paths=ignored_paths) as ls:
with start_ls_context(ls_id=LanguageServerId.ELIXIR, ignored_paths=ignored_paths) as ls:
yield ls
@@ -81,7 +84,7 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
@pytest.mark.slow
@SKIP_SLOW_IN_CI
@pytest.mark.parametrize("repo_path", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ELIXIR], indirect=True)
def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
"""Tests that refs and symbols with glob patterns are ignored.
@@ -89,7 +92,7 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
which adds ~60-90s startup time.
"""
ignored_paths = ["*cripts", "ignored_*"] # codespell:ignore cripts
with start_ls_context(language=Language.ELIXIR, repo_path=str(repo_path), ignored_paths=ignored_paths) as ls:
with start_ls_context(ls_id=LanguageServerId.ELIXIR, repo_path=str(repo_path), ignored_paths=ignored_paths) as ls:
# Same as in the above tests
root = ls.request_full_symbol_tree()[0]
root_children = root["children"]
@@ -121,7 +124,7 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)"
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_default_ignored_directories(language_server: SolidLanguageServer):
"""Test that default Elixir directories are ignored."""
# Test that Elixir-specific directories are ignored by default
@@ -142,7 +145,7 @@ def test_default_ignored_directories(language_server: SolidLanguageServer):
reason="Expert 0.1.0 bug: document_symbols may return nil for some files (flaky)",
raises=Exception,
)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
"""Test that symbol tree excludes build and dependency directories."""
symbol_tree = language_server.request_full_symbol_tree()
@@ -12,11 +12,14 @@ import pytest
from serena.project import Project
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
# These marks will be applied to all tests in this module
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(not language_tests_enabled(Language.ELIXIR), reason="Elixir tests are disabled")]
pytestmark = [
pytest.mark.elixir,
pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ELIXIR), reason="Elixir tests are disabled"),
]
class TestElixirIntegration:
@@ -42,7 +45,7 @@ class TestElixirIntegration:
assert (repo_path / "test" / "test_repo_test.exs").exists(), "test file should exist"
assert (repo_path / "test" / "models_test.exs").exists(), "models test should exist"
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer):
"""Test that symbols can be resolved across different files."""
# Test that User struct from models.ex can be found when referenced in services.ex
@@ -67,7 +70,7 @@ class TestElixirIntegration:
# Should point to models.ex
assert "models.ex" in defining_symbol["location"]["uri"]
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_module_hierarchy_understanding(self, language_server: SolidLanguageServer):
"""Test that the language server understands Elixir module hierarchy."""
models_file = os.path.join("lib", "models.ex")
@@ -91,7 +94,7 @@ class TestElixirIntegration:
def test_file_extension_matching(self):
"""Test that the Elixir language recognizes the correct file extensions."""
language = Language.ELIXIR
language = LanguageServerId.ELIXIR
matcher = language.get_source_fn_matcher()
# Test Elixir file extensions
@@ -110,7 +113,7 @@ class TestElixirIntegration:
class TestElixirProject:
@pytest.mark.parametrize("project", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("project", [LanguageServerId.ELIXIR], indirect=True)
def test_comprehensive_symbol_search(self, project: Project):
"""Test comprehensive symbol search across the entire project."""
# Search for all function definitions
@@ -138,7 +141,7 @@ class TestElixirProject:
models_structs = [m for m in struct_matches if m.source_file_path and "models.ex" in m.source_file_path]
assert len(models_structs) > 0, "Should find struct definitions in models.ex"
@pytest.mark.parametrize("project", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("project", [LanguageServerId.ELIXIR], indirect=True)
def test_protocol_and_implementation_understanding(self, project: Project):
"""Test that the language server understands Elixir protocols and implementations."""
# Search for protocol definitions
@@ -12,12 +12,15 @@ import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from test.conftest import language_tests_enabled
from test.conftest import language_server_tests_enabled
# These marks will be applied to all tests in this module
pytestmark = [pytest.mark.elixir, pytest.mark.skipif(not language_tests_enabled(Language.ELIXIR), reason="Elixir tests are disabled")]
pytestmark = [
pytest.mark.elixir,
pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ELIXIR), reason="Elixir tests are disabled"),
]
class TestElixirLanguageServerSymbols:
@@ -26,7 +29,7 @@ class TestElixirLanguageServerSymbols:
@pytest.mark.xfail(
reason="Expert 0.1.0 bug: document_symbols returns nil for some files (FunctionClauseError in XPExpert.EngineApi.document_symbols/2)"
)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a function."""
# Test for a position inside the create_user function
@@ -54,7 +57,7 @@ class TestElixirLanguageServerSymbols:
if "body" in containing_symbol:
assert "def create_user" in containing_symbol["body"].get_text()
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_containing_symbol_module(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a module."""
# Test for a position inside the UserService module but outside any function
@@ -79,7 +82,7 @@ class TestElixirLanguageServerSymbols:
assert "UserService" in containing_symbol["name"]
assert containing_symbol["kind"] == SymbolKind.Module or containing_symbol["kind"] == SymbolKind.Class
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol with nested scopes."""
# Test for a position inside a function which is inside a module
@@ -104,7 +107,7 @@ class TestElixirLanguageServerSymbols:
expected_names = ["create_user", "UserService"]
assert any(name in containing_symbol["name"] for name in expected_names)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a position with no containing symbol."""
# Test for a position outside any function/module (e.g., in module doc)
@@ -116,7 +119,7 @@ class TestElixirLanguageServerSymbols:
# This is acceptable behavior for module-level positions
assert containing_symbol is None or containing_symbol == {} or "TestRepo.Services" in str(containing_symbol)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_referencing_symbols_struct(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a struct."""
# Test referencing symbols for User struct
@@ -146,7 +149,7 @@ class TestElixirLanguageServerSymbols:
# We expect some references from services.ex
assert len(services_references) >= 0 # At least attempt to find references
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a position with no symbol."""
file_path = os.path.join("lib", "services.ex")
@@ -164,7 +167,7 @@ class TestElixirLanguageServerSymbols:
@pytest.mark.xfail(
reason="Expert 0.1.0 bug: definition request crashes (FunctionClauseError in XPExpert.Protocol.Conversions.to_elixir/2)"
)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_defining_symbol_function_call(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a function call."""
# Find a place where User.new is called in services.ex
@@ -191,7 +194,7 @@ class TestElixirLanguageServerSymbols:
@pytest.mark.xfail(
reason="Expert 0.1.0 bug: definition request crashes (FunctionClauseError in XPExpert.Protocol.Conversions.to_elixir/2)"
)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_defining_symbol_struct_usage(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a struct usage."""
# Find a place where User struct is used in services.ex
@@ -215,7 +218,7 @@ class TestElixirLanguageServerSymbols:
@pytest.mark.xfail(
reason="Expert 0.1.0 bug: definition request crashes (FunctionClauseError in XPExpert.Protocol.Conversions.to_elixir/2)"
)
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a position with no symbol."""
# Test for a position with no symbol (e.g., whitespace or comment)
@@ -226,7 +229,7 @@ class TestElixirLanguageServerSymbols:
# Should return None or empty
assert defining_symbol is None or defining_symbol == {}
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
"""Test integration between different symbol methods."""
file_path = os.path.join("lib", "models.ex")
@@ -257,7 +260,7 @@ class TestElixirLanguageServerSymbols:
assert isinstance(refs, list)
@pytest.mark.xfail(reason="Flaky test, sometimes fails with an Expert-internal error")
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
"""Test that symbol tree structure is correctly built."""
symbol_tree = language_server.request_full_symbol_tree()
@@ -283,7 +286,7 @@ class TestElixirLanguageServerSymbols:
found_modules = [name for name in expected_modules if name in file_names]
assert len(found_modules) > 0, f"Expected to find some modules from {expected_modules}, but got {file_names}"
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
"""Test request_dir_overview functionality."""
lib_overview = language_server.request_dir_overview("lib")
@@ -321,7 +324,7 @@ class TestElixirLanguageServerSymbols:
# found_terms = [term for term in expected_terms if term in overview_text]
# assert len(found_terms) > 0, f"Expected to find some terms from {expected_terms} in overview"
@pytest.mark.parametrize("language_server", [Language.ELIXIR], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELIXIR], indirect=True)
def test_containing_symbol_of_module_attribute(self, language_server: SolidLanguageServer) -> None:
"""Test containing symbol for module attributes."""
file_path = os.path.join("lib", "models.ex")
+7 -7
View File
@@ -3,16 +3,16 @@ import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from test.conftest import language_tests_enabled
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@pytest.mark.skipif(not language_tests_enabled(Language.ELM), reason="Elm tests are disabled (elm compiler not available)")
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ELM), reason="Elm tests are disabled (elm compiler not available)")
@pytest.mark.elm
class TestElmLanguageServer:
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELM], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
symbols = language_server.request_full_symbol_tree()
assert SymbolUtils.symbol_tree_contains_name(symbols, "greet"), "greet function not found in symbol tree"
@@ -20,7 +20,7 @@ class TestElmLanguageServer:
assert SymbolUtils.symbol_tree_contains_name(symbols, "formatMessage"), "formatMessage function not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "addNumbers"), "addNumbers function not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELM], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("Main.elm")
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
@@ -34,7 +34,7 @@ class TestElmLanguageServer:
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Main.elm should reference greet function"
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELM], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
# Test formatMessage function which is defined in Utils.elm and used in Main.elm
utils_path = os.path.join("Utils.elm")
@@ -56,7 +56,7 @@ class TestElmLanguageServer:
# Verify that at least one reference is in Main.elm (where formatMessage is used)
assert any("Main.elm" in ref.get("relativePath", "") for ref in refs), "Expected to find usage of formatMessage in Main.elm"
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELM], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
+2 -2
View File
@@ -3,14 +3,14 @@ import shutil
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.elm
@pytest.mark.skipif(shutil.which("node") is None or shutil.which("npm") is None, reason="Elm diagnostics require Node.js and npm")
class TestElmDiagnostics:
@pytest.mark.parametrize("language_server", [Language.ELM], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ELM], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+8 -8
View File
@@ -8,24 +8,24 @@ like request_references using the test repository.
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.erlang
@pytest.mark.skipif(not language_tests_enabled(Language.ERLANG), reason="Erlang tests are disabled")
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ERLANG), reason="Erlang tests are disabled")
class TestErlangLanguageServerBasics:
"""Test basic functionality of the Erlang language server."""
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_language_server_initialization(self, language_server: SolidLanguageServer) -> None:
"""Test that the Erlang language server initializes properly."""
assert language_server is not None
assert language_server.language == Language.ERLANG
assert language_server.ls_id == LanguageServerId.ERLANG
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test document symbols retrieval for Erlang files."""
try:
@@ -43,7 +43,7 @@ class TestErlangLanguageServerBasics:
else:
raise
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
@@ -56,7 +56,7 @@ class TestErlangLanguageServerBasics:
pytrace=False,
)
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
@@ -4,13 +4,13 @@ from pathlib import Path
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from test.conftest import language_tests_enabled, start_ls_context
from solidlsp.ls_config import LanguageServerId
from test.conftest import language_server_tests_enabled, start_ls_context
# These marks will be applied to all tests in this module
pytestmark = [
pytest.mark.erlang,
pytest.mark.skipif(not language_tests_enabled(Language.ERLANG), reason="Erlang tests are disabled"),
pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ERLANG), reason="Erlang tests are disabled"),
]
@@ -18,13 +18,13 @@ pytestmark = [
def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
"""Fixture to set up an LS for the erlang test repo with the 'ignored_dir' directory ignored."""
ignored_paths = ["_build", "ignored_dir"]
with start_ls_context(language=Language.ERLANG, ignored_paths=ignored_paths) as ls:
with start_ls_context(ls_id=LanguageServerId.ERLANG, ignored_paths=ignored_paths) as ls:
yield ls
@pytest.mark.timeout(60) # Add 60 second timeout
@pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("ls_with_ignored_dirs", [LanguageServerId.ERLANG], indirect=True)
def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
"""Tests that request_full_symbol_tree ignores the configured directory."""
root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
@@ -41,7 +41,7 @@ def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
@pytest.mark.timeout(60) # Add 60 second timeout
@pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("ls_with_ignored_dirs", [LanguageServerId.ERLANG], indirect=True)
def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
"""Tests that find_references ignores the configured directory."""
# Location of user record, which might be referenced in ignored_dir
@@ -68,11 +68,11 @@ def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
@pytest.mark.timeout(60) # Add 60 second timeout
@pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
@pytest.mark.parametrize("repo_path", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("repo_path", [LanguageServerId.ERLANG], indirect=True)
def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
"""Tests that refs and symbols with glob patterns are ignored."""
ignored_paths = ["_build*", "ignored_*", "*.tmp"]
with start_ls_context(language=Language.ERLANG, repo_path=str(repo_path), ignored_paths=ignored_paths) as ls:
with start_ls_context(ls_id=LanguageServerId.ERLANG, repo_path=str(repo_path), ignored_paths=ignored_paths) as ls:
# Same as in the above tests
root = ls.request_full_symbol_tree()[0]
root_children = root["children"]
@@ -105,7 +105,7 @@ def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)"
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_default_ignored_directories(language_server: SolidLanguageServer):
"""Test that default Erlang directories are ignored."""
# Test that Erlang-specific directories are ignored by default
@@ -123,7 +123,7 @@ def test_default_ignored_directories(language_server: SolidLanguageServer):
assert not language_server.is_ignored_dirname("priv"), "priv should not be ignored"
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
"""Test that symbol tree excludes build and dependency directories."""
symbol_tree = language_server.request_full_symbol_tree()
@@ -143,7 +143,7 @@ def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}"
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_ignore_compiled_files(language_server: SolidLanguageServer):
"""Test that compiled Erlang files are ignored."""
# Test that beam files are ignored
@@ -155,7 +155,7 @@ def test_ignore_compiled_files(language_server: SolidLanguageServer):
assert not language_server.is_ignored_filename("records.hrl"), "Header files should not be ignored"
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_rebar_directories_ignored(language_server: SolidLanguageServer):
"""Test that rebar-specific directories are ignored."""
# Test rebar3-specific directories
@@ -168,7 +168,7 @@ def test_rebar_directories_ignored(language_server: SolidLanguageServer):
assert not language_server.is_ignored_filename("rebar.lock"), "rebar.lock should not be ignored"
@pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("ls_with_ignored_dirs", [LanguageServerId.ERLANG], indirect=True)
def test_document_symbols_ignores_dirs(ls_with_ignored_dirs: SolidLanguageServer):
"""Test that document symbols from ignored directories are not included."""
# Try to get symbols from a file in ignored directory (should not find it)
@@ -183,7 +183,7 @@ def test_document_symbols_ignores_dirs(ls_with_ignored_dirs: SolidLanguageServer
pass
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_erlang_specific_ignore_patterns(language_server: SolidLanguageServer):
"""Test Erlang-specific ignore patterns work correctly."""
erlang_ignored_dirs = ["_build", "ebin", ".rebar3", "_checkouts", "cover"]
@@ -12,21 +12,21 @@ import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from test.conftest import language_tests_enabled
from test.conftest import language_server_tests_enabled
# These marks will be applied to all tests in this module
pytestmark = [
pytest.mark.erlang,
pytest.mark.skipif(not language_tests_enabled(Language.ERLANG), reason="Erlang tests are disabled"),
pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.ERLANG), reason="Erlang tests are disabled"),
]
class TestErlangLanguageServerSymbols:
"""Test the Erlang language server's symbol-related functionality."""
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a function."""
# Test for a position inside the create_user function
@@ -53,7 +53,7 @@ class TestErlangLanguageServerSymbols:
if "body" in containing_symbol:
assert "create_user" in containing_symbol["body"].get_text()
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_containing_symbol_module(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a module."""
# Test for a position inside the models module but outside any function
@@ -78,7 +78,7 @@ class TestErlangLanguageServerSymbols:
assert "models" in containing_symbol["name"] or "module" in containing_symbol["name"].lower()
assert containing_symbol["kind"] == SymbolKind.Module or containing_symbol["kind"] == SymbolKind.Class
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol with nested scopes."""
# Test for a position inside a function which is inside a module
@@ -107,7 +107,7 @@ class TestErlangLanguageServerSymbols:
expected_names = ["create_user", "models"]
assert any(name in containing_symbol["name"] for name in expected_names)
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
"""Test request_containing_symbol for a position with no containing symbol."""
# Test for a position outside any function/module (e.g., in comments)
@@ -119,7 +119,7 @@ class TestErlangLanguageServerSymbols:
# This is acceptable behavior for module-level positions
assert containing_symbol is None or containing_symbol == {} or "models" in str(containing_symbol)
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_referencing_symbols_record(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a record."""
# Test referencing symbols for user record
@@ -149,7 +149,7 @@ class TestErlangLanguageServerSymbols:
# We expect some references from models.erl
assert len(models_references) >= 0 # At least attempt to find references
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a function."""
# Test referencing symbols for create_user function
@@ -181,7 +181,7 @@ class TestErlangLanguageServerSymbols:
]
assert len(service_references) >= 0 # At least attempt to find references
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
"""Test request_referencing_symbols for a position with no symbol."""
file_path = os.path.join("src", "models.erl")
@@ -196,7 +196,7 @@ class TestErlangLanguageServerSymbols:
pass
# Tests for request_defining_symbol
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_defining_symbol_function_call(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a function call."""
# Find a place where models:create_user is called in services.erl
@@ -220,7 +220,7 @@ class TestErlangLanguageServerSymbols:
if "location" in defining_symbol and "uri" in defining_symbol["location"]:
assert "models.erl" in defining_symbol["location"]["uri"]
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_defining_symbol_record_usage(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a record usage."""
# Find a place where #user{} record is used in models.erl
@@ -243,7 +243,7 @@ class TestErlangLanguageServerSymbols:
if "location" in defining_symbol and "uri" in defining_symbol["location"]:
assert "records.hrl" in defining_symbol["location"]["uri"]
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_defining_symbol_module_call(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a module function call."""
# Find a place where utils:validate_input is called
@@ -264,7 +264,7 @@ class TestErlangLanguageServerSymbols:
if defining_symbol:
assert "validate" in defining_symbol.get("name", "") or "email" in defining_symbol.get("name", "")
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
"""Test request_defining_symbol for a position with no symbol."""
# Test for a position with no symbol (e.g., whitespace or comment)
@@ -275,7 +275,7 @@ class TestErlangLanguageServerSymbols:
# Should return None or empty
assert defining_symbol is None or defining_symbol == {}
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
"""Test integration between different symbol methods."""
file_path = os.path.join("src", "models.erl")
@@ -310,7 +310,7 @@ class TestErlangLanguageServerSymbols:
reason="Known intermittent timeout issue in Erlang LS in CI environments. May pass locally but can timeout on slower CI systems.",
strict=False,
)
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
"""Test that symbol tree structure is correctly built."""
symbol_tree = language_server.request_full_symbol_tree()
@@ -336,7 +336,7 @@ class TestErlangLanguageServerSymbols:
found_modules = [name for name in expected_modules if any(name in fname for fname in file_names)]
assert len(found_modules) > 0, f"Expected to find some modules from {expected_modules}, but got {file_names}"
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
"""Test request_dir_overview functionality."""
src_overview = language_server.request_dir_overview("src")
@@ -353,7 +353,7 @@ class TestErlangLanguageServerSymbols:
found_terms = [term for term in expected_terms if term in overview_text]
assert len(found_terms) > 0, f"Expected to find some terms from {expected_terms} in overview"
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_containing_symbol_of_record_field(self, language_server: SolidLanguageServer) -> None:
"""Test containing symbol for record field access."""
file_path = os.path.join("src", "models.erl")
@@ -378,7 +378,7 @@ class TestErlangLanguageServerSymbols:
expected_names = ["create_user", "update_user", "format_user_info"]
assert any(name in containing_symbol["name"] for name in expected_names)
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_containing_symbol_of_spec(self, language_server: SolidLanguageServer) -> None:
"""Test containing symbol for function specs."""
file_path = os.path.join("src", "models.erl")
@@ -410,7 +410,7 @@ class TestErlangLanguageServerSymbols:
"Similar to known Next LS timeout issues.",
strict=False,
)
@pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.ERLANG], indirect=True)
def test_referencing_symbols_across_files(self, language_server: SolidLanguageServer) -> None:
"""Test finding references across different files."""
# Test that we can find references to models module functions in services.erl
+16 -16
View File
@@ -8,10 +8,10 @@ Note: These tests require fortls to be installed: pip install fortls
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import SymbolUtils
from test.conftest import find_identifier_position, get_repo_path, language_has_verified_implementation_support
from test.conftest import find_identifier_position, get_repo_path, ls_has_verified_implementation_support
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
# Mark all tests in this module as fortran tests
@@ -21,7 +21,7 @@ pytestmark = pytest.mark.fortran
class TestFortranLanguageServer:
"""Test Fortran language server functionality."""
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols using request_full_symbol_tree."""
symbols = language_server.request_full_symbol_tree()
@@ -39,11 +39,11 @@ class TestFortranLanguageServer:
# Verify subroutine symbol
assert SymbolUtils.symbol_tree_contains_name(symbols, "print_result"), "print_result subroutine not found in symbol tree"
if language_has_verified_implementation_support(Language.FORTRAN):
if ls_has_verified_implementation_support(LanguageServerId.FORTRAN):
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_find_implementations(self, language_server: SolidLanguageServer) -> None:
repo_path = get_repo_path(Language.FORTRAN)
repo_path = get_repo_path(LanguageServerId.FORTRAN)
pos = find_identifier_position(repo_path / "modules" / "geometry.f90", "distance")
assert pos is not None, "Could not find interface distance in geometry.f90"
@@ -53,9 +53,9 @@ class TestFortranLanguageServer:
assert implementation_files == {"modules/geometry.f90"}, f"Unexpected implementation locations: {implementations}"
assert len(implementations) >= 2, f"Expected module procedure implementations, got: {implementations}"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_request_implementing_symbols(self, language_server: SolidLanguageServer) -> None:
repo_path = get_repo_path(Language.FORTRAN)
repo_path = get_repo_path(LanguageServerId.FORTRAN)
pos = find_identifier_position(repo_path / "modules" / "geometry.f90", "distance")
assert pos is not None, "Could not find interface distance in geometry.f90"
@@ -66,7 +66,7 @@ class TestFortranLanguageServer:
f"Expected distance_2d and distance_3d, got: {implementing_symbols}"
)
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test that document symbols can be retrieved from Fortran files."""
# Test main.f90 - should have a program symbol
@@ -82,7 +82,7 @@ class TestFortranLanguageServer:
assert "multiply_numbers" in all_names, f"Function 'multiply_numbers' not found. Found: {all_names}"
assert "print_result" in all_names, f"Subroutine 'print_result' not found. Found: {all_names}"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_find_references_cross_file(self, language_server: SolidLanguageServer) -> None:
"""Test finding references across files using low-level request_references.
@@ -116,7 +116,7 @@ class TestFortranLanguageServer:
f"Expected to find reference in main.f90, but found references in: {[ref.get('relativePath') for ref in refs]}"
)
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_find_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
"""Test finding definition across files using request_definition."""
# In main.f90, line 7 (0-indexed: line 6) contains: result = add_numbers(5.0, 3.0)
@@ -142,7 +142,7 @@ class TestFortranLanguageServer:
f"Expected definition at line 4, but found at line {definition_location['range']['start']['line']}"
)
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_request_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols that reference a function - Serena's high-level API.
@@ -189,7 +189,7 @@ class TestFortranLanguageServer:
# because it depends on finding containing symbols for each reference. We verify that
# the API works and returns valid symbols with proper structure.
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_request_defining_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding the defining symbol - Serena's high-level API.
@@ -218,7 +218,7 @@ class TestFortranLanguageServer:
defining_path = defining_symbol["location"]["relativePath"]
assert "math_utils.f90" in defining_path, f"Expected definition to be in math_utils.f90, but found in: {defining_path}"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_request_containing_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding the containing symbol for a position in the code."""
# Test finding the containing symbol for a position inside the add_numbers function
@@ -246,7 +246,7 @@ class TestFortranLanguageServer:
assert "range" in location, "Location should contain range information"
assert "start" in location["range"] and "end" in location["range"], "Range should have start and end positions"
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_type_and_interface_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test that type definitions and interfaces are properly recognized with corrected selectionRange.
@@ -302,7 +302,7 @@ class TestFortranLanguageServer:
# refs might be empty if Point3D isn't used elsewhere, but the call should not fail
# The important thing is that it doesn't error due to wrong character position
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.fortran
class TestFortranDiagnostics:
@pytest.mark.parametrize("language_server", [Language.FORTRAN], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FORTRAN], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+23 -20
View File
@@ -5,23 +5,26 @@ from typing import Any
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from test.conftest import (
find_identifier_position,
get_repo_path,
language_has_verified_implementation_support,
language_tests_enabled,
language_server_tests_enabled,
ls_has_verified_implementation_support,
)
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
from test.solidlsp.util.diagnostics import assert_file_diagnostics
# Currently, most F# tests fail (regression/instability), so the suite is disabled on CI.
pytestmark = [pytest.mark.fsharp, pytest.mark.skipif(not language_tests_enabled(Language.FSHARP), reason="F# tests are disabled")]
pytestmark = [
pytest.mark.fsharp,
pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.FSHARP), reason="F# tests are disabled"),
]
class TestFSharpLanguageServer:
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
"""Test finding symbols in the full symbol tree."""
symbols = language_server.request_full_symbol_tree()
@@ -35,7 +38,7 @@ class TestFSharpLanguageServer:
assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add function not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "CalculatorClass"), "CalculatorClass not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_get_document_symbols_program(self, language_server: SolidLanguageServer) -> None:
"""Test getting document symbols from the main Program.fs file."""
file_path = os.path.join("Program.fs")
@@ -45,11 +48,11 @@ class TestFSharpLanguageServer:
symbol_names = [s.get("name") for s in symbols]
assert "main" in symbol_names, "main function not found in Program.fs symbols"
if language_has_verified_implementation_support(Language.FSHARP):
if ls_has_verified_implementation_support(LanguageServerId.FSHARP):
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_find_implementations(self, language_server: SolidLanguageServer) -> None:
repo_path = get_repo_path(Language.FSHARP)
repo_path = get_repo_path(LanguageServerId.FSHARP)
pos = find_identifier_position(repo_path / "Formatter.fs", "FormatGreeting")
assert pos is not None, "Could not find IGreeter.FormatGreeting in fixture"
@@ -59,9 +62,9 @@ class TestFSharpLanguageServer:
f"Expected ConsoleGreeter.FormatGreeting in implementations, got: {implementations}"
)
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_request_implementing_symbols(self, language_server: SolidLanguageServer) -> None:
repo_path = get_repo_path(Language.FSHARP)
repo_path = get_repo_path(LanguageServerId.FSHARP)
pos = find_identifier_position(repo_path / "Formatter.fs", "FormatGreeting")
assert pos is not None, "Could not find IGreeter.FormatGreeting in fixture"
@@ -72,7 +75,7 @@ class TestFSharpLanguageServer:
for symbol in implementing_symbols
), f"Expected ConsoleGreeter.FormatGreeting symbol, got: {implementing_symbols}"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_get_document_symbols_calculator(self, language_server: SolidLanguageServer) -> None:
"""Test getting document symbols from Calculator.fs file."""
file_path = os.path.join("Calculator.fs")
@@ -85,7 +88,7 @@ class TestFSharpLanguageServer:
for expected in expected_symbols:
assert expected in symbol_names, f"{expected} function not found in Calculator.fs symbols"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test finding references using symbol selection range."""
file_path = os.path.join("Calculator.fs")
@@ -108,7 +111,7 @@ class TestFSharpLanguageServer:
# The add function should be referenced in Program.fs
assert any("Program.fs" in ref.get("relativePath", "") for ref in refs), "Program.fs should reference add function"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_nested_module_symbols(self, language_server: SolidLanguageServer) -> None:
"""Test getting symbols from nested Models namespace."""
file_path = os.path.join("Models", "Person.fs")
@@ -121,7 +124,7 @@ class TestFSharpLanguageServer:
for expected in expected_symbols:
assert expected in symbol_names, f"{expected} not found in Person.fs symbols"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_find_referencing_symbols_across_files(self, language_server: SolidLanguageServer) -> None:
"""Test finding references to Calculator functions across files."""
# Find the subtract function in Calculator.fs
@@ -143,7 +146,7 @@ class TestFSharpLanguageServer:
# The subtract function should be referenced in Program.fs
assert any("Program.fs" in ref.get("relativePath", "") for ref in refs), "Program.fs should reference subtract function"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_go_to_definition(self, language_server: SolidLanguageServer) -> None:
"""Test go-to-definition functionality."""
# Test going to definition of 'add' function from Program.fs
@@ -156,7 +159,7 @@ class TestFSharpLanguageServer:
# We should get at least some definitions
assert len(definitions) >= 0, "Should get definitions (even if empty for complex cases)"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_hover_information(self, language_server: SolidLanguageServer) -> None:
"""Test hover information functionality."""
file_path = os.path.join("Calculator.fs")
@@ -168,7 +171,7 @@ class TestFSharpLanguageServer:
# This is acceptable as it depends on the LSP server's capabilities and timing
assert hover_info is None or isinstance(hover_info, dict), "Hover info should be None or dict"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_completion(self, language_server: SolidLanguageServer) -> None:
"""Test code completion functionality."""
file_path = os.path.join("Program.fs")
@@ -197,7 +200,7 @@ class TestFSharpLanguageServer:
assert isinstance(result["value"], list), "Completions should be a list"
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
@@ -206,7 +209,7 @@ class TestFSharpLanguageServer:
min_count=1,
)
@pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.FSHARP], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
+20 -20
View File
@@ -5,22 +5,22 @@ import pytest
from serena.symbol import LanguageServerSymbol
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from test.conftest import find_identifier_position, get_repo_path, language_has_verified_implementation_support
from test.conftest import find_identifier_position, get_repo_path, ls_has_verified_implementation_support
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@pytest.mark.go
class TestGoLanguageServer:
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
symbols = language_server.request_full_symbol_tree()
assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "Helper"), "Helper function not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "DemoStruct"), "DemoStruct not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)
def test_find_symbol_matches_go_method_by_bare_name(self, language_server: SolidLanguageServer) -> None:
symbols = language_server.request_full_symbol_tree(within_relative_path="main.go")
@@ -34,7 +34,7 @@ class TestGoLanguageServer:
assert bare_name_matches, "Expected a Go method to match by bare name"
assert all(match.name == "Value" for match in bare_name_matches)
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)
def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("main.go")
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
@@ -48,7 +48,7 @@ class TestGoLanguageServer:
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])
assert any("main.go" in ref.get("uri", "") for ref in refs), "Expected at least one reference result to point at main.go"
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)
def test_type_var_const_body_includes_leading_keyword(self, language_server: SolidLanguageServer) -> None:
"""
Single ``type``/``var``/``const`` declarations must expose a body and replacement range that
@@ -88,11 +88,11 @@ class TestGoLanguageServer:
assert body.startswith(name), f"Expected grouped var {name} body to start with the identifier, got {body[:24]!r}"
assert not body.startswith("var"), f"Grouped var {name} body must not include the 'var' keyword"
if language_has_verified_implementation_support(Language.GO):
if ls_has_verified_implementation_support(LanguageServerId.GO):
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)
def test_find_implementations(self, language_server: SolidLanguageServer) -> None:
repo_path = get_repo_path(Language.GO)
repo_path = get_repo_path(LanguageServerId.GO)
pos = find_identifier_position(repo_path / "main.go", "FormatGreeting")
assert pos is not None, "Could not find Greeter.FormatGreeting in fixture"
@@ -102,9 +102,9 @@ class TestGoLanguageServer:
f"Expected ConsoleGreeter.FormatGreeting in implementations, got: {implementations}"
)
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)
def test_request_implementing_symbols(self, language_server: SolidLanguageServer) -> None:
repo_path = get_repo_path(Language.GO)
repo_path = get_repo_path(LanguageServerId.GO)
pos = find_identifier_position(repo_path / "main.go", "FormatGreeting")
assert pos is not None, "Could not find Greeter.FormatGreeting in fixture"
@@ -133,7 +133,7 @@ class TestGoBuildTags:
from test.conftest import get_repo_path
fixture_path = get_repo_path(Language.GO)
fixture_path = get_repo_path(LanguageServerId.GO)
target_path = tmp_path / "test_repo"
shutil.copytree(fixture_path, target_path)
@@ -145,7 +145,7 @@ class TestGoBuildTags:
repo_path = self._copy_go_fixture(tmp_path)
with start_ls_context(Language.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls:
with start_ls_context(LanguageServerId.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls:
xnotfoo_symbols = ls.request_workspace_symbol("XNotFoo")
xfoo_symbols = ls.request_workspace_symbol("XFoo")
@@ -162,14 +162,14 @@ class TestGoBuildTags:
repo_path = self._copy_go_fixture(tmp_path)
ls_settings = {
Language.GO: {
LanguageServerId.GO: {
"gopls_settings": {
"buildFlags": ["-tags=foo"],
},
},
}
with start_ls_context(Language.GO, repo_path=str(repo_path), ls_specific_settings=ls_settings, solidlsp_dir=tmp_path) as ls:
with start_ls_context(LanguageServerId.GO, repo_path=str(repo_path), ls_specific_settings=ls_settings, solidlsp_dir=tmp_path) as ls:
xfoo_symbols = ls.request_workspace_symbol("XFoo")
xnotfoo_symbols = ls.request_workspace_symbol("XNotFoo")
@@ -188,7 +188,7 @@ class TestGoBuildTags:
repo_path = self._copy_go_fixture(tmp_path)
ls_settings_foo = {
Language.GO: {
LanguageServerId.GO: {
"gopls_settings": {
"buildFlags": ["-tags=foo"],
},
@@ -214,7 +214,7 @@ class TestGoBuildTags:
assert ls._document_symbols_cache_is_modified
# Run 1 (default context): populate caches and persist them to disk.
with start_ls_context(Language.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default:
with start_ls_context(LanguageServerId.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default:
_ = ls_default.request_document_symbols(main_go)
default_raw_cache_version = ls_default._raw_document_symbols_cache_version()
@@ -243,7 +243,7 @@ class TestGoBuildTags:
)
# Run 2 (default context again): prove that persisted caches are actually loaded and used.
with start_ls_context(Language.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again:
with start_ls_context(LanguageServerId.GO, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again:
assert ls_default_again.cache_dir == cache_dir
_assert_caches_loaded_and_clean(ls_default_again)
@@ -256,7 +256,7 @@ class TestGoBuildTags:
# Run 3 (foo context): the same on-disk cache directory exists, but MUST be treated as stale.
with start_ls_context(
Language.GO,
LanguageServerId.GO,
repo_path=str(repo_path),
ls_specific_settings=ls_settings_foo,
solidlsp_dir=tmp_path,
@@ -277,7 +277,7 @@ class TestGoBuildTags:
# A cache miss should repopulate and mark caches modified.
_assert_caches_modified(ls_foo)
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
+2 -2
View File
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.go
class TestGoDiagnostics:
@pytest.mark.parametrize("language_server", [Language.GO], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.GO], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+3 -3
View File
@@ -5,7 +5,7 @@ import pytest
from serena.constants import SERENA_MANAGED_DIR_NAME
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language, LanguageServerConfig
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.ls_utils import SymbolUtils
from solidlsp.settings import SolidLSPSettings
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@@ -43,13 +43,13 @@ class TestGroovyLanguageServer:
# Create language server directly with Groovy-specific settings
repo_path = str(cls.test_repo_path)
config = LanguageServerConfig(code_language=Language.GROOVY, ignored_paths=[], trace_lsp_communication=False)
config = LanguageServerConfig(ls_id=LanguageServerId.GROOVY, ignored_paths=[], trace_lsp_communication=False)
project_data_path = os.path.join(repo_path, SERENA_MANAGED_DIR_NAME)
solidlsp_settings = SolidLSPSettings(
solidlsp_dir=str(Path.home() / ".serena"),
project_data_path=project_data_path,
ls_specific_settings={Language.GROOVY: groovy_settings},
ls_specific_settings={LanguageServerId.GROOVY: groovy_settings},
)
cls.language_server = SolidLanguageServer.create(config, repo_path, solidlsp_settings=solidlsp_settings)
+15 -13
View File
@@ -16,18 +16,20 @@ Test Repository Structure:
import pytest
from solidlsp.ls import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from test.conftest import language_tests_enabled
from test.conftest import language_server_tests_enabled
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
from test.solidlsp.util.diagnostics import assert_file_diagnostics
pytestmark = pytest.mark.skipif(not language_tests_enabled(Language.HASKELL), reason="Haskell tests are disabled (HLS not available)")
pytestmark = pytest.mark.skipif(
not language_server_tests_enabled(LanguageServerId.HASKELL), reason="Haskell tests are disabled (HLS not available)"
)
@pytest.mark.haskell
class TestHaskellLanguageServer:
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_calculator_module_symbols(self, language_server: SolidLanguageServer):
"""
Test precise symbol discovery in Calculator.hs.
@@ -66,7 +68,7 @@ class TestHaskellLanguageServer:
23,
], f"Calculator should be a data type (kind 1, 5, or 23), got kind {calculator_symbol['kind']}"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_helper_module_symbols(self, language_server: SolidLanguageServer):
"""
Test precise symbol discovery in Helper.hs.
@@ -93,7 +95,7 @@ class TestHaskellLanguageServer:
extra = symbol_names - expected_symbols - {"Helper"}
assert not extra, f"Unexpected symbols in Helper.hs: {extra}"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_main_module_imports(self, language_server: SolidLanguageServer):
"""
Test that Main.hs properly references both Calculator and Helper modules.
@@ -106,7 +108,7 @@ class TestHaskellLanguageServer:
# Main.hs should have the main function
assert "main" in symbol_names, "Main.hs should contain 'main' function"
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_cross_file_references_validateNumber(self, language_server: SolidLanguageServer):
"""
Test cross-file reference tracking for validateNumber function.
@@ -132,7 +134,7 @@ class TestHaskellLanguageServer:
f"got {len(calculator_refs)} references in Calculator.hs"
)
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_within_file_references_isNegative(self, language_server: SolidLanguageServer):
"""
Test within-file reference tracking for isNegative function.
@@ -152,7 +154,7 @@ class TestHaskellLanguageServer:
f"All isNegative references should be in Helper.hs, got: {reference_paths}"
)
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_function_references_from_main(self, language_server: SolidLanguageServer):
"""
Test that functions used in Main.hs can be traced back to their definitions.
@@ -175,7 +177,7 @@ class TestHaskellLanguageServer:
f"Expected 'add' to be referenced in Main.hs or Calculator.hs, got: {add_ref_paths}"
)
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_multiply_function_usage_in_calculate(self, language_server: SolidLanguageServer):
"""
Test that multiply function usage is tracked within Calculator module.
@@ -198,7 +200,7 @@ class TestHaskellLanguageServer:
f"Expected 'multiply' to be referenced in Calculator.hs, got: {multiply_ref_paths}"
)
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_data_type_constructor_references(self, language_server: SolidLanguageServer):
"""
Test that Calculator data type constructor usage is tracked.
@@ -221,7 +223,7 @@ class TestHaskellLanguageServer:
f"Expected Calculator to be referenced in Main.hs or Calculator.hs, got: {calc_ref_paths}"
)
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
@@ -242,7 +244,7 @@ class TestHaskellLanguageServer:
]
pytest.fail(f"Found malformed symbols: {diagnostics}", pytrace=False)
@pytest.mark.parametrize("language_server", [Language.HASKELL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HASKELL], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+16 -16
View File
@@ -3,7 +3,7 @@ import os
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import SymbolUtils
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols
@@ -11,12 +11,12 @@ from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name,
@pytest.mark.haxe
class TestHaxeLanguageServer:
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_ls_is_running(self, language_server: SolidLanguageServer) -> None:
"""Test that the Haxe language server starts successfully."""
assert language_server.is_running()
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
symbols = language_server.request_full_symbol_tree()
assert SymbolUtils.symbol_tree_contains_name(symbols, "Main"), "Main class not found in symbol tree"
@@ -26,7 +26,7 @@ class TestHaxeLanguageServer:
assert SymbolUtils.symbol_tree_contains_name(symbols, "addNumbers"), "addNumbers method not found in symbol tree"
assert SymbolUtils.symbol_tree_contains_name(symbols, "formatMessage"), "formatMessage method not found in symbol tree"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None:
"""Test that symbol names do not contain unexpected formatting characters."""
all_symbols = request_all_symbols(language_server)
@@ -40,7 +40,7 @@ class TestHaxeLanguageServer:
pytrace=False,
)
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "Main.hx")
all_symbols, _ = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
@@ -69,7 +69,7 @@ class TestHaxeLanguageServer:
f"Expected all greet references in Main.hx, got {actual_locations}"
)
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_find_references_across_files(self, language_server: SolidLanguageServer) -> None:
# Test addNumbers which is defined in Helper.hx and used in Main.hx
helper_path = os.path.join("src", "utils", "Helper.hx")
@@ -103,7 +103,7 @@ class TestHaxeLanguageServer:
main_refs = [loc for loc in actual_locations if loc["uri_suffix"] == "Main.hx"]
assert len(main_refs) >= 2, f"Expected at least 2 references in Main.hx (lines 16 and 30), got {main_refs}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_document_symbols_structure(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "Main.hx")
result = language_server.request_document_symbols(file_path)
@@ -139,14 +139,14 @@ class TestHaxeLanguageServer:
SymbolKind.Property,
), f"Expected message to be Field/Variable, got {sym.get('kind')}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_workspace_symbol(self, language_server: SolidLanguageServer) -> None:
result = language_server.request_workspace_symbol("Helper")
assert result is not None, "Workspace symbol search returned None"
assert len(result) > 0, "Workspace symbol search returned no results"
assert any("Helper" in str(s.get("name", "")) for s in result), f"Expected at least one result containing 'Helper', got {result}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_go_to_definition_within_file(self, language_server: SolidLanguageServer) -> None:
"""Go to definition of greet from its call site in Main.hx -- should resolve within the same file."""
main_path = os.path.join("src", "Main.hx")
@@ -161,7 +161,7 @@ class TestHaxeLanguageServer:
definition_lines = [d["range"]["start"]["line"] for d in definitions if "Main.hx" in d.get("uri", d.get("relativePath", ""))]
assert 22 in definition_lines, f"Expected definition of greet at line 22 in Main.hx, got lines {definition_lines}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_go_to_definition(self, language_server: SolidLanguageServer) -> None:
"""Go to definition of addNumbers from a call site in Main.hx -- should resolve to Helper.hx."""
main_path = os.path.join("src", "Main.hx")
@@ -176,7 +176,7 @@ class TestHaxeLanguageServer:
definition_lines = [d["range"]["start"]["line"] for d in definitions if "Helper.hx" in d.get("uri", d.get("relativePath", ""))]
assert 18 in definition_lines, f"Expected definition of addNumbers at line 18 in Helper.hx, got lines {definition_lines}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_hover(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "Main.hx")
result = language_server.request_document_symbols(file_path)
@@ -190,7 +190,7 @@ class TestHaxeLanguageServer:
hover_str = str(hover)
assert "String" in hover_str or "greet" in hover_str, f"Expected hover to contain type info, got {hover_str}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_hover_on_class_declaration(self, language_server: SolidLanguageServer) -> None:
"""Hovering on a class name should return hover info."""
file_path = os.path.join("src", "Main.hx")
@@ -206,7 +206,7 @@ class TestHaxeLanguageServer:
hover_str = str(hover)
assert "Main" in hover_str, f"Expected hover to contain 'Main', got {hover_str}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_rename_symbol(self, language_server: SolidLanguageServer) -> None:
file_path = os.path.join("src", "Main.hx")
result = language_server.request_document_symbols(file_path)
@@ -222,7 +222,7 @@ class TestHaxeLanguageServer:
assert "Main.hx" in edits_str, f"Expected rename edits for Main.hx, got {edits}"
assert "sayHello" in edits_str, f"Expected new name 'sayHello' in rename edits, got {edits}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_completions(self, language_server: SolidLanguageServer) -> None:
"""Request completions after Helper. in calculateResult — should return Helper's static methods."""
file_path = os.path.join("src", "Main.hx")
@@ -233,7 +233,7 @@ class TestHaxeLanguageServer:
completion_texts = [c.get("completionText", c.get("label", "")) for c in completions]
assert "addNumbers" in completion_texts, f"Expected 'addNumbers' in completions after Helper., got {completion_texts[:10]}"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_document_overview(self, language_server: SolidLanguageServer) -> None:
overview = language_server.request_document_overview(os.path.join("src", "Main.hx"))
assert overview, "Document overview returned empty list"
@@ -248,7 +248,7 @@ class TestHaxeLanguageServer:
assert s.get("name"), "Symbol missing 'name' field"
assert s.get("kind"), "Symbol missing 'kind' field"
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_rapid_successive_requests(self, language_server: SolidLanguageServer) -> None:
"""Verify that rapid successive requests don't return empty results.
+2 -2
View File
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.haxe
class TestHaxeDiagnostics:
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HAXE], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
+12 -12
View File
@@ -10,7 +10,7 @@ from typing import Any
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from solidlsp.ls_exceptions import SolidLSPException
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import SymbolUtils
@@ -30,19 +30,19 @@ def _find_symbol_by_name(language_server: SolidLanguageServer, file_path: str, n
class TestHlslSymbols:
"""Tests for document symbol extraction."""
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_find_struct(self, language_server: SolidLanguageServer) -> None:
"""VertexInput struct should appear in common.hlsl symbols."""
symbol = _find_symbol_by_name(language_server, "common.hlsl", "VertexInput")
assert symbol is not None, "Expected 'VertexInput' struct in document symbols"
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_find_function(self, language_server: SolidLanguageServer) -> None:
"""SafeNormalize function should appear in common.hlsl."""
symbol = _find_symbol_by_name(language_server, "common.hlsl", "SafeNormalize")
assert symbol is not None, "Expected 'SafeNormalize' function in document symbols"
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_find_cbuffer_members(self, language_server: SolidLanguageServer) -> None:
"""Cbuffer members should appear as variables in compute_test.hlsl.
@@ -52,13 +52,13 @@ class TestHlslSymbols:
symbol = _find_symbol_by_name(language_server, "compute_test.hlsl", "TextureSize")
assert symbol is not None, "Expected 'TextureSize' cbuffer member in document symbols"
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_find_compute_kernel(self, language_server: SolidLanguageServer) -> None:
"""CSMain kernel should appear in compute_test.hlsl."""
symbol = _find_symbol_by_name(language_server, "compute_test.hlsl", "CSMain")
assert symbol is not None, "Expected 'CSMain' compute kernel in document symbols"
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
"""Full symbol tree should contain symbols from multiple files."""
symbols = language_server.request_full_symbol_tree()
@@ -73,7 +73,7 @@ class TestHlslSymbols:
class TestHlslDefinition:
"""Tests for go-to-definition capability."""
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_goto_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
"""Navigating to SafeNormalize call in lighting.hlsl should resolve to common.hlsl.
@@ -85,7 +85,7 @@ class TestHlslDefinition:
def_paths = [d.get("relativePath", d.get("uri", "")) for d in definitions]
assert any("common.hlsl" in p for p in def_paths), f"Expected definition in common.hlsl, got: {def_paths}"
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_goto_definition_cross_file_remap(self, language_server: SolidLanguageServer) -> None:
"""Navigating to Remap call in compute_test.hlsl should resolve to common.hlsl.
@@ -109,7 +109,7 @@ class TestHlslReferences:
request_references is expected to return an empty list.
"""
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_references_not_supported(self, language_server: SolidLanguageServer) -> None:
"""References request should raise because shader-language-server does not support it.
@@ -137,7 +137,7 @@ def _extract_hover_text(hover_info: dict[str, Any]) -> str:
class TestHlslHover:
"""Tests for hover information."""
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_hover_on_function(self, language_server: SolidLanguageServer) -> None:
"""Hovering over SafeNormalize definition should return info.
@@ -150,7 +150,7 @@ class TestHlslHover:
hover_text = _extract_hover_text(hover_info)
assert len(hover_text) > 0, "Hover text should not be empty"
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_hover_on_struct(self, language_server: SolidLanguageServer) -> None:
"""Hovering over VertexInput should return struct info.
@@ -161,7 +161,7 @@ class TestHlslHover:
assert hover_info is not None, "Hover should return information for VertexInput"
assert "contents" in hover_info, "Hover should have contents"
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_bare_symbol_names(self, language_server) -> None:
all_symbols = request_all_symbols(language_server)
malformed_symbols = []
+2 -2
View File
@@ -1,13 +1,13 @@
import pytest
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_config import LanguageServerId
from test.solidlsp.util.diagnostics import assert_file_diagnostics
@pytest.mark.hlsl
class TestHlslDiagnostics:
@pytest.mark.parametrize("language_server", [Language.HLSL], indirect=True)
@pytest.mark.parametrize("language_server", [LanguageServerId.HLSL], indirect=True)
def test_file_diagnostics(self, language_server: SolidLanguageServer) -> None:
assert_file_diagnostics(
language_server,
Loaded 100 of 180 files, more files were not shown because too many files have changed in this diff. Show more