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:
Dominik Jain authored and GitHub committed 2026-02-03 11:08:19 +01:00
commit 2b04ab1879
6 files changed
+763 -189

No files matched your search

+44
View File
@@ -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:
"""
+3
View File
@@ -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 -9
View File
@@ -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"