diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index 8d575531db9b0..37f79452981b2 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -1,9 +1,8 @@ use futures::future::join_all; use log::{debug, info, warn}; -use oxc_linter::FixKind; -use rustc_hash::{FxBuildHasher, FxHashMap}; -use serde::{Deserialize, Serialize}; -use std::{fmt::Debug, str::FromStr}; +use options::{Options, Run, WorkspaceOption}; +use rustc_hash::FxBuildHasher; +use std::str::FromStr; use tokio::sync::{Mutex, OnceCell, SetError}; use tower_lsp_server::{ Client, LanguageServer, LspService, Server, @@ -26,6 +25,7 @@ mod capabilities; mod code_actions; mod commands; mod linter; +mod options; #[cfg(test)] mod tester; mod worker; @@ -44,49 +44,6 @@ struct Backend { capabilities: OnceCell, } -#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy)] -#[serde(rename_all = "camelCase")] -pub enum Run { - OnSave, - #[default] - OnType, -} -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -struct Options { - run: Run, - config_path: Option, - flags: FxHashMap, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -struct WorkspaceOption { - workspace_uri: Uri, - options: Options, -} - -impl Options { - fn use_nested_configs(&self) -> bool { - !self.flags.contains_key("disable_nested_config") || self.config_path.is_some() - } - - fn fix_kind(&self) -> FixKind { - self.flags.get("fix_kind").map_or(FixKind::SafeFix, |kind| match kind.as_str() { - "safe_fix" => FixKind::SafeFix, - "safe_fix_or_suggestion" => FixKind::SafeFixOrSuggestion, - "dangerous_fix" => FixKind::DangerousFix, - "dangerous_fix_or_suggestion" => FixKind::DangerousFixOrSuggestion, - "none" => FixKind::None, - "all" => FixKind::All, - _ => { - info!("invalid fix_kind flag `{kind}`, fallback to `safe_fix`"); - FixKind::SafeFix - } - }) - } -} - impl LanguageServer for Backend { #[expect(deprecated)] // `params.root_uri` is deprecated, we are only falling back to it if no workspace folder is provided async fn initialize(&self, params: InitializeParams) -> Result { @@ -100,8 +57,7 @@ impl LanguageServer for Backend { return Some(new_settings); } - let deprecated_settings = - serde_json::from_value::(value.get_mut("settings")?.take()).ok(); + let deprecated_settings = Options::try_from(value.get_mut("settings")?.take()).ok(); // the client has deprecated settings and has a deprecated root uri. // handle all things like the old way diff --git a/crates/oxc_language_server/src/options.rs b/crates/oxc_language_server/src/options.rs new file mode 100644 index 0000000000000..eeedce6266a1c --- /dev/null +++ b/crates/oxc_language_server/src/options.rs @@ -0,0 +1,181 @@ +use log::info; +use oxc_linter::FixKind; +use rustc_hash::{FxBuildHasher, FxHashMap}; +use serde::{Deserialize, Deserializer, Serialize, de::Error}; +use serde_json::Value; +use tower_lsp_server::lsp_types::Uri; + +#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub enum Run { + OnSave, + #[default] + OnType, +} + +#[derive(Debug, Default, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Options { + pub run: Run, + pub config_path: Option, + pub flags: FxHashMap, +} + +impl Options { + pub fn use_nested_configs(&self) -> bool { + !self.flags.contains_key("disable_nested_config") || self.config_path.is_some() + } + + pub fn fix_kind(&self) -> FixKind { + self.flags.get("fix_kind").map_or(FixKind::SafeFix, |kind| match kind.as_str() { + "safe_fix" => FixKind::SafeFix, + "safe_fix_or_suggestion" => FixKind::SafeFixOrSuggestion, + "dangerous_fix" => FixKind::DangerousFix, + "dangerous_fix_or_suggestion" => FixKind::DangerousFixOrSuggestion, + "none" => FixKind::None, + "all" => FixKind::All, + _ => { + info!("invalid fix_kind flag `{kind}`, fallback to `safe_fix`"); + FixKind::SafeFix + } + }) + } +} + +impl<'de> Deserialize<'de> for Options { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + Options::try_from(value).map_err(Error::custom) + } +} + +impl TryFrom for Options { + type Error = String; + + fn try_from(value: Value) -> Result { + let Some(object) = value.as_object() else { + return Err("no object passed".to_string()); + }; + + let mut flags = FxHashMap::with_capacity_and_hasher(2, FxBuildHasher); + if let Some(json_flags) = object.get("flags").and_then(|value| value.as_object()) { + if let Some(disable_nested_config) = + json_flags.get("disable_nested_config").and_then(|value| value.as_str()) + { + flags + .insert("disable_nested_config".to_string(), disable_nested_config.to_string()); + } + + if let Some(fix_kind) = json_flags.get("fix_kind").and_then(|value| value.as_str()) { + flags.insert("fix_kind".to_string(), fix_kind.to_string()); + } + } + + Ok(Self { + run: object + .get("run") + .map(|run| serde_json::from_value::(run.clone()).unwrap_or_default()) + .unwrap_or_default(), + config_path: object + .get("configPath") + .and_then(|config_path| serde_json::from_value::(config_path.clone()).ok()), + flags, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WorkspaceOption { + pub workspace_uri: Uri, + pub options: Options, +} + +#[cfg(test)] +mod test { + use serde_json::json; + + use super::{Options, Run, WorkspaceOption}; + + #[test] + fn test_valid_options_json() { + let json = json!({ + "run": "onSave", + "configPath": "./custom.json", + "flags": { + "disable_nested_config": "true", + "fix_kind": "dangerous_fix" + } + }); + + let options = Options::try_from(json).unwrap(); + assert_eq!(options.run, Run::OnSave); + assert_eq!(options.config_path, Some("./custom.json".into())); + assert_eq!(options.flags.get("disable_nested_config"), Some(&"true".to_string())); + assert_eq!(options.flags.get("fix_kind"), Some(&"dangerous_fix".to_string())); + } + + #[test] + fn test_empty_options_json() { + let json = json!({}); + + let options = Options::try_from(json).unwrap(); + assert_eq!(options.run, Run::OnType); + assert_eq!(options.config_path, None); + assert!(options.flags.is_empty()); + } + + #[test] + fn test_invalid_options_json() { + let json = json!({ + "run": true, + "configPath": "./custom.json" + }); + + let options = Options::try_from(json).unwrap(); + assert_eq!(options.run, Run::OnType); // fallback + assert_eq!(options.config_path, Some("./custom.json".into())); + assert!(options.flags.is_empty()); + } + + #[test] + fn test_invalid_flags_options_json() { + let json = json!({ + "configPath": "./custom.json", + "flags": { + "disable_nested_config": true, // should be string + "fix_kind": "dangerous_fix" + } + }); + + let options = Options::try_from(json).unwrap(); + assert_eq!(options.run, Run::OnType); // fallback + assert_eq!(options.config_path, Some("./custom.json".into())); + assert_eq!(options.flags.get("disable_nested_config"), None); + assert_eq!(options.flags.get("fix_kind"), Some(&"dangerous_fix".to_string())); + } + + #[test] + fn test_invalid_workspace_options_json() { + let json = json!([{ + "workspaceUri": "file:///root/", + "options": { + "run": true, + "configPath": "./custom.json" + } + }]); + + let workspace = serde_json::from_value::>(json).unwrap(); + + assert_eq!(workspace.len(), 1); + assert_eq!(workspace[0].workspace_uri.path().as_str(), "/root/"); + + let options = &workspace[0].options; + assert_eq!(options.run, Run::OnType); // fallback + assert_eq!(options.config_path, Some("./custom.json".into())); + assert!(options.flags.is_empty()); + } +} diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 3817e3f385b3c..2c7de2c57dacb 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -51,4 +51,4 @@ Following configuration are supported via `settings.json` and can be changed for ## Testing Run `pnpm server:build:debug` to build the language server. -After that, you can test the vscode plugin + E2E Tests with `pnm test`. +After that, you can test the vscode plugin + E2E Tests with `pnpm test`.