[ty] Warn users if server received unknown options (#19779)

## Summary

This PR updates the client settings handling to recognize unknown
options provided by the user and show a warning popup along with a
warning log message.

## Test Plan

Add E2E tests.
This commit is contained in:
Dhruv Manilawala 2025-08-06 18:41:13 +05:30 committed by GitHub
parent 1f29a04e9a
commit fa711fa40f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 90 additions and 2 deletions

View file

@ -98,6 +98,15 @@ impl Server {
let (main_loop_sender, main_loop_receiver) = crossbeam::channel::bounded(32); let (main_loop_sender, main_loop_receiver) = crossbeam::channel::bounded(32);
let client = Client::new(main_loop_sender.clone(), connection.sender.clone()); let client = Client::new(main_loop_sender.clone(), connection.sender.clone());
if !initialization_options.options.unknown.is_empty() {
let options = serde_json::to_string_pretty(&initialization_options.options.unknown)
.unwrap_or_else(|_| "<invalid JSON>".to_string());
tracing::warn!("Received unknown options during initialization: {options}");
client.show_warning_message(format_args!(
"Received unknown options during initialization: {options}"
));
}
// Get workspace URLs without settings - settings will come from workspace/configuration // Get workspace URLs without settings - settings will come from workspace/configuration
let workspace_urls = workspace_folders let workspace_urls = workspace_folders
.filter(|folders| !folders.is_empty()) .filter(|folders| !folders.is_empty())

View file

@ -450,12 +450,23 @@ impl Session {
// Combine the global options specified during initialization with the // Combine the global options specified during initialization with the
// workspace-specific options to create the final workspace options. // workspace-specific options to create the final workspace options.
let ClientOptions { global, workspace } = self let ClientOptions {
global, workspace, ..
} = self
.initialization_options .initialization_options
.options .options
.clone() .clone()
.combine(options.clone()); .combine(options.clone());
if !options.unknown.is_empty() {
let options = serde_json::to_string_pretty(&options.unknown)
.unwrap_or_else(|_| "<invalid JSON>".to_string());
tracing::warn!("Received unknown options for workspace `{url}`: {options}");
client.show_warning_message(format!(
"Received unknown options for workspace `{url}`: {options}",
));
}
combined_global_options.combine_with(Some(global)); combined_global_options.combine_with(Some(global));
let workspace_settings = workspace.into_settings(); let workspace_settings = workspace.into_settings();

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use lsp_types::Url; use lsp_types::Url;
use ruff_db::system::SystemPathBuf; use ruff_db::system::SystemPathBuf;
use ruff_macros::Combine; use ruff_macros::Combine;
@ -84,6 +86,11 @@ pub struct ClientOptions {
#[serde(flatten)] #[serde(flatten)]
pub(crate) workspace: WorkspaceOptions, pub(crate) workspace: WorkspaceOptions,
/// Additional options that aren't valid as per the schema but we accept it to provide better
/// error message to the user.
#[serde(flatten)]
pub(crate) unknown: HashMap<String, Value>,
} }
impl ClientOptions { impl ClientOptions {
@ -98,6 +105,12 @@ impl ClientOptions {
self.workspace.disable_language_services = Some(disable_language_services); self.workspace.disable_language_services = Some(disable_language_services);
self self
} }
#[must_use]
pub fn with_unknown(mut self, unknown: HashMap<String, Value>) -> Self {
self.unknown = unknown;
self
}
} }
/// Options that are global to the language server. /// Options that are global to the language server.

View file

@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use lsp_types::{Position, request::RegisterCapability}; use lsp_types::{Position, notification::ShowMessage, request::RegisterCapability};
use ruff_db::system::SystemPath; use ruff_db::system::SystemPath;
use serde_json::Value;
use ty_server::{ClientOptions, DiagnosticMode}; use ty_server::{ClientOptions, DiagnosticMode};
use crate::TestServerBuilder; use crate::TestServerBuilder;
@ -382,3 +383,57 @@ def bar() -> str:
Ok(()) Ok(())
} }
/// Tests that the server sends a warning notification if user provided unknown options during
/// initialization.
#[test]
fn unknown_initialization_options() -> Result<()> {
let workspace_root = SystemPath::new("foo");
let mut server = TestServerBuilder::new()?
.with_workspace(workspace_root, None)?
.with_initialization_options(
ClientOptions::default()
.with_unknown([("foo".to_string(), Value::String("bar".to_string()))].into()),
)
.build()?
.wait_until_workspaces_are_initialized()?;
let show_message_params = server.await_notification::<ShowMessage>()?;
insta::assert_json_snapshot!(show_message_params, @r#"
{
"type": 2,
"message": "Received unknown options during initialization: {\n /"foo/": /"bar/"\n}"
}
"#);
Ok(())
}
/// Tests that the server sends a warning notification if user provided unknown options in the
/// workspace configuration.
#[test]
fn unknown_options_in_workspace_configuration() -> Result<()> {
let workspace_root = SystemPath::new("foo");
let mut server = TestServerBuilder::new()?
.with_workspace(
workspace_root,
Some(
ClientOptions::default()
.with_unknown([("foo".to_string(), Value::String("bar".to_string()))].into()),
),
)?
.build()?
.wait_until_workspaces_are_initialized()?;
let show_message_params = server.await_notification::<ShowMessage>()?;
insta::assert_json_snapshot!(show_message_params, @r#"
{
"type": 2,
"message": "Received unknown options for workspace `file://<temp_dir>/foo`: {\n /"foo/": /"bar/"\n}"
}
"#);
Ok(())
}