fix: disable automatic type acquisition in TypeScript backends (#1990)

* fix: disable automatic type acquisition in TypeScript backends

* fix: use shared initialization builder for VTS

Follow review guidance to apply shallow per-key overrides without restoring the ATA default after a user replaces the typescript block. Preserve the legacy settings alias, document the behavior, and cover canonical-key precedence.

---------

Co-authored-by: GuddXzy <267062192+GuddXzy@users.noreply.github.com>
This commit is contained in:
GuddXzyandGuddXzy authored and GitHub committed 2026-09-08 11:17:35 +02:00
1 parent 9f9db76622
commit 2d2e87763a
4 files changed
+137 -39

No files matched your search

+5
View File
@@ -33,6 +33,11 @@ Status of the `main` branch. Changes prior to the next official version change w
Serena's own tools to close the gap (#1852)
* Language Servers:
- Fix: TypeScript and VTS now disable automatic type acquisition as intended, while VTS
preserves explicit user settings across initialization and configuration requests (#1989)
VTS initialization options now override defaults per top-level key rather than replacing the
entire configuration; a user-provided `typescript` block replaces the ATA default too.
`initializationOptions` takes precedence over the legacy `initialization_options` alias.
- Add FreeBSD mapping to platform detection
- Remove unnecessary platform checks from the following language servers, expanding the set of
supported platforms accordingly: Elixir Tools, Intelephense, Perl, TypeScript, VTS
@@ -339,9 +339,7 @@ class TypeScriptLanguageServer(SolidLanguageServer):
# slow, network-dependent, and nondeterministic (and can hang on offline/locked-down
# machines). Serena relies on the types already installed in the project instead.
"initializationOptions": {
"preferences": {
"disableAutomaticTypingAcquisition": True,
},
"disableAutomaticTypingAcquisition": True,
},
"capabilities": {
"textDocument": {
@@ -8,6 +8,7 @@ import logging
import os
import shutil
import threading
from typing import cast
from overrides import override
@@ -34,16 +35,18 @@ class VtsLanguageServer(SolidLanguageServer):
Supported entries in ``ls_specific_settings["typescript_vts"]``:
- ``vtsls_version``: version of ``@vtsls/language-server`` to install (default: ``"0.2.9"``).
- ``npm_registry``: custom npm registry for the managed install.
- ``initialization_options``: optional dict forwarded verbatim as LSP
``initializationOptions``. Useful for Yarn PnP projects, e.g.::
- ``initializationOptions``: optional LSP configuration, overriding defaults per top-level key.
Automatic type acquisition is disabled by default, but a user-provided ``typescript`` block
replaces that entire default block. Useful for Yarn PnP projects, e.g.::
initialization_options:
initializationOptions:
typescript:
tsdk: "project/.yarn/sdks/typescript/lib"
vtsls:
autoUseWorkspaceTsdk: true
See https://github.com/yioneko/vtsls/issues/169 for the PnP recipe.
- ``initialization_options``: legacy alias, used only when ``initializationOptions`` is absent.
"""
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
@@ -109,35 +112,18 @@ class VtsLanguageServer(SolidLanguageServer):
assert os.path.exists(vts_executable_path), "vtsls executable not found. Please install @vtsls/language-server and try again."
return f"{vts_executable_path} --stdio"
@property
def _initialization_options(self) -> dict:
"""
Validated user-provided ``initializationOptions``.
:raises ValueError: if ``ls_specific_settings.typescript_vts.initialization_options``
is set to a value that is not a dict.
"""
opts = self._custom_settings.get("initialization_options")
if opts is None:
return {}
if not isinstance(opts, dict):
raise ValueError(f"ls_specific_settings.typescript_vts.initialization_options must be a dict, got {type(opts).__name__}")
return opts
def _create_base_initialize_params(self) -> dict:
"""
Returns the initialize params for the VTS Language Server.
If ``initialization_options`` is set in ``ls_specific_settings["typescript_vts"]``,
it is forwarded verbatim as LSP ``initializationOptions``.
"""
# normalize the legacy key for the shared builder without changing caller-owned settings
settings = self._custom_settings.settings
if "initialization_options" in settings and "initializationOptions" not in settings:
self._custom_settings.settings = {**settings, "initializationOptions": settings["initialization_options"]}
initialize_params: dict = {
"locale": "en",
"initializationOptions": {
"preferences": {
"disableAutomaticTypingAcquisition": True,
},
},
"initializationOptions": {"typescript": {"disableAutomaticTypeAcquisition": True}},
"capabilities": {
"textDocument": {
"synchronization": {"didSave": True, "dynamicRegistration": True},
@@ -161,10 +147,6 @@ class VtsLanguageServer(SolidLanguageServer):
},
}
if self._initialization_options:
log.info("Forwarding user-provided initializationOptions to vtsls: %s", self._initialization_options)
initialize_params["initializationOptions"] = self._initialization_options
return initialize_params
def _start_server(self) -> None:
@@ -192,12 +174,13 @@ class VtsLanguageServer(SolidLanguageServer):
def execute_client_command_handler(params: dict) -> list:
return []
init_options = self._initialization_options
# share the final configuration across initialization, requests and notifications
initialize_params = self._create_initialize_params()
init_options = cast(dict, initialize_params["initializationOptions"])
def workspace_configuration_handler(params: dict) -> list[object]:
# vtsls pulls settings for sections like "typescript", "vtsls", "javascript".
# Return the matching sub-dicts from the user-provided initialization_options.
return [init_options.get(item.get("section", ""), {}) for item in params["items"]]
# vtsls requests the whole configuration (empty section) during startup
return [init_options.get(item["section"], {}) if item.get("section") else init_options for item in params["items"]]
def do_nothing(params: dict) -> None:
return
@@ -222,7 +205,6 @@ class VtsLanguageServer(SolidLanguageServer):
log.info("Starting VTS server process")
self.server.start()
initialize_params = self._create_initialize_params()
log.info("Sending initialize request from LSP client to LSP server and awaiting response")
init_response = self.server.send.initialize(initialize_params)
@@ -243,7 +225,7 @@ class VtsLanguageServer(SolidLanguageServer):
# vtsls also reads settings via workspace/didChangeConfiguration (in addition
# to initializationOptions and workspace/configuration pulls). Push the same
# user-provided settings on all three channels for maximum compatibility,
# effective settings on all three channels for maximum compatibility,
# e.g. so that `typescript.tsdk` is honoured for Yarn PnP projects.
if init_options:
self.server.notify.workspace_did_change_configuration({"settings": init_options})
@@ -0,0 +1,113 @@
"""Automatic type acquisition must be disabled on the semantic TypeScript worker."""
import copy
import os
import threading
from pathlib import Path
from unittest.mock import Mock
import psutil
import pytest
from solidlsp.language_servers.vts_language_server import VtsLanguageServer
from solidlsp.ls_config import LanguageServerConfig, LanguageServerId
from solidlsp.settings import SolidLSPSettings
from test.conftest import get_repo_path, start_ls_context
@pytest.mark.typescript
@pytest.mark.parametrize("backend", [LanguageServerId.TYPESCRIPT, LanguageServerId.TYPESCRIPT_VTS])
def test_default_disables_type_acquisition_in_running_server(backend: LanguageServerId) -> None:
parent = psutil.Process(os.getpid())
existing_pids = {child.pid for child in parent.children(recursive=True)}
# VTS uses the same fixture project, but has no default repository alias.
with start_ls_context(backend, repo_path=str(get_repo_path(LanguageServerId.TYPESCRIPT))) as ls:
symbols = ls.request_document_symbols("index.ts").get_all_symbols_and_roots()[0]
assert any(symbol["name"] == "DemoClass" for symbol in symbols)
semantic_workers = []
typings_installers = []
for child in parent.children(recursive=True):
if child.pid in existing_pids:
continue
try:
command = child.cmdline()
except psutil.NoSuchProcess:
continue
filenames = {Path(arg).name for arg in command}
if "typingsInstaller.js" in filenames:
typings_installers.append(command)
if "tsserver.js" in filenames and "partialSemantic" not in command and "--syntaxOnly" not in command:
semantic_workers.append(command)
# The syntax worker disables ATA independently, even on the broken baseline.
assert semantic_workers, "No semantic tsserver was observed after a successful symbol request"
assert all("--disableAutomaticTypingAcquisition" in command for command in semantic_workers), semantic_workers
assert not typings_installers, typings_installers
@pytest.mark.parametrize(
("custom_settings", "expected"),
[
({}, {"typescript": {"disableAutomaticTypeAcquisition": True}}),
(
{"initialization_options": {"typescript": {"tsdk": "workspace/typescript/lib"}, "vtsls": {"autoUseWorkspaceTsdk": True}}},
{
"typescript": {"tsdk": "workspace/typescript/lib"},
"vtsls": {"autoUseWorkspaceTsdk": True},
},
),
(
{"initialization_options": {"typescript": {"disableAutomaticTypeAcquisition": False}}},
{"typescript": {"disableAutomaticTypeAcquisition": False}},
),
(
{"initializationOptions": {"typescript": {"tsdk": "workspace/typescript/lib"}}},
{"typescript": {"tsdk": "workspace/typescript/lib"}},
),
(
{"initialization_options": {"vtsls": {"autoUseWorkspaceTsdk": True}}},
{"typescript": {"disableAutomaticTypeAcquisition": True}, "vtsls": {"autoUseWorkspaceTsdk": True}},
),
(
{
"initialization_options": {
"typescript": {"disableAutomaticTypeAcquisition": False},
"vtsls": {"autoUseWorkspaceTsdk": True},
},
"initializationOptions": {"typescript": {"tsdk": "workspace/typescript/lib"}},
},
{"typescript": {"tsdk": "workspace/typescript/lib"}},
),
],
)
def test_vts_sends_consistent_effective_configuration(tmp_path: Path, custom_settings: dict, expected: dict) -> None:
original_settings = copy.deepcopy(custom_settings)
# Use the real startup/configuration path with only the remote LSP transport replaced.
server = object.__new__(VtsLanguageServer)
server._custom_settings = SolidLSPSettings.CustomLSSettings(custom_settings)
server.repository_root_path = str(tmp_path)
server.config = LanguageServerConfig(ls_id=LanguageServerId.TYPESCRIPT_VTS)
server.server_ready = threading.Event()
server.server_ready.set()
server.initialize_searcher_command_available = threading.Event()
transport = Mock()
transport.send.initialize.return_value = {"capabilities": {"textDocumentSync": 1, "completionProvider": {}}}
handlers = {}
transport.on_request.side_effect = handlers.__setitem__
server.server = transport
server._start_server()
assert transport.send.initialize.call_args.args[0]["initializationOptions"] == expected
configuration = handlers["workspace/configuration"]
assert configuration({"items": [{"section": ""}, {}, {"section": "typescript"}, {"section": "missing"}]}) == [
expected,
expected,
expected["typescript"],
{},
]
assert transport.notify.workspace_did_change_configuration.call_args.args[0] == {"settings": expected}
assert custom_settings == original_settings