mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-03 09:13:49 +00:00
Java (JDT-LS): allow registering additional JRE/JDK runtimes via config (#1715)
* Java (JDT-LS): allow registering additional JRE/JDK runtimes via config JDT-LS resolves each imported module's JRE_CONTAINER/.../JavaSE-NN/ classpath entry against its own "Installed JREs" list, which Serena previously hardcoded to a single JavaSE-21 entry pointing at the bundled JRE. Projects whose source/target level exceeds that bundled JDK (e.g. sourceCompatibility = VERSION_25) get a container that never resolves, so every JDK type -- java.lang.Object, java.util.*, etc. -- comes back "cannot be resolved", silently breaking diagnostics and cross-module resolution. There was no config key to tell JDT-LS about an installed newer JDK. Add an optional `runtimes` list under ls_specific_settings.java, mirroring VS Code's java.configuration.runtimes shape (name, path, optional default/sources/javadoc). Entries are validated (name+path required, path must exist) via the new _resolve_configured_runtimes(), then merged with the bundled JavaSE-21 default: configured runtimes extend the list; an entry reusing the "JavaSE-21" name overrides the bundled one; if a configured entry claims `default`, the bundled runtime's own default flag is cleared so JDT-LS doesn't see two defaults. The `runtimes` key is also added to the JDTLS workspace-hash inputs so changing it lands in a fresh workspace instead of reusing a stale Buildship/Maven import. Testing: added 14 unit tests in test/solidlsp/java/test_jdtls_path_resolution.py covering _resolve_configured_runtimes validation (missing keys, wrong types, nonexistent paths) and the merge behavior in _create_base_initialize_params (extension, name-collision override, default hand-off). Ran `poe format`, `poe type-check`, and `pytest test/solidlsp/java -m "not java"` (69 passed, 8 deselected real-JDTLS tests that require a JDK) locally. Fixes #1478 * docs: document java.runtimes in configuration guide Address review feedback on #1715 by documenting ls_specific_settings.java.runtimes in 050_configuration.md (settings table, note, and example). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: arimu1 <19286898+arimu1@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4 files changed
+231
-4
No files matched your search
@@ -15,6 +15,14 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
line; any other out-of-range end position now raises `InvalidTextLocationError` instead,
|
||||
rather than guessing at a body that could be wrong #1498
|
||||
|
||||
* Language Servers:
|
||||
- Java (JDT-LS): add `runtimes` to `ls_specific_settings.java`, a list of extra JRE/JDK entries
|
||||
(`name`, `path`, optional `default`/`sources`/`javadoc`) passed through to JDT-LS's
|
||||
`java.configuration.runtimes`. Fixes silently broken JDK type resolution (`java.lang.Object`
|
||||
and other JDK types reported as "cannot be resolved") for projects whose source/target level
|
||||
exceeds the bundled JDK 21 JRE JDT-LS registers by default; configured runtimes extend rather
|
||||
than replace that bundled default. #1478
|
||||
|
||||
* Tools:
|
||||
- Fix: `search_for_pattern` marked one line too many as matched whenever a match ended with a line
|
||||
break, because the match's exclusive end index was mapped to a line number directly and therefore
|
||||
|
||||
@@ -697,6 +697,7 @@ The following settings are supported for the Java language server:
|
||||
| `gradle_wrapper_enabled` | `false` | Use the project's Gradle wrapper (`gradlew`) instead of the bundled Gradle distribution. Enable this for projects with custom plugins or repositories. |
|
||||
| `gradle_java_home` | `null` | Path to the JDK used by Gradle. When unset, Gradle uses `JAVA_HOME` if `use_system_java_home` is enabled and `JAVA_HOME` is set; otherwise it falls back to Serena's bundled JRE. |
|
||||
| `use_system_java_home` | `false` | Use the system's `JAVA_HOME` environment variable for JDTLS itself and, when `gradle_java_home` is unset, Gradle import. Enable this if your project requires a specific JDK vendor or version for Gradle's JDK checks. |
|
||||
| `runtimes` | `[]` | Extra JRE/JDK entries registered with JDT-LS via `java.configuration.runtimes`. Use this when a project's source/target level exceeds the JDK JDT-LS itself runs on (currently JDK 21 in default vscode-java VSIX mode). Each entry is a mapping with required `name` (e.g. `JavaSE-25`, matching the `JavaSE-NN` container the build tool requests) and `path` (JDK/JRE home directory; must exist), plus optional `default`, `sources`, and `javadoc` (passed through to JDT-LS). Entries extend rather than replace the bundled `JavaSE-21` runtime; an entry that reuses the `JavaSE-21` name overrides the bundled one. Changing this setting invalidates the JDTLS workspace hash so a fresh import is performed. |
|
||||
| `gradle_version` | `8.14.2` | (vscode-java mode only) Override the Gradle distribution version Serena downloads by default. |
|
||||
| `vscode_java_version` | `1.54.0-923` | (vscode-java mode only) Override the bundled `vscode-java` runtime bundle version Serena downloads by default. |
|
||||
| `intellicode_version` | `1.2.30` | (vscode-java mode only) Override the IntelliCode VSIX version Serena downloads by default. |
|
||||
@@ -716,6 +717,10 @@ Notes:
|
||||
- In upstream-jdtls mode the `gradle_version`, `vscode_java_version`, `intellicode_version`,
|
||||
`intellicode_xmx`, `intellicode_xms` settings are silently ignored — they only apply to the
|
||||
vscode-java VSIX mode.
|
||||
- Without `runtimes`, JDT-LS only knows about the bundled `JavaSE-21` JRE. Projects that request a newer
|
||||
container (e.g. `sourceCompatibility = JavaVersion.VERSION_25`) then fail to resolve JDK types such as
|
||||
`java.lang.Object`. Register the matching installed JDK via `runtimes` instead of symlinking over Serena's
|
||||
bundled JRE directory.
|
||||
|
||||
Example: upstream-jdtls mode (offline / corporate network):
|
||||
|
||||
@@ -736,6 +741,19 @@ ls_specific_settings:
|
||||
use_system_java_home: true
|
||||
```
|
||||
|
||||
Example: register an additional JDK for a project targeting a newer Java version:
|
||||
|
||||
```yaml
|
||||
ls_specific_settings:
|
||||
java:
|
||||
runtimes:
|
||||
- name: JavaSE-21
|
||||
path: /usr/lib/jvm/java-21-openjdk
|
||||
- name: JavaSE-25
|
||||
path: /home/user/Java/jdk25
|
||||
default: true
|
||||
```
|
||||
|
||||
#### Kotlin
|
||||
|
||||
Serena uses [JetBrains' Kotlin Language Server](https://github.com/Kotlin/kotlin-lsp) for Kotlin support.
|
||||
|
||||
@@ -15,7 +15,7 @@ import subprocess
|
||||
import threading
|
||||
from pathlib import Path, PurePath
|
||||
from time import sleep
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
from overrides import override
|
||||
|
||||
@@ -199,6 +199,17 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
are not supported in default VSIX mode (the resource paths inside the archive change between
|
||||
releases); use upstream-jdtls mode for arbitrary versions.
|
||||
- intellicode_version: Override the pinned IntelliCode VSIX version downloaded by Serena
|
||||
- runtimes: Additional JRE/JDK entries to register with JDT-LS's ``java.configuration.runtimes``,
|
||||
for projects whose source/target level exceeds the JDK JDT-LS itself runs on (currently
|
||||
JDK 21 in default vscode-java VSIX mode). Each entry is a mapping with:
|
||||
- name (required): the JRE container name JDT-LS should register the entry under,
|
||||
e.g. "JavaSE-25" (must match the ``JavaSE-NN`` the build tool requests).
|
||||
- path (required): filesystem path to the JDK/JRE home directory; must exist.
|
||||
- default (optional): whether this runtime is JDT-LS's default when no container name matches.
|
||||
- sources / javadoc (optional): passed through unchanged to JDT-LS.
|
||||
These entries extend rather than replace the bundled JRE, which is still registered as
|
||||
"JavaSE-21" unless a configured entry reuses that same name (in which case it is overridden).
|
||||
See serena #1478.
|
||||
|
||||
Example configuration for upstream JDTLS mode (no downloads, suitable for offline/corporate):
|
||||
```yaml
|
||||
@@ -228,6 +239,10 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
gradle_version: "8.14.2"
|
||||
vscode_java_version: "1.54.0-923" # also accepts pinned legacy "1.42.0-561"
|
||||
intellicode_version: "1.2.30"
|
||||
runtimes: # register additional JDKs for projects targeting a newer Java version
|
||||
- name: "JavaSE-25"
|
||||
path: "/home/user/Java/jdk25"
|
||||
default: true
|
||||
```
|
||||
"""
|
||||
|
||||
@@ -779,6 +794,7 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
"java_home",
|
||||
"use_system_java_home",
|
||||
"maven_offline",
|
||||
"runtimes",
|
||||
)
|
||||
workspace_settings = {key: custom_settings.settings[key] for key in workspace_setting_keys if key in custom_settings.settings}
|
||||
workspace_settings_json = json.dumps(workspace_settings, sort_keys=True, separators=(",", ":"))
|
||||
@@ -922,6 +938,52 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
log.info(f"Using bundled JRE for Gradle: {self.runtime_dependency_paths.jre_path}")
|
||||
return self.runtime_dependency_paths.jre_path
|
||||
|
||||
def _resolve_configured_runtimes(self) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Validate and normalize the optional extra JRE/JDK runtimes to register with JDT-LS, as configured
|
||||
via ``ls_specific_settings.java.runtimes``. Each entry mirrors the shape VS Code's Java extension
|
||||
sends via ``java.configuration.runtimes`` (``name``, ``path``, optional ``default``/``sources``/
|
||||
``javadoc``); see serena #1478.
|
||||
|
||||
:return: the validated list of runtime dicts (empty if the setting is unset)
|
||||
:raises ValueError: if the setting or one of its entries is malformed
|
||||
:raises FileNotFoundError: if an entry's ``path`` does not exist
|
||||
"""
|
||||
configured_runtimes = self._custom_settings.get("runtimes", [])
|
||||
if not isinstance(configured_runtimes, list):
|
||||
raise ValueError(
|
||||
f"ls_specific_settings.java.runtimes must be a list of {{name, path}} entries, "
|
||||
f"got {type(configured_runtimes).__name__}: {configured_runtimes!r}"
|
||||
)
|
||||
|
||||
# validate each entry and normalize it to the shape JDT-LS expects...
|
||||
validated_runtimes: list[dict[str, Any]] = []
|
||||
for entry in configured_runtimes:
|
||||
if not isinstance(entry, dict) or "name" not in entry or "path" not in entry:
|
||||
raise ValueError(
|
||||
f"Invalid ls_specific_settings.java.runtimes entry {entry!r}: each entry requires at least a 'name' and a 'path' key."
|
||||
)
|
||||
path = entry["path"]
|
||||
if not os.path.exists(path):
|
||||
error_msg = (
|
||||
f"ls_specific_settings.java.runtimes entry '{entry['name']}' points to a path that "
|
||||
f"does not exist: {path}. Fix: update the path in ~/.serena/serena_config.yml "
|
||||
f"(ls_specific_settings -> java -> runtimes), or remove the entry."
|
||||
)
|
||||
log.error(error_msg)
|
||||
raise FileNotFoundError(error_msg)
|
||||
|
||||
runtime: dict[str, Any] = {"name": entry["name"], "path": path}
|
||||
if "default" in entry:
|
||||
runtime["default"] = bool(entry["default"])
|
||||
for optional_key in ("sources", "javadoc"):
|
||||
if optional_key in entry:
|
||||
runtime[optional_key] = entry[optional_key]
|
||||
validated_runtimes.append(runtime)
|
||||
log.info(f"Registering additional JDT-LS runtime '{runtime['name']}' from custom settings: {path}")
|
||||
|
||||
return validated_runtimes
|
||||
|
||||
def _create_base_initialize_params(self) -> dict:
|
||||
"""
|
||||
Returns the initialize parameters for the EclipseJDTLS server.
|
||||
@@ -1304,9 +1366,19 @@ class EclipseJDTLS(SolidLanguageServer):
|
||||
else:
|
||||
initialize_params["initializationOptions"]["bundles"] = []
|
||||
|
||||
initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = [
|
||||
{"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True}
|
||||
]
|
||||
# merge the bundled JRE with any additional runtimes configured via ls_specific_settings.java.runtimes
|
||||
# (e.g. so projects targeting a newer Java version than the bundled JRE resolve their JRE container)...
|
||||
default_runtime = {"name": "JavaSE-21", "path": self.runtime_dependency_paths.jre_home_path, "default": True}
|
||||
configured_runtimes = self._resolve_configured_runtimes()
|
||||
runtimes_by_name = {default_runtime["name"]: default_runtime}
|
||||
for runtime in configured_runtimes:
|
||||
runtimes_by_name[runtime["name"]] = runtime
|
||||
if runtimes_by_name["JavaSE-21"] is default_runtime and any(runtime.get("default") for runtime in configured_runtimes):
|
||||
# a configured runtime claims the JDT-LS default; the bundled runtime must not also claim it,
|
||||
# since JDT-LS expects at most one default runtime
|
||||
default_runtime["default"] = False
|
||||
|
||||
initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"] = list(runtimes_by_name.values())
|
||||
|
||||
for runtime in initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]:
|
||||
assert "name" in runtime
|
||||
|
||||
@@ -644,3 +644,132 @@ class TestIsIgnoredDirname:
|
||||
@pytest.mark.parametrize("dirname", [".git", ".venv", ".idea", ".serena", ".mypy_cache"])
|
||||
def test_always_ignored_dirs_are_still_ignored(self, jdtls: EclipseJDTLS, dirname: str) -> None:
|
||||
assert jdtls.is_ignored_dirname(dirname) is True
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# _resolve_configured_runtimes / configured `runtimes` initialize settings (#1478)
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _runtimes(initialize_params: dict) -> list[dict]:
|
||||
"""
|
||||
Return the JDT-LS ``java.configuration.runtimes`` list from initialize parameters.
|
||||
|
||||
:param initialize_params: JDTLS initialize-parameter payload.
|
||||
:return: The configured runtimes list.
|
||||
"""
|
||||
return initialize_params["initializationOptions"]["settings"]["java"]["configuration"]["runtimes"]
|
||||
|
||||
|
||||
class TestResolveConfiguredRuntimes:
|
||||
def test_empty_when_unset(self, custom_settings: SolidLSPSettings.CustomLSSettings) -> None:
|
||||
server = object.__new__(EclipseJDTLS)
|
||||
server._custom_settings = custom_settings
|
||||
assert server._resolve_configured_runtimes() == []
|
||||
|
||||
def test_valid_entry_is_normalized(self, tmp_path: Path) -> None:
|
||||
jdk = tmp_path / "jdk-25"
|
||||
jdk.mkdir()
|
||||
server = object.__new__(EclipseJDTLS)
|
||||
server._custom_settings = SolidLSPSettings.CustomLSSettings(
|
||||
{"runtimes": [{"name": "JavaSE-25", "path": str(jdk), "default": True}]}
|
||||
)
|
||||
assert server._resolve_configured_runtimes() == [{"name": "JavaSE-25", "path": str(jdk), "default": True}]
|
||||
|
||||
def test_optional_sources_and_javadoc_pass_through(self, tmp_path: Path) -> None:
|
||||
jdk = tmp_path / "jdk-25"
|
||||
jdk.mkdir()
|
||||
server = object.__new__(EclipseJDTLS)
|
||||
server._custom_settings = SolidLSPSettings.CustomLSSettings(
|
||||
{"runtimes": [{"name": "JavaSE-25", "path": str(jdk), "sources": "/src", "javadoc": "/doc"}]}
|
||||
)
|
||||
assert server._resolve_configured_runtimes() == [{"name": "JavaSE-25", "path": str(jdk), "sources": "/src", "javadoc": "/doc"}]
|
||||
|
||||
def test_raises_when_runtimes_is_not_a_list(self) -> None:
|
||||
server = object.__new__(EclipseJDTLS)
|
||||
server._custom_settings = SolidLSPSettings.CustomLSSettings({"runtimes": {"name": "JavaSE-25", "path": "/x"}})
|
||||
with pytest.raises(ValueError, match="must be a list"):
|
||||
server._resolve_configured_runtimes()
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entry", [{"path": "/x"}, {"name": "JavaSE-25"}, "not-a-dict", 42], ids=["missing-name", "missing-path", "string", "int"]
|
||||
)
|
||||
def test_raises_for_malformed_entry(self, entry: object) -> None:
|
||||
server = object.__new__(EclipseJDTLS)
|
||||
server._custom_settings = SolidLSPSettings.CustomLSSettings({"runtimes": [entry]})
|
||||
with pytest.raises(ValueError, match="requires at least a 'name' and a 'path' key"):
|
||||
server._resolve_configured_runtimes()
|
||||
|
||||
def test_raises_for_nonexistent_path(self, tmp_path: Path) -> None:
|
||||
server = object.__new__(EclipseJDTLS)
|
||||
server._custom_settings = SolidLSPSettings.CustomLSSettings(
|
||||
{"runtimes": [{"name": "JavaSE-25", "path": str(tmp_path / "missing-jdk")}]}
|
||||
)
|
||||
with pytest.raises(FileNotFoundError, match="does not exist"):
|
||||
server._resolve_configured_runtimes()
|
||||
|
||||
|
||||
class TestConfiguredRuntimesInitializeSettings:
|
||||
def test_bundled_runtime_is_sole_default_when_unconfigured(
|
||||
self, tmp_path: Path, custom_settings: SolidLSPSettings.CustomLSSettings
|
||||
) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
||||
|
||||
server = _make_uninitialized_jdtls(repo, {}, runtime_paths)
|
||||
|
||||
assert _runtimes(server._create_base_initialize_params()) == [
|
||||
{"name": "JavaSE-21", "path": runtime_paths.jre_home_path, "default": True}
|
||||
]
|
||||
|
||||
def test_configured_runtime_extends_bundled_runtime(self, tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
||||
jdk25 = tmp_path / "jdk-25"
|
||||
jdk25.mkdir()
|
||||
|
||||
server = _make_uninitialized_jdtls(repo, {"runtimes": [{"name": "JavaSE-25", "path": str(jdk25)}]}, runtime_paths)
|
||||
|
||||
runtimes = _runtimes(server._create_base_initialize_params())
|
||||
assert {"name": "JavaSE-21", "path": runtime_paths.jre_home_path, "default": True} in runtimes
|
||||
assert {"name": "JavaSE-25", "path": str(jdk25)} in runtimes
|
||||
assert len(runtimes) == 2
|
||||
|
||||
def test_configured_runtime_with_same_name_overrides_bundled_runtime(self, tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
||||
override_jdk = tmp_path / "override-jdk-21"
|
||||
override_jdk.mkdir()
|
||||
|
||||
server = _make_uninitialized_jdtls(repo, {"runtimes": [{"name": "JavaSE-21", "path": str(override_jdk)}]}, runtime_paths)
|
||||
|
||||
runtimes = _runtimes(server._create_base_initialize_params())
|
||||
assert runtimes == [{"name": "JavaSE-21", "path": str(override_jdk)}]
|
||||
|
||||
def test_configured_default_unsets_bundled_default(self, tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
||||
jdk25 = tmp_path / "jdk-25"
|
||||
jdk25.mkdir()
|
||||
|
||||
server = _make_uninitialized_jdtls(repo, {"runtimes": [{"name": "JavaSE-25", "path": str(jdk25), "default": True}]}, runtime_paths)
|
||||
|
||||
runtimes = _runtimes(server._create_base_initialize_params())
|
||||
bundled = next(r for r in runtimes if r["name"] == "JavaSE-21")
|
||||
configured = next(r for r in runtimes if r["name"] == "JavaSE-25")
|
||||
assert bundled["default"] is False
|
||||
assert configured["default"] is True
|
||||
|
||||
def test_invalid_configured_runtime_raises_during_initialize_params(self, tmp_path: Path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
runtime_paths = _make_runtime_dependency_paths(tmp_path / "runtime")
|
||||
|
||||
server = _make_uninitialized_jdtls(repo, {"runtimes": [{"name": "JavaSE-25"}]}, runtime_paths)
|
||||
|
||||
with pytest.raises(ValueError, match="requires at least a 'name' and a 'path' key"):
|
||||
server._create_base_initialize_params()
|
||||
Reference in new issue
Block a user