mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 10:16:00 +00:00
Improve Haxe LS: configurable version/haxePath, top-level imports, CI docs, regression test
This commit is contained in:
1 parent
e9b6c0ff85
commit
addccfa41f
4 files changed
+71
-28
No files matched your search
@@ -387,6 +387,8 @@ jobs:
|
||||
shell: bash
|
||||
run: uv run pip install ansible-core ansible-lint
|
||||
- name: Install Haxe
|
||||
# The Haxe LS binary (server.js) is auto-downloaded and runs on Node.js,
|
||||
# but it delegates to the Haxe compiler for code analysis at runtime.
|
||||
uses: krdlab/setup-haxe@v2
|
||||
with:
|
||||
haxe-version: 4.3.7
|
||||
|
||||
@@ -496,7 +496,9 @@ Supported settings:
|
||||
| Setting | Default | Description |
|
||||
|---|---|---|
|
||||
| `ls_path` | auto-discovered | Path to the Haxe language server binary (e.g., `/path/to/server.js`). |
|
||||
| `version` | `2.34.2` | Override the vshaxe extension version downloaded from Open VSX. SHA256 verification is only performed for the default version. |
|
||||
| `buildFile` | auto-discovered `.hxml` | Relative path to the `.hxml` build file used for compilation (e.g., `build/debug.hxml`). If not set, Serena searches the project for `.hxml` files (max depth 5, skipping dependency directories). |
|
||||
| `haxePath` | `haxe` from PATH | Path to the Haxe compiler executable. The LS delegates to this for code analysis. Useful when multiple Haxe versions are installed or when `haxe` is not on the PATH. |
|
||||
| `renameSourceFolders` | not set (LS default) | List of source directories for scoping rename operations (e.g., `["src", "lib"]`). If not set, the Haxe LS uses its own defaults. |
|
||||
|
||||
Example (typically in `project.yml`, since these are project-specific):
|
||||
@@ -505,6 +507,7 @@ Example (typically in `project.yml`, since these are project-specific):
|
||||
ls_specific_settings:
|
||||
haxe:
|
||||
buildFile: "build/debug.hxml"
|
||||
haxePath: "/usr/local/bin/haxe"
|
||||
renameSourceFolders: ["src", "lib"]
|
||||
```
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import shutil
|
||||
import tempfile
|
||||
import threading
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
from overrides import override
|
||||
|
||||
@@ -55,9 +58,8 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
class DependencyProvider(LanguageServerDependencyProviderSinglePath):
|
||||
# Downloaded from Open VSX (not the VS Code Marketplace) because Open VSX
|
||||
# provides stable versioned URLs and SHA256 checksums for integrity verification.
|
||||
_VSHAXE_VERSION = "2.34.2"
|
||||
_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2"
|
||||
_VSHAXE_DOWNLOAD_URL = f"https://open-vsx.org/api/nadako/vshaxe/{_VSHAXE_VERSION}/file/nadako.vshaxe-{_VSHAXE_VERSION}.vsix"
|
||||
_DEFAULT_VSHAXE_VERSION = "2.34.2"
|
||||
_DEFAULT_VSHAXE_SHA256 = "104d785e3f7b57a7f3debf520d9751f7e7abf3a7e78d203db1a8ff3dc7ca30e2"
|
||||
|
||||
@override
|
||||
def _get_or_install_core_dependency(self) -> str:
|
||||
@@ -90,7 +92,8 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
" 3. Set ls_path in serena_config.yml under ls_specific_settings.haxe"
|
||||
)
|
||||
|
||||
downloaded_path = self._download_from_open_vsx(haxe_ls_dir)
|
||||
version = self._custom_settings.get("version", self._DEFAULT_VSHAXE_VERSION)
|
||||
downloaded_path = self._download_from_open_vsx(haxe_ls_dir, version)
|
||||
if downloaded_path:
|
||||
return downloaded_path
|
||||
|
||||
@@ -116,32 +119,31 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _download_from_open_vsx(cls, target_dir: str) -> str | None:
|
||||
"""Download a pinned vshaxe VSIX from Open VSX and extract server.js.
|
||||
Verifies the download against a hardcoded SHA256 checksum.
|
||||
def _download_from_open_vsx(cls, target_dir: str, version: str) -> str | None:
|
||||
"""Download a vshaxe VSIX from Open VSX and extract server.js.
|
||||
Verifies the download against a hardcoded SHA256 checksum when using the default version.
|
||||
"""
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
try:
|
||||
import urllib.request
|
||||
|
||||
log.info("Downloading Haxe Language Server v%s from Open VSX...", cls._VSHAXE_VERSION)
|
||||
download_url = f"https://open-vsx.org/api/nadako/vshaxe/{version}/file/nadako.vshaxe-{version}.vsix"
|
||||
log.info("Downloading Haxe Language Server v%s from Open VSX...", version)
|
||||
vsix_path = os.path.join(tempfile.gettempdir(), "vshaxe.vsix")
|
||||
urllib.request.urlretrieve(cls._VSHAXE_DOWNLOAD_URL, vsix_path)
|
||||
urllib.request.urlretrieve(download_url, vsix_path)
|
||||
|
||||
# Verify SHA256 checksum
|
||||
sha256 = hashlib.sha256()
|
||||
with open(vsix_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
sha256.update(chunk)
|
||||
if sha256.hexdigest().lower() != cls._VSHAXE_SHA256:
|
||||
os.remove(vsix_path)
|
||||
raise RuntimeError(
|
||||
f"SHA256 checksum mismatch for vshaxe VSIX. Expected {cls._VSHAXE_SHA256}, "
|
||||
f"got {sha256.hexdigest()}. The file may be corrupted or tampered with."
|
||||
)
|
||||
log.info("SHA256 checksum verified")
|
||||
# Verify SHA256 checksum only for the default (pinned) version
|
||||
if version == cls._DEFAULT_VSHAXE_VERSION:
|
||||
sha256 = hashlib.sha256()
|
||||
with open(vsix_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8192), b""):
|
||||
sha256.update(chunk)
|
||||
if sha256.hexdigest().lower() != cls._DEFAULT_VSHAXE_SHA256:
|
||||
os.remove(vsix_path)
|
||||
raise RuntimeError(
|
||||
f"SHA256 checksum mismatch for vshaxe VSIX. Expected {cls._DEFAULT_VSHAXE_SHA256}, "
|
||||
f"got {sha256.hexdigest()}. The file may be corrupted or tampered with."
|
||||
)
|
||||
log.info("SHA256 checksum verified")
|
||||
else:
|
||||
log.info("Using custom version %s — skipping SHA256 verification", version)
|
||||
|
||||
# VSIX files are ZIP archives — extract bin/ contents
|
||||
bin_dir = os.path.join(target_dir, "bin")
|
||||
@@ -160,7 +162,7 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
|
||||
server_js_path = os.path.join(bin_dir, "server.js")
|
||||
if os.path.exists(server_js_path):
|
||||
log.info(f"Haxe Language Server v{cls._VSHAXE_VERSION} installed to {server_js_path}")
|
||||
log.info(f"Haxe Language Server v{version} installed to {server_js_path}")
|
||||
return server_js_path
|
||||
|
||||
log.error("Downloaded VSIX but server.js not found after extraction")
|
||||
@@ -205,6 +207,10 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
if rename_source_folders:
|
||||
init_options["renameSourceFolders"] = rename_source_folders
|
||||
|
||||
haxe_path = self._custom_settings.get("haxePath")
|
||||
if haxe_path:
|
||||
init_options["haxePath"] = haxe_path
|
||||
|
||||
initialize_params = {
|
||||
"locale": "en",
|
||||
"capabilities": {
|
||||
@@ -357,7 +363,11 @@ class HaxeLanguageServer(SolidLanguageServer):
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# LS doesn't properly initialise without a workspace_did_change_configuration notification here.
|
||||
self.server.notify.workspace_did_change_configuration({"settings": {}})
|
||||
config_settings: dict = {}
|
||||
haxe_path = self._custom_settings.get("haxePath")
|
||||
if haxe_path:
|
||||
config_settings["haxePath"] = haxe_path
|
||||
self.server.notify.workspace_did_change_configuration({"settings": config_settings})
|
||||
|
||||
log.info("Waiting for Haxe LSP compilation to complete...")
|
||||
if self._server_ready.wait(timeout=self._COMPILATION_TIMEOUT):
|
||||
|
||||
@@ -247,3 +247,31 @@ class TestHaxeLanguageServer:
|
||||
for s in overview:
|
||||
assert s.get("name"), "Symbol missing 'name' field"
|
||||
assert s.get("kind"), "Symbol missing 'kind' field"
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.HAXE], indirect=True)
|
||||
def test_rapid_successive_requests(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Verify that rapid successive requests don't return empty results.
|
||||
|
||||
The Haxe LS triggers a recompilation on didOpen. Without suppression,
|
||||
subsequent requests during recompilation may return empty results.
|
||||
"""
|
||||
main_path = os.path.join("src", "Main.hx")
|
||||
helper_path = os.path.join("src", "utils", "Helper.hx")
|
||||
|
||||
# First request: hover on greet in Main.hx (line 22 is the greet method definition)
|
||||
hover1 = language_server.request_hover(main_path, 22, 20)
|
||||
|
||||
# Second request: references in a different file (triggers another didOpen)
|
||||
all_symbols, _ = language_server.request_document_symbols(helper_path).get_all_symbols_and_roots()
|
||||
add_numbers = next((s for s in all_symbols if s.get("name") == "addNumbers"), None)
|
||||
assert add_numbers is not None
|
||||
sel_start = add_numbers["selectionRange"]["start"]
|
||||
refs = language_server.request_references(helper_path, sel_start["line"], sel_start["character"])
|
||||
|
||||
# Third request: back to Main.hx hover (would be affected by recompilation)
|
||||
hover2 = language_server.request_hover(main_path, 22, 20)
|
||||
|
||||
# All three should return non-empty results
|
||||
assert hover1 is not None, "First hover returned None"
|
||||
assert refs, "References returned empty after switching files"
|
||||
assert hover2 is not None, "Second hover returned None after file switching"
|
||||
Reference in new issue
Block a user