mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 18:16:43 +00:00
Merge pull request #983 from thomhurst/csharp-nuget-org-and-dotnet-install
Modernize C# language server: Switch to NuGet.org and official .NET install scripts
This commit is contained in:
6 files changed
+763
-189
No files matched your search
@@ -212,6 +212,50 @@ This is supported by all language servers deriving their dependency provider fro
|
||||
Currently, this includes the following languages: `clojure`, `cpp`, `php`, `python`, `rust`, `typescript`.
|
||||
We will add support for more languages over time.
|
||||
|
||||
#### C# (Roslyn Language Server)
|
||||
|
||||
Serena uses [Microsoft's Roslyn Language Server](https://github.com/dotnet/roslyn) for C# support.
|
||||
|
||||
**Runtime Requirements:**
|
||||
|
||||
- .NET 10 or higher is required. If not found in PATH, Serena automatically installs it using Microsoft's official install scripts.
|
||||
- The Roslyn Language Server is automatically downloaded from NuGet.org.
|
||||
|
||||
**Supported Platforms:**
|
||||
|
||||
Automatic download is supported for: Windows (x64, ARM64), macOS (x64, ARM64), Linux (x64, ARM64).
|
||||
|
||||
**Configuration:**
|
||||
|
||||
The `runtime_dependencies` setting allows you to override the download URLs for the Roslyn Language Server. This is useful if you need to use a private package mirror or a specific version.
|
||||
|
||||
Example configuration to override the language server download URL:
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
csharp:
|
||||
runtime_dependencies:
|
||||
- id: "CSharpLanguageServer"
|
||||
platform_id: "linux-x64" # or win-x64, win-arm64, osx-x64, osx-arm64, linux-arm64
|
||||
url: "https://your-mirror.example.com/roslyn-language-server.linux-x64.5.5.0-2.26078.4.nupkg"
|
||||
package_version: "5.5.0-2.26078.4"
|
||||
```
|
||||
|
||||
Available fields for `runtime_dependencies` entries:
|
||||
|
||||
| Field | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------- |
|
||||
| `id` | Dependency identifier (use `CSharpLanguageServer`) |
|
||||
| `platform_id` | Target platform: `win-x64`, `win-arm64`, `osx-x64`, `osx-arm64`, `linux-x64`, `linux-arm64` |
|
||||
| `url` | Download URL for the NuGet package |
|
||||
| `package_version` | Package version string |
|
||||
| `extract_path` | Path within the package to extract (default: `tools/net10.0/<platform>`) |
|
||||
|
||||
Notes:
|
||||
- Only specify the platforms you want to override; others will use the defaults.
|
||||
- The language server package is a `.nupkg` file (ZIP format) downloaded from NuGet.org by default.
|
||||
- If you have .NET 10+ already installed, Serena will use your system installation.
|
||||
|
||||
#### Go (`gopls`)
|
||||
|
||||
Serena forwards `ls_specific_settings.go.gopls_settings` to `gopls` as LSP `initializationOptions` when the Go language server is started.
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
"""
|
||||
CSharp Language Server using Microsoft.CodeAnalysis.LanguageServer (Official Roslyn-based LSP server)
|
||||
CSharp Language Server using Roslyn Language Server (Official Roslyn-based LSP server from NuGet.org)
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import threading
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
from solidlsp.ls import LanguageServerDependencyProvider, SolidLanguageServer
|
||||
from solidlsp.ls import DocumentSymbols, LanguageServerDependencyProvider, SolidLanguageServer
|
||||
from solidlsp.ls_config import LanguageServerConfig
|
||||
from solidlsp.ls_exceptions import SolidLSPException
|
||||
from solidlsp.ls_types import Hover, UnifiedSymbolInformation
|
||||
from solidlsp.ls_utils import PathUtils
|
||||
from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams, InitializeResult
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
@@ -33,111 +31,69 @@ log = logging.getLogger(__name__)
|
||||
_RUNTIME_DEPENDENCIES = [
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for Windows (x64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.win-x64",
|
||||
package_version="5.0.0-1.25329.6",
|
||||
description="Roslyn Language Server for Windows (x64)",
|
||||
package_name="roslyn-language-server.win-x64",
|
||||
package_version="5.5.0-2.26078.4",
|
||||
url="https://www.nuget.org/api/v2/package/roslyn-language-server.win-x64/5.5.0-2.26078.4",
|
||||
platform_id="win-x64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/win-x64",
|
||||
extract_path="tools/net10.0/win-x64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for Windows (ARM64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.win-arm64",
|
||||
package_version="5.0.0-1.25329.6",
|
||||
description="Roslyn Language Server for Windows (ARM64)",
|
||||
package_name="roslyn-language-server.win-arm64",
|
||||
package_version="5.5.0-2.26078.4",
|
||||
url="https://www.nuget.org/api/v2/package/roslyn-language-server.win-arm64/5.5.0-2.26078.4",
|
||||
platform_id="win-arm64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/win-arm64",
|
||||
extract_path="tools/net10.0/win-arm64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for macOS (x64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.osx-x64",
|
||||
package_version="5.0.0-1.25329.6",
|
||||
description="Roslyn Language Server for macOS (x64)",
|
||||
package_name="roslyn-language-server.osx-x64",
|
||||
package_version="5.5.0-2.26078.4",
|
||||
url="https://www.nuget.org/api/v2/package/roslyn-language-server.osx-x64/5.5.0-2.26078.4",
|
||||
platform_id="osx-x64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/osx-x64",
|
||||
extract_path="tools/net10.0/osx-x64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for macOS (ARM64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.osx-arm64",
|
||||
package_version="5.0.0-1.25329.6",
|
||||
description="Roslyn Language Server for macOS (ARM64)",
|
||||
package_name="roslyn-language-server.osx-arm64",
|
||||
package_version="5.5.0-2.26078.4",
|
||||
url="https://www.nuget.org/api/v2/package/roslyn-language-server.osx-arm64/5.5.0-2.26078.4",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/osx-arm64",
|
||||
extract_path="tools/net10.0/osx-arm64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for Linux (x64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.linux-x64",
|
||||
package_version="5.0.0-1.25329.6",
|
||||
description="Roslyn Language Server for Linux (x64)",
|
||||
package_name="roslyn-language-server.linux-x64",
|
||||
package_version="5.5.0-2.26078.4",
|
||||
url="https://www.nuget.org/api/v2/package/roslyn-language-server.linux-x64/5.5.0-2.26078.4",
|
||||
platform_id="linux-x64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/linux-x64",
|
||||
extract_path="tools/net10.0/linux-x64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="CSharpLanguageServer",
|
||||
description="Microsoft.CodeAnalysis.LanguageServer for Linux (ARM64)",
|
||||
package_name="Microsoft.CodeAnalysis.LanguageServer.linux-arm64",
|
||||
package_version="5.0.0-1.25329.6",
|
||||
description="Roslyn Language Server for Linux (ARM64)",
|
||||
package_name="roslyn-language-server.linux-arm64",
|
||||
package_version="5.5.0-2.26078.4",
|
||||
url="https://www.nuget.org/api/v2/package/roslyn-language-server.linux-arm64/5.5.0-2.26078.4",
|
||||
platform_id="linux-arm64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/linux-arm64",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Windows (x64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x64.zip",
|
||||
platform_id="win-x64",
|
||||
archive_type="zip",
|
||||
binary_name="dotnet.exe",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Linux (x64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-x64.tar.gz",
|
||||
platform_id="linux-x64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Linux (ARM64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-arm64.tar.gz",
|
||||
platform_id="linux-arm64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for macOS (x64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-osx-x64.tar.gz",
|
||||
platform_id="osx-x64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for macOS (ARM64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-osx-arm64.tar.gz",
|
||||
platform_id="osx-arm64",
|
||||
archive_type="tar.gz",
|
||||
binary_name="dotnet",
|
||||
),
|
||||
RuntimeDependency(
|
||||
id="DotNetRuntime",
|
||||
description=".NET 9 Runtime for Windows (ARM64)",
|
||||
url="https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-arm64.zip",
|
||||
platform_id="win-arm64",
|
||||
archive_type="zip",
|
||||
binary_name="dotnet.exe",
|
||||
extract_path="tools/net10.0/linux-arm64",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -188,22 +144,26 @@ def find_solution_or_project_file(root_dir: str) -> str | None:
|
||||
|
||||
class CSharpLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
Provides C# specific instantiation of the LanguageServer class using `Microsoft.CodeAnalysis.LanguageServer`,
|
||||
the official Roslyn-based language server from Microsoft.
|
||||
Provides C# specific instantiation of the LanguageServer class using the official Roslyn-based
|
||||
language server from NuGet.org.
|
||||
|
||||
You can pass a list of runtime dependency overrides in ls_specific_settings["csharp"]["runtime_dependencies"]. This is a list of
|
||||
dicts, each containing at least the "id" key, and optionally "platform_id" to uniquely identify the dependency to override.
|
||||
For example, to override the URL of the .NET runtime on windows-x64, add the entry:
|
||||
You can pass a list of runtime dependency overrides in ls_specific_settings["csharp"]["runtime_dependencies"].
|
||||
This is a list of dicts, each containing at least the "id" key, and optionally "platform_id" to uniquely
|
||||
identify the dependency to override.
|
||||
|
||||
Example - Override Roslyn Language Server URL:
|
||||
```
|
||||
{
|
||||
"id": "DotNetRuntime",
|
||||
"id": "CSharpLanguageServer",
|
||||
"platform_id": "win-x64",
|
||||
"url": "https://example.com/custom-dotnet-runtime.zip"
|
||||
"url": "https://example.com/custom-roslyn-server.nupkg"
|
||||
}
|
||||
```
|
||||
|
||||
See the `_RUNTIME_DEPENDENCIES` variable above for the available dependency ids and platform_ids.
|
||||
|
||||
Note: .NET runtime (version 10+) is required and installed automatically via Microsoft's official install
|
||||
scripts. If you have a custom .NET installation, ensure 'dotnet' is available in PATH with version 10 or higher.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
@@ -212,6 +172,9 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
super().__init__(config, repository_root_path, None, "csharp", solidlsp_settings)
|
||||
# Cache for original Roslyn symbol names with type annotations
|
||||
# Key: (relative_file_path, line, character) -> Value: original name
|
||||
self._original_symbol_names: dict[tuple[str, int, int], str] = {}
|
||||
|
||||
def _create_dependency_provider(self) -> LanguageServerDependencyProvider:
|
||||
return self.DependencyProvider(self._custom_settings, self._ls_resources_dir, self._solidlsp_settings, self.repository_root_path)
|
||||
@@ -220,6 +183,116 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
def is_ignored_dirname(self, dirname: str) -> bool:
|
||||
return super().is_ignored_dirname(dirname) or dirname in ["bin", "obj", "packages", ".vs"]
|
||||
|
||||
@override
|
||||
def request_document_symbols(self, relative_file_path: str, file_buffer: Any = None) -> DocumentSymbols:
|
||||
"""
|
||||
Override to normalize Roslyn symbol names and cache originals.
|
||||
|
||||
Roslyn 5.5.0+ returns symbol names with type annotations:
|
||||
- Properties: "Name : string"
|
||||
- Methods: "Add(int, int) : int"
|
||||
|
||||
This method:
|
||||
1. Normalizes names to base form ("Name", "Add")
|
||||
2. Caches original names for rich information display
|
||||
3. Populates LSP spec's 'detail' field with type/signature info
|
||||
"""
|
||||
symbols = super().request_document_symbols(relative_file_path, file_buffer)
|
||||
|
||||
# Normalize all symbols recursively
|
||||
for symbol in symbols.iter_symbols():
|
||||
self._normalize_symbol_name(symbol, relative_file_path)
|
||||
|
||||
return symbols
|
||||
|
||||
@override
|
||||
def request_hover(self, relative_file_path: str, line: int, column: int) -> Hover | None:
|
||||
"""
|
||||
Override to inject original Roslyn symbol names (with type annotations) into hover responses.
|
||||
|
||||
When hovering over a symbol whose name was normalized, we prepend the original
|
||||
full name (e.g., 'Add(int, int) : int') to the hover content.
|
||||
"""
|
||||
hover = super().request_hover(relative_file_path, line, column)
|
||||
|
||||
if hover is None:
|
||||
return None
|
||||
|
||||
# Check if we have an original name for this position
|
||||
original_name = self._original_symbol_names.get((relative_file_path, line, column))
|
||||
|
||||
if original_name and "contents" in hover:
|
||||
contents = hover["contents"]
|
||||
if isinstance(contents, dict) and "value" in contents:
|
||||
# Prepend the original full name with type information to the hover content
|
||||
prefix = f"**{original_name}**\n\n---\n\n"
|
||||
contents["value"] = prefix + contents["value"]
|
||||
|
||||
return hover
|
||||
|
||||
def _normalize_symbol_name(self, symbol: UnifiedSymbolInformation, relative_file_path: str) -> None:
|
||||
"""
|
||||
Normalize a single symbol's name and cache the original.
|
||||
Processes children recursively.
|
||||
"""
|
||||
original_name = symbol.get("name", "")
|
||||
|
||||
# Extract base name and type/signature info
|
||||
normalized_name, type_info = self._extract_base_name_and_type(original_name)
|
||||
|
||||
# Store original name if it was normalized
|
||||
if original_name != normalized_name:
|
||||
sel_range = symbol.get("selectionRange")
|
||||
if sel_range:
|
||||
start = sel_range.get("start")
|
||||
if start and "line" in start and "character" in start:
|
||||
line = start["line"]
|
||||
char = start["character"]
|
||||
cache_key = (relative_file_path, line, char)
|
||||
self._original_symbol_names[cache_key] = original_name
|
||||
|
||||
# Populate LSP spec's 'detail' field with type/signature information
|
||||
if type_info and "detail" not in symbol:
|
||||
symbol["detail"] = type_info
|
||||
|
||||
# Update the symbol name
|
||||
symbol["name"] = normalized_name
|
||||
|
||||
# Process children recursively
|
||||
children = symbol.get("children", [])
|
||||
for child in children:
|
||||
self._normalize_symbol_name(child, relative_file_path)
|
||||
|
||||
@staticmethod
|
||||
def _extract_base_name_and_type(roslyn_name: str) -> tuple[str, str]:
|
||||
"""
|
||||
Extract base name and type/signature information from Roslyn symbol names.
|
||||
|
||||
Examples:
|
||||
"Name : string" -> ("Name", ": string")
|
||||
"Add(int, int) : int" -> ("Add", "(int, int) : int")
|
||||
"ToString()" -> ("ToString", "()")
|
||||
"SimpleMethod" -> ("SimpleMethod", "")
|
||||
|
||||
Returns:
|
||||
Tuple of (base_name, type_info)
|
||||
|
||||
"""
|
||||
# Check for property pattern: "Name : Type"
|
||||
if " : " in roslyn_name and "(" not in roslyn_name:
|
||||
base_name, type_part = roslyn_name.split(" : ", 1)
|
||||
return base_name.strip(), f": {type_part.strip()}"
|
||||
|
||||
# Check for method pattern: "MethodName(params) : ReturnType"
|
||||
if "(" in roslyn_name:
|
||||
paren_idx = roslyn_name.index("(")
|
||||
base_name = roslyn_name[:paren_idx].strip()
|
||||
signature = roslyn_name[paren_idx:].strip()
|
||||
return base_name, signature
|
||||
|
||||
# No type annotation
|
||||
return roslyn_name, ""
|
||||
|
||||
class DependencyProvider(LanguageServerDependencyProvider):
|
||||
def __init__(
|
||||
self,
|
||||
@@ -261,11 +334,23 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
runtime_dependency_overrides = cast(list[dict[str, Any]], self._custom_settings.get("runtime_dependencies", []))
|
||||
|
||||
# Filter out deprecated DotNetRuntime overrides and warn users
|
||||
filtered_overrides = []
|
||||
for dep_override in runtime_dependency_overrides:
|
||||
if dep_override.get("id") == "DotNetRuntime":
|
||||
log.warning(
|
||||
"The 'DotNetRuntime' runtime_dependencies override is no longer supported. "
|
||||
".NET is now installed automatically via Microsoft's official install scripts. "
|
||||
"Please remove this override from your configuration."
|
||||
)
|
||||
else:
|
||||
filtered_overrides.append(dep_override)
|
||||
|
||||
log.debug("Resolving runtime dependencies")
|
||||
|
||||
runtime_dependencies = RuntimeDependencyCollection(
|
||||
_RUNTIME_DEPENDENCIES,
|
||||
overrides=runtime_dependency_overrides,
|
||||
overrides=filtered_overrides,
|
||||
)
|
||||
|
||||
log.debug(
|
||||
@@ -274,30 +359,28 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
|
||||
# Find the dependencies for our platform
|
||||
lang_server_dep = runtime_dependencies.get_single_dep_for_current_platform("CSharpLanguageServer")
|
||||
dotnet_runtime_dep = runtime_dependencies.get_single_dep_for_current_platform("DotNetRuntime")
|
||||
dotnet_path = self._ensure_dotnet_runtime(dotnet_runtime_dep)
|
||||
dotnet_path = self._ensure_dotnet_runtime()
|
||||
server_dll_path = self._ensure_language_server(lang_server_dep)
|
||||
|
||||
return dotnet_path, server_dll_path
|
||||
|
||||
def _ensure_dotnet_runtime(self, dotnet_runtime_dep: RuntimeDependency) -> str:
|
||||
def _ensure_dotnet_runtime(self) -> str:
|
||||
"""Ensure .NET runtime is available and return the dotnet executable path."""
|
||||
# TODO: use RuntimeDependency util methods instead of custom validation/download logic
|
||||
|
||||
# Check if dotnet is already available on the system
|
||||
system_dotnet = shutil.which("dotnet")
|
||||
if system_dotnet:
|
||||
# Check if it's .NET 9
|
||||
# Check if it's .NET 10 or compatible
|
||||
try:
|
||||
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
if "Microsoft.NETCore.App 9." in result.stdout:
|
||||
log.info("Found system .NET 9 runtime")
|
||||
# Accept .NET 10 or higher (10.x, 11.x, etc.)
|
||||
if any(f"Microsoft.NETCore.App {v}." in result.stdout for v in range(10, 20)):
|
||||
log.info("Found system .NET 10+ runtime")
|
||||
return system_dotnet
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
# Download .NET 9 runtime using config
|
||||
return self._ensure_dotnet_runtime_from_config(dotnet_runtime_dep)
|
||||
# Install .NET 10 runtime using Microsoft's install script
|
||||
return self._install_dotnet_with_script()
|
||||
|
||||
def _ensure_language_server(self, lang_server_dep: RuntimeDependency) -> str:
|
||||
"""Ensure language server is available and return the DLL path."""
|
||||
@@ -309,26 +392,24 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
server_dll = server_dir / lang_server_dep.binary_name
|
||||
|
||||
if server_dll.exists():
|
||||
log.info(f"Using cached Microsoft.CodeAnalysis.LanguageServer from {server_dll}")
|
||||
log.info(f"Using cached Roslyn Language Server from {server_dll}")
|
||||
return str(server_dll)
|
||||
|
||||
# Download and install the language server
|
||||
log.info(f"Downloading {package_name} version {package_version}...")
|
||||
assert package_version is not None
|
||||
assert package_name is not None
|
||||
package_path = self._download_nuget_package_direct(package_name, package_version)
|
||||
log.info(f"Downloading {package_name} version {package_version} from NuGet.org...")
|
||||
package_path = self._download_nuget_package(lang_server_dep)
|
||||
|
||||
# Extract and install
|
||||
self._extract_language_server(lang_server_dep, package_path, server_dir)
|
||||
|
||||
if not server_dll.exists():
|
||||
raise SolidLSPException("Microsoft.CodeAnalysis.LanguageServer.dll not found after extraction")
|
||||
raise SolidLSPException("Roslyn Language Server DLL not found after extraction")
|
||||
|
||||
# Make executable on Unix systems
|
||||
if platform.system().lower() != "windows":
|
||||
server_dll.chmod(0o755)
|
||||
|
||||
log.info(f"Successfully installed Microsoft.CodeAnalysis.LanguageServer to {server_dll}")
|
||||
log.info(f"Successfully installed Roslyn Language Server to {server_dll}")
|
||||
return str(server_dll)
|
||||
|
||||
@staticmethod
|
||||
@@ -354,43 +435,28 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
server_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(source_dir, server_dir, dirs_exist_ok=True)
|
||||
|
||||
def _download_nuget_package_direct(self, package_name: str, package_version: str) -> Path:
|
||||
def _download_nuget_package(self, dependency: RuntimeDependency) -> Path:
|
||||
"""
|
||||
Download a NuGet package directly from the Azure NuGet feed.
|
||||
Download a NuGet package from NuGet.org and extract it.
|
||||
Returns the path to the extracted package directory.
|
||||
"""
|
||||
azure_feed_url = "https://pkgs.dev.azure.com/azure-public/vside/_packaging/vs-impl/nuget/v3/index.json"
|
||||
package_name = dependency.package_name
|
||||
package_version = dependency.package_version
|
||||
url = dependency.url
|
||||
|
||||
if url is None:
|
||||
raise SolidLSPException(f"No URL specified for package {package_name} version {package_version}")
|
||||
|
||||
# Create temporary directory for package download
|
||||
temp_dir = Path(self._ls_resources_dir) / "temp_downloads"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
# First, get the service index from the Azure feed
|
||||
log.debug("Fetching NuGet service index from Azure feed...")
|
||||
with urllib.request.urlopen(azure_feed_url) as response:
|
||||
service_index = json.loads(response.read().decode())
|
||||
|
||||
# Find the package base address (for downloading packages)
|
||||
package_base_address = None
|
||||
for resource in service_index.get("resources", []):
|
||||
if resource.get("@type") == "PackageBaseAddress/3.0.0":
|
||||
package_base_address = resource.get("@id")
|
||||
break
|
||||
|
||||
if not package_base_address:
|
||||
raise SolidLSPException("Could not find package base address in Azure NuGet feed")
|
||||
|
||||
# Construct the download URL for the specific package
|
||||
package_id_lower = package_name.lower()
|
||||
package_version_lower = package_version.lower()
|
||||
package_url = f"{package_base_address.rstrip('/')}/{package_id_lower}/{package_version_lower}/{package_id_lower}.{package_version_lower}.nupkg"
|
||||
|
||||
log.debug(f"Downloading package from: {package_url}")
|
||||
log.debug(f"Downloading package from: {url}")
|
||||
|
||||
# Download the .nupkg file
|
||||
nupkg_file = temp_dir / f"{package_name}.{package_version}.nupkg"
|
||||
urllib.request.urlretrieve(package_url, nupkg_file)
|
||||
urllib.request.urlretrieve(url, nupkg_file)
|
||||
|
||||
# Extract the .nupkg file (it's just a zip file)
|
||||
package_extract_dir = temp_dir / f"{package_name}.{package_version}"
|
||||
@@ -403,82 +469,88 @@ class CSharpLanguageServer(SolidLanguageServer):
|
||||
# Clean up the nupkg file
|
||||
nupkg_file.unlink()
|
||||
|
||||
log.info(f"Successfully downloaded and extracted {package_name} version {package_version}")
|
||||
log.info(f"Successfully downloaded and extracted {package_name} version {package_version} from NuGet.org")
|
||||
return package_extract_dir
|
||||
|
||||
except Exception as e:
|
||||
raise SolidLSPException(
|
||||
f"Failed to download package {package_name} version {package_version} from Azure NuGet feed: {e}"
|
||||
) from e
|
||||
raise SolidLSPException(f"Failed to download package {package_name} version {package_version} from NuGet.org: {e}") from e
|
||||
|
||||
def _ensure_dotnet_runtime_from_config(self, dotnet_runtime_dep: RuntimeDependency) -> str:
|
||||
def _install_dotnet_with_script(self, version: str = "10.0") -> str:
|
||||
"""
|
||||
Ensure .NET 9 runtime is available using runtime dependency configuration.
|
||||
Install .NET runtime using Microsoft's official install script.
|
||||
Returns the path to the dotnet executable.
|
||||
"""
|
||||
# TODO: use RuntimeDependency util methods instead of custom download logic
|
||||
dotnet_dir = Path(self._ls_resources_dir) / f"dotnet-runtime-{version}"
|
||||
|
||||
# Check if dotnet is already available on the system
|
||||
system_dotnet = shutil.which("dotnet")
|
||||
if system_dotnet:
|
||||
# Check if it's .NET 9
|
||||
try:
|
||||
result = subprocess.run([system_dotnet, "--list-runtimes"], capture_output=True, text=True, check=True)
|
||||
if "Microsoft.NETCore.App 9." in result.stdout:
|
||||
log.info("Found system .NET 9 runtime")
|
||||
return system_dotnet
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
# Download .NET 9 runtime using config
|
||||
dotnet_dir = Path(self._ls_resources_dir) / "dotnet-runtime-9.0"
|
||||
assert dotnet_runtime_dep.binary_name is not None, "Runtime dependency must have a binary_name"
|
||||
dotnet_exe = dotnet_dir / dotnet_runtime_dep.binary_name
|
||||
# Determine binary name based on platform
|
||||
is_windows = platform.system().lower() == "windows"
|
||||
dotnet_exe = dotnet_dir / ("dotnet.exe" if is_windows else "dotnet")
|
||||
|
||||
if dotnet_exe.exists():
|
||||
log.info(f"Using cached .NET runtime from {dotnet_exe}")
|
||||
log.info(f"Using cached .NET {version} runtime from {dotnet_exe}")
|
||||
return str(dotnet_exe)
|
||||
|
||||
# Download .NET runtime
|
||||
log.info("Downloading .NET 9 runtime...")
|
||||
# Download and run install script
|
||||
log.info(f"Installing .NET {version} runtime using official Microsoft install script...")
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
custom_dotnet_runtime_url = self._custom_settings.get("dotnet_runtime_url")
|
||||
if custom_dotnet_runtime_url is not None:
|
||||
log.info(f"Using custom .NET runtime url: {custom_dotnet_runtime_url}")
|
||||
url = custom_dotnet_runtime_url
|
||||
else:
|
||||
url = dotnet_runtime_dep.url
|
||||
|
||||
archive_type = dotnet_runtime_dep.archive_type
|
||||
|
||||
# Download the runtime
|
||||
download_path = dotnet_dir / f"dotnet-runtime.{archive_type}"
|
||||
try:
|
||||
log.debug(f"Downloading from {url}")
|
||||
urllib.request.urlretrieve(url, download_path)
|
||||
if is_windows:
|
||||
# PowerShell script for Windows
|
||||
script_url = "https://dot.net/v1/dotnet-install.ps1"
|
||||
script_path = dotnet_dir / "dotnet-install.ps1"
|
||||
urllib.request.urlretrieve(script_url, script_path)
|
||||
|
||||
# Extract the archive
|
||||
if archive_type == "zip":
|
||||
with zipfile.ZipFile(download_path, "r") as zip_ref:
|
||||
zip_ref.extractall(dotnet_dir)
|
||||
cmd = [
|
||||
"pwsh",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(script_path),
|
||||
"-Version",
|
||||
version,
|
||||
"-InstallDir",
|
||||
str(dotnet_dir),
|
||||
"-Runtime",
|
||||
"dotnet",
|
||||
"-NoPath",
|
||||
]
|
||||
else:
|
||||
# tar.gz
|
||||
with tarfile.open(download_path, "r:gz") as tar_ref:
|
||||
tar_ref.extractall(dotnet_dir)
|
||||
# Bash script for Linux/macOS
|
||||
script_url = "https://dot.net/v1/dotnet-install.sh"
|
||||
script_path = dotnet_dir / "dotnet-install.sh"
|
||||
urllib.request.urlretrieve(script_url, script_path)
|
||||
script_path.chmod(0o755)
|
||||
|
||||
# Remove the archive
|
||||
download_path.unlink()
|
||||
cmd = [
|
||||
"bash",
|
||||
str(script_path),
|
||||
"--version",
|
||||
version,
|
||||
"--install-dir",
|
||||
str(dotnet_dir),
|
||||
"--runtime",
|
||||
"dotnet",
|
||||
"--no-path",
|
||||
]
|
||||
|
||||
# Make dotnet executable on Unix
|
||||
if platform.system().lower() != "windows":
|
||||
dotnet_exe.chmod(0o755)
|
||||
# Run the install script
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
log.debug(f"Install script output: {result.stdout}")
|
||||
|
||||
log.info(f"Successfully installed .NET 9 runtime to {dotnet_exe}")
|
||||
if not dotnet_exe.exists():
|
||||
raise SolidLSPException(f"dotnet executable not found at {dotnet_exe} after installation")
|
||||
|
||||
log.info(f"Successfully installed .NET {version} runtime to {dotnet_exe}")
|
||||
return str(dotnet_exe)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SolidLSPException(
|
||||
f"Failed to install .NET {version} runtime using install script: {e.stderr if e.stderr else e}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise SolidLSPException(f"Failed to download .NET 9 runtime from {url}: {e}") from e
|
||||
raise SolidLSPException(f"Failed to install .NET {version} runtime: {e}") from e
|
||||
|
||||
def _get_initialize_params(self) -> InitializeParams:
|
||||
"""
|
||||
|
||||
@@ -414,6 +414,9 @@ class PlatformUtils:
|
||||
class SymbolUtils:
|
||||
@staticmethod
|
||||
def symbol_tree_contains_name(roots: list[UnifiedSymbolInformation], name: str) -> bool:
|
||||
"""
|
||||
Check if any symbol in the tree has a name matching the given name.
|
||||
"""
|
||||
for symbol in roots:
|
||||
if symbol["name"] == name:
|
||||
return True
|
||||
|
||||
@@ -56,6 +56,7 @@ class TestCSharpLanguageServer:
|
||||
# Handle nested symbol structure
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
for sym in symbol_list:
|
||||
# Symbol names are normalized to base form (e.g., "Add" not "Add(int, int) : int")
|
||||
if sym.get("name") == "Add":
|
||||
add_symbol = sym
|
||||
break
|
||||
@@ -82,13 +83,13 @@ class TestCSharpLanguageServer:
|
||||
# Check that we have the Person class
|
||||
assert any(s.get("name") == "Person" and s.get("kind") == 5 for s in symbols)
|
||||
|
||||
# Check for properties and methods
|
||||
# Check for properties and methods (names are normalized to base form)
|
||||
symbol_names = [s.get("name") for s in symbols]
|
||||
assert "Name" in symbol_names
|
||||
assert "Age" in symbol_names
|
||||
assert "Email" in symbol_names
|
||||
assert "ToString" in symbol_names
|
||||
assert "IsAdult" in symbol_names
|
||||
assert "Name" in symbol_names, "Name property not found"
|
||||
assert "Age" in symbol_names, "Age property not found"
|
||||
assert "Email" in symbol_names, "Email property not found"
|
||||
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)
|
||||
def test_find_referencing_symbols_across_files(self, language_server: SolidLanguageServer) -> None:
|
||||
@@ -102,6 +103,7 @@ class TestCSharpLanguageServer:
|
||||
|
||||
subtract_symbol = None
|
||||
for sym in symbol_list:
|
||||
# Symbol names are normalized to base form (e.g., "Subtract" not "Subtract(int, int) : int")
|
||||
if sym.get("name") == "Subtract":
|
||||
subtract_symbol = sym
|
||||
break
|
||||
@@ -112,21 +114,66 @@ class TestCSharpLanguageServer:
|
||||
sel_start = subtract_symbol["selectionRange"]["start"]
|
||||
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"] + 1)
|
||||
|
||||
# Should find references in both Program.cs and Models/Person.cs
|
||||
# Should find references where the method is called
|
||||
ref_files = cast(list[str], [ref.get("relativePath", "") for ref in refs])
|
||||
print(f"Found references: {refs}")
|
||||
print(f"Reference files: {ref_files}")
|
||||
|
||||
# Check that we have references from both files
|
||||
assert any("Program.cs" in ref_file for ref_file in ref_files), "Should find reference in Program.cs"
|
||||
# Check that we have reference in Models/Person.cs where Calculator.Subtract is called
|
||||
# Note: New Roslyn version doesn't include the definition itself as a reference (more correct behavior)
|
||||
assert any(
|
||||
os.path.join("Models", "Person.cs") in ref_file for ref_file in ref_files
|
||||
), "Should find reference in Models/Person.cs where Calculator.Subtract is called"
|
||||
assert len(refs) > 0, "Should find at least one reference"
|
||||
|
||||
# check for a second time, since the first call may trigger initialization and change the state of the LS
|
||||
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)
|
||||
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")
|
||||
|
||||
# Open the file first
|
||||
language_server.open_file(file_path)
|
||||
|
||||
# Test 1: Hover over the Name property (line 6, column 23 - on "Name")
|
||||
# Source: public string Name { get; set; }
|
||||
hover_info = language_server.request_hover(file_path, 6, 23)
|
||||
|
||||
# Verify hover returns content
|
||||
assert hover_info is not None, "Hover should return information for Name property"
|
||||
assert isinstance(hover_info, dict), "Hover should be a dict"
|
||||
assert "contents" in hover_info, "Hover should have contents"
|
||||
|
||||
contents = hover_info["contents"]
|
||||
assert isinstance(contents, dict), "Hover contents should be a dict"
|
||||
assert "value" in contents, "Hover contents should have value"
|
||||
hover_text = contents["value"]
|
||||
|
||||
# Verify the hover contains property signature with type
|
||||
assert "string" in hover_text, f"Hover should include 'string' type, got: {hover_text}"
|
||||
assert "Name" in hover_text, f"Hover should include 'Name' property name, got: {hover_text}"
|
||||
|
||||
# Test 2: Hover over the IsAdult method (line 22, column 21 - on "IsAdult")
|
||||
# Source: public bool IsAdult()
|
||||
hover_method = language_server.request_hover(file_path, 22, 21)
|
||||
|
||||
# Verify method hover returns content
|
||||
assert hover_method is not None, "Hover should return information for IsAdult method"
|
||||
assert isinstance(hover_method, dict), "Hover should be a dict"
|
||||
assert "contents" in hover_method, "Hover should have contents"
|
||||
|
||||
contents = hover_method["contents"]
|
||||
assert isinstance(contents, dict), "Hover contents should be a dict"
|
||||
assert "value" in contents, "Hover contents should have value"
|
||||
method_hover_text = contents["value"]
|
||||
|
||||
# Verify the hover contains method signature with return type
|
||||
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}"
|
||||
|
||||
|
||||
@pytest.mark.csharp
|
||||
class TestCSharpSolutionProjectOpening:
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
"""Tests for C# language server .NET runtime installation using official install scripts."""
|
||||
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
@pytest.mark.csharp
|
||||
class TestDotNetInstallScript:
|
||||
"""Test .NET runtime installation using Microsoft's official install scripts."""
|
||||
|
||||
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server")
|
||||
@patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.platform.system")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.subprocess.run")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.shutil.which")
|
||||
def test_install_dotnet_uses_bash_script_on_linux(
|
||||
self, mock_which, mock_urlretrieve, mock_subprocess, mock_platform, mock_platform_id, mock_ensure_ls
|
||||
):
|
||||
"""Test that Linux uses bash install script."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
mock_platform.return_value = "Linux"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
mock_which.return_value = None # No system dotnet
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
def mock_retrieve(url, path):
|
||||
# Create the file that urlretrieve would create
|
||||
Path(path).touch()
|
||||
# Also create the dotnet executable after script download
|
||||
if "dotnet-install.sh" in str(path):
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet"
|
||||
dotnet_exe.touch()
|
||||
dotnet_exe.chmod(0o755)
|
||||
|
||||
mock_urlretrieve.side_effect = mock_retrieve
|
||||
mock_subprocess.return_value = Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
_ = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Verify bash script was downloaded
|
||||
assert mock_urlretrieve.called
|
||||
script_url = mock_urlretrieve.call_args[0][0]
|
||||
assert script_url == "https://dot.net/v1/dotnet-install.sh"
|
||||
|
||||
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server")
|
||||
@patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.platform.system")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.subprocess.run")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.shutil.which")
|
||||
def test_install_dotnet_uses_powershell_script_on_windows(
|
||||
self, mock_which, mock_urlretrieve, mock_subprocess, mock_platform, mock_platform_id, mock_ensure_ls
|
||||
):
|
||||
"""Test that Windows uses PowerShell install script."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
mock_platform.return_value = "Windows"
|
||||
mock_platform_id.return_value = PlatformId.WIN_x64
|
||||
mock_which.return_value = None # No system dotnet
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
def mock_retrieve(url, path):
|
||||
# Create the file that urlretrieve would create
|
||||
Path(path).touch()
|
||||
# Also create the dotnet executable after script download
|
||||
if "dotnet-install.ps1" in str(path):
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet.exe"
|
||||
dotnet_exe.touch()
|
||||
|
||||
mock_urlretrieve.side_effect = mock_retrieve
|
||||
mock_subprocess.return_value = Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
_ = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Verify PowerShell script was downloaded
|
||||
assert mock_urlretrieve.called
|
||||
script_url = mock_urlretrieve.call_args[0][0]
|
||||
assert script_url == "https://dot.net/v1/dotnet-install.ps1"
|
||||
|
||||
def test_uses_system_dotnet_10_if_available(self):
|
||||
"""Test that system .NET 10 is used if available instead of downloading."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
with patch("solidlsp.language_servers.csharp_language_server.shutil.which") as mock_which:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.subprocess.run") as mock_subprocess:
|
||||
with patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id") as mock_platform_id:
|
||||
with patch(
|
||||
"solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server"
|
||||
) as mock_ensure_ls:
|
||||
mock_which.return_value = "/usr/bin/dotnet"
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
|
||||
# Mock dotnet --list-runtimes output showing .NET 10
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "Microsoft.NETCore.App 10.0.1 [/usr/share/dotnet/shared/Microsoft.NETCore.App]"
|
||||
mock_result.returncode = 0
|
||||
mock_subprocess.return_value = mock_result
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should have used system dotnet
|
||||
assert dependency_provider._dotnet_path == "/usr/bin/dotnet"
|
||||
|
||||
def test_runtime_dependencies_no_longer_include_dotnet_downloads(self):
|
||||
"""Test that _RUNTIME_DEPENDENCIES no longer includes manual .NET download entries."""
|
||||
from solidlsp.language_servers.csharp_language_server import _RUNTIME_DEPENDENCIES
|
||||
|
||||
# Check that DotNetRuntime entries don't exist (we use install script instead)
|
||||
dotnet_deps = [dep for dep in _RUNTIME_DEPENDENCIES if dep.id == "DotNetRuntime"]
|
||||
|
||||
# After our changes, there should be no DotNetRuntime entries
|
||||
# (we'll use the install script instead)
|
||||
assert len(dotnet_deps) == 0, "DotNetRuntime dependencies should be removed in favor of install script"
|
||||
|
||||
@patch("solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server")
|
||||
@patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.platform.system")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.subprocess.run")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve")
|
||||
@patch("solidlsp.language_servers.csharp_language_server.shutil.which")
|
||||
def test_cached_dotnet_is_reused(self, mock_which, mock_urlretrieve, mock_subprocess, mock_platform, mock_platform_id, mock_ensure_ls):
|
||||
"""Test that cached .NET installation is reused without re-downloading."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
mock_platform.return_value = "Linux"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
mock_which.return_value = None
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Pre-create the cached dotnet executable
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet"
|
||||
dotnet_exe.touch()
|
||||
dotnet_exe.chmod(0o755)
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should use cached dotnet without downloading install script
|
||||
assert dependency_provider._dotnet_path == str(dotnet_exe)
|
||||
assert not mock_urlretrieve.called, "Should not download install script when cached dotnet exists"
|
||||
|
||||
def test_rejects_dotnet_9_and_installs_dotnet_10(self):
|
||||
"""Test that .NET 9 is rejected and .NET 10 install is triggered."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
with patch("solidlsp.language_servers.csharp_language_server.shutil.which") as mock_which:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.subprocess.run") as mock_subprocess:
|
||||
with patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id") as mock_platform_id:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve") as mock_urlretrieve:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.platform.system") as mock_platform:
|
||||
with patch(
|
||||
"solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server"
|
||||
) as mock_ensure_ls:
|
||||
mock_which.return_value = "/usr/bin/dotnet"
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
mock_platform.return_value = "Linux"
|
||||
|
||||
# Mock dotnet --list-runtimes output showing only .NET 9
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = "Microsoft.NETCore.App 9.0.1 [/usr/share/dotnet/shared/Microsoft.NETCore.App]"
|
||||
mock_result.returncode = 0
|
||||
|
||||
call_count = 0
|
||||
|
||||
def subprocess_side_effect(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
# First call: dotnet --list-runtimes returns .NET 9
|
||||
return mock_result
|
||||
else:
|
||||
# Second call: install script execution
|
||||
return Mock(returncode=0, stdout="", stderr="")
|
||||
|
||||
mock_subprocess.side_effect = subprocess_side_effect
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
|
||||
def mock_retrieve(url, path):
|
||||
Path(path).touch()
|
||||
if "dotnet-install.sh" in str(path):
|
||||
dotnet_dir = Path(temp_dir) / "dotnet-runtime-10.0"
|
||||
dotnet_dir.mkdir(parents=True, exist_ok=True)
|
||||
dotnet_exe = dotnet_dir / "dotnet"
|
||||
dotnet_exe.touch()
|
||||
dotnet_exe.chmod(0o755)
|
||||
|
||||
mock_urlretrieve.side_effect = mock_retrieve
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should have installed .NET 10 (not used system .NET 9)
|
||||
assert dependency_provider._dotnet_path == str(Path(temp_dir) / "dotnet-runtime-10.0" / "dotnet")
|
||||
assert mock_urlretrieve.called, "Should download install script when .NET 9 found"
|
||||
|
||||
def test_accepts_dotnet_11_and_higher(self):
|
||||
"""Test that .NET 11+ versions are accepted for forward compatibility."""
|
||||
from solidlsp.ls_utils import PlatformId
|
||||
|
||||
for version in ["11.0.1", "12.0.0", "15.3.2"]:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.shutil.which") as mock_which:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.subprocess.run") as mock_subprocess:
|
||||
with patch("solidlsp.language_servers.common.PlatformUtils.get_platform_id") as mock_platform_id:
|
||||
with patch(
|
||||
"solidlsp.language_servers.csharp_language_server.CSharpLanguageServer.DependencyProvider._ensure_language_server"
|
||||
) as mock_ensure_ls:
|
||||
mock_which.return_value = "/usr/bin/dotnet"
|
||||
mock_ensure_ls.return_value = "/fake/server.dll"
|
||||
mock_platform_id.return_value = PlatformId.LINUX_x64
|
||||
|
||||
# Mock dotnet --list-runtimes output showing .NET 11+
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = f"Microsoft.NETCore.App {version} [/usr/share/dotnet/shared/Microsoft.NETCore.App]"
|
||||
mock_result.returncode = 0
|
||||
mock_subprocess.return_value = mock_result
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Should use system dotnet for .NET 11+
|
||||
assert dependency_provider._dotnet_path == "/usr/bin/dotnet", f"Should accept .NET {version}"
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Tests for C# language server NuGet package download from NuGet.org."""
|
||||
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp.language_servers.common import RuntimeDependency
|
||||
from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
|
||||
from solidlsp.settings import SolidLSPSettings
|
||||
|
||||
|
||||
@pytest.mark.csharp
|
||||
class TestNuGetOrgDownload:
|
||||
"""Test downloading Roslyn language server packages from NuGet.org."""
|
||||
|
||||
def test_download_nuget_package_uses_direct_url(self):
|
||||
"""Test that _download_nuget_package uses the URL from RuntimeDependency directly."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
# Create a RuntimeDependency with a NuGet.org URL
|
||||
test_dependency = RuntimeDependency(
|
||||
id="TestPackage",
|
||||
description="Test package from NuGet.org",
|
||||
package_name="roslyn-language-server.linux-x64",
|
||||
package_version="5.5.0-2.26078.4",
|
||||
url="https://www.nuget.org/api/v2/package/roslyn-language-server.linux-x64/5.5.0-2.26078.4",
|
||||
platform_id="linux-x64",
|
||||
archive_type="nupkg",
|
||||
binary_name="Microsoft.CodeAnalysis.LanguageServer.dll",
|
||||
extract_path="content/LanguageServer/linux-x64",
|
||||
)
|
||||
|
||||
# Mock the dependency provider
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Mock urllib.request.urlretrieve to capture the URL being used
|
||||
with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve") as mock_retrieve:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.SafeZipExtractor"):
|
||||
try:
|
||||
dependency_provider._download_nuget_package(test_dependency)
|
||||
except Exception:
|
||||
# Expected to fail since we're mocking, but we want to check the URL
|
||||
pass
|
||||
|
||||
# Verify that urlretrieve was called with the NuGet.org URL
|
||||
assert mock_retrieve.called, "urlretrieve should be called"
|
||||
called_url = mock_retrieve.call_args[0][0]
|
||||
assert called_url == test_dependency.url, f"Should use URL from RuntimeDependency: {test_dependency.url}"
|
||||
assert "nuget.org" in called_url, "Should use NuGet.org URL"
|
||||
assert "azure" not in called_url.lower(), "Should not use Azure feed"
|
||||
|
||||
def test_runtime_dependencies_use_nuget_org_urls(self):
|
||||
"""Test that _RUNTIME_DEPENDENCIES are configured with NuGet.org URLs."""
|
||||
from solidlsp.language_servers.csharp_language_server import _RUNTIME_DEPENDENCIES
|
||||
|
||||
# Check language server dependencies
|
||||
lang_server_deps = [dep for dep in _RUNTIME_DEPENDENCIES if dep.id == "CSharpLanguageServer"]
|
||||
|
||||
assert len(lang_server_deps) == 6, "Should have 6 language server platform variants"
|
||||
|
||||
for dep in lang_server_deps:
|
||||
# Verify package name uses roslyn-language-server
|
||||
assert dep.package_name is not None, f"Package name should be set for {dep.platform_id}"
|
||||
assert dep.package_name.startswith(
|
||||
"roslyn-language-server."
|
||||
), f"Package name should start with 'roslyn-language-server.' but got: {dep.package_name}"
|
||||
|
||||
# Verify version is the newer NuGet.org version
|
||||
assert dep.package_version == "5.5.0-2.26078.4", f"Should use NuGet.org version 5.5.0-2.26078.4, got: {dep.package_version}"
|
||||
|
||||
# Verify URL points to NuGet.org
|
||||
assert dep.url is not None, f"URL should be set for {dep.platform_id}"
|
||||
assert "nuget.org" in dep.url, f"URL should point to nuget.org, got: {dep.url}"
|
||||
assert "azure" not in dep.url.lower(), f"URL should not point to Azure feed, got: {dep.url}"
|
||||
|
||||
def test_download_method_does_not_call_azure_feed(self):
|
||||
"""Test that the new download method does not attempt to access Azure feed."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
test_dependency = RuntimeDependency(
|
||||
id="TestPackage",
|
||||
description="Test package",
|
||||
package_name="roslyn-language-server.linux-x64",
|
||||
package_version="5.5.0-2.26078.4",
|
||||
url="https://www.nuget.org/api/v2/package/roslyn-language-server.linux-x64/5.5.0-2.26078.4",
|
||||
platform_id="linux-x64",
|
||||
archive_type="nupkg",
|
||||
binary_name="test.dll",
|
||||
)
|
||||
|
||||
mock_settings = SolidLSPSettings()
|
||||
custom_settings = SolidLSPSettings.CustomLSSettings({})
|
||||
|
||||
dependency_provider = CSharpLanguageServer.DependencyProvider(
|
||||
custom_settings=custom_settings,
|
||||
ls_resources_dir=temp_dir,
|
||||
solidlsp_settings=mock_settings,
|
||||
repository_root_path="/fake/repo",
|
||||
)
|
||||
|
||||
# Mock urllib.request.urlopen to track if Azure feed is accessed
|
||||
with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlopen") as mock_urlopen:
|
||||
with patch("solidlsp.language_servers.csharp_language_server.urllib.request.urlretrieve"):
|
||||
with patch("solidlsp.language_servers.csharp_language_server.SafeZipExtractor"):
|
||||
try:
|
||||
dependency_provider._download_nuget_package(test_dependency)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Verify that urlopen was NOT called (no service index lookup)
|
||||
assert not mock_urlopen.called, "Should not call urlopen for Azure service index lookup"
|
||||
Reference in new issue
Block a user