Skip to content

Commit 078bf0b

Browse files
committed
feat(language_server): better fallback handling when passing invalid Options values (#10930)
closes #10386
1 parent 76b6b33 commit 078bf0b

File tree

3 files changed

+187
-50
lines changed

3 files changed

+187
-50
lines changed

crates/oxc_language_server/src/main.rs

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
use futures::future::join_all;
22
use log::{debug, info, warn};
3-
use oxc_linter::FixKind;
4-
use rustc_hash::{FxBuildHasher, FxHashMap};
5-
use serde::{Deserialize, Serialize};
6-
use std::{fmt::Debug, str::FromStr};
3+
use options::{Options, Run, WorkspaceOption};
4+
use rustc_hash::FxBuildHasher;
5+
use std::str::FromStr;
76
use tokio::sync::{Mutex, OnceCell, SetError};
87
use tower_lsp_server::{
98
Client, LanguageServer, LspService, Server,
@@ -26,6 +25,7 @@ mod capabilities;
2625
mod code_actions;
2726
mod commands;
2827
mod linter;
28+
mod options;
2929
#[cfg(test)]
3030
mod tester;
3131
mod worker;
@@ -44,49 +44,6 @@ struct Backend {
4444
capabilities: OnceCell<Capabilities>,
4545
}
4646

47-
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy)]
48-
#[serde(rename_all = "camelCase")]
49-
pub enum Run {
50-
OnSave,
51-
#[default]
52-
OnType,
53-
}
54-
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
55-
#[serde(rename_all = "camelCase")]
56-
struct Options {
57-
run: Run,
58-
config_path: Option<String>,
59-
flags: FxHashMap<String, String>,
60-
}
61-
62-
#[derive(Debug, Serialize, Deserialize, Clone)]
63-
#[serde(rename_all = "camelCase")]
64-
struct WorkspaceOption {
65-
workspace_uri: Uri,
66-
options: Options,
67-
}
68-
69-
impl Options {
70-
fn use_nested_configs(&self) -> bool {
71-
!self.flags.contains_key("disable_nested_config") || self.config_path.is_some()
72-
}
73-
74-
fn fix_kind(&self) -> FixKind {
75-
self.flags.get("fix_kind").map_or(FixKind::SafeFix, |kind| match kind.as_str() {
76-
"safe_fix" => FixKind::SafeFix,
77-
"safe_fix_or_suggestion" => FixKind::SafeFixOrSuggestion,
78-
"dangerous_fix" => FixKind::DangerousFix,
79-
"dangerous_fix_or_suggestion" => FixKind::DangerousFixOrSuggestion,
80-
"none" => FixKind::None,
81-
"all" => FixKind::All,
82-
_ => {
83-
info!("invalid fix_kind flag `{kind}`, fallback to `safe_fix`");
84-
FixKind::SafeFix
85-
}
86-
})
87-
}
88-
}
89-
9047
impl LanguageServer for Backend {
9148
#[expect(deprecated)] // `params.root_uri` is deprecated, we are only falling back to it if no workspace folder is provided
9249
async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
@@ -100,8 +57,7 @@ impl LanguageServer for Backend {
10057
return Some(new_settings);
10158
}
10259

103-
let deprecated_settings =
104-
serde_json::from_value::<Options>(value.get_mut("settings")?.take()).ok();
60+
let deprecated_settings = Options::try_from(value.get_mut("settings")?.take()).ok();
10561

10662
// the client has deprecated settings and has a deprecated root uri.
10763
// handle all things like the old way
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use log::info;
2+
use oxc_linter::FixKind;
3+
use rustc_hash::{FxBuildHasher, FxHashMap};
4+
use serde::{Deserialize, Deserializer, Serialize, de::Error};
5+
use serde_json::Value;
6+
use tower_lsp_server::lsp_types::Uri;
7+
8+
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy)]
9+
#[serde(rename_all = "camelCase")]
10+
pub enum Run {
11+
OnSave,
12+
#[default]
13+
OnType,
14+
}
15+
16+
#[derive(Debug, Default, Serialize, Clone)]
17+
#[serde(rename_all = "camelCase")]
18+
pub struct Options {
19+
pub run: Run,
20+
pub config_path: Option<String>,
21+
pub flags: FxHashMap<String, String>,
22+
}
23+
24+
impl Options {
25+
pub fn use_nested_configs(&self) -> bool {
26+
!self.flags.contains_key("disable_nested_config") || self.config_path.is_some()
27+
}
28+
29+
pub fn fix_kind(&self) -> FixKind {
30+
self.flags.get("fix_kind").map_or(FixKind::SafeFix, |kind| match kind.as_str() {
31+
"safe_fix" => FixKind::SafeFix,
32+
"safe_fix_or_suggestion" => FixKind::SafeFixOrSuggestion,
33+
"dangerous_fix" => FixKind::DangerousFix,
34+
"dangerous_fix_or_suggestion" => FixKind::DangerousFixOrSuggestion,
35+
"none" => FixKind::None,
36+
"all" => FixKind::All,
37+
_ => {
38+
info!("invalid fix_kind flag `{kind}`, fallback to `safe_fix`");
39+
FixKind::SafeFix
40+
}
41+
})
42+
}
43+
}
44+
45+
impl<'de> Deserialize<'de> for Options {
46+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47+
where
48+
D: Deserializer<'de>,
49+
{
50+
let value = Value::deserialize(deserializer)?;
51+
Options::try_from(value).map_err(Error::custom)
52+
}
53+
}
54+
55+
impl TryFrom<Value> for Options {
56+
type Error = String;
57+
58+
fn try_from(value: Value) -> Result<Self, Self::Error> {
59+
let Some(object) = value.as_object() else {
60+
return Err("no object passed".to_string());
61+
};
62+
63+
let mut flags = FxHashMap::with_capacity_and_hasher(2, FxBuildHasher);
64+
if let Some(json_flags) = object.get("flags").and_then(|value| value.as_object()) {
65+
if let Some(disable_nested_config) =
66+
json_flags.get("disable_nested_config").and_then(|value| value.as_str())
67+
{
68+
flags
69+
.insert("disable_nested_config".to_string(), disable_nested_config.to_string());
70+
}
71+
72+
if let Some(fix_kind) = json_flags.get("fix_kind").and_then(|value| value.as_str()) {
73+
flags.insert("fix_kind".to_string(), fix_kind.to_string());
74+
}
75+
}
76+
77+
Ok(Self {
78+
run: object
79+
.get("run")
80+
.map(|run| serde_json::from_value::<Run>(run.clone()).unwrap_or_default())
81+
.unwrap_or_default(),
82+
config_path: object
83+
.get("configPath")
84+
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
85+
flags,
86+
})
87+
}
88+
}
89+
90+
#[derive(Debug, Serialize, Deserialize, Clone)]
91+
#[serde(rename_all = "camelCase")]
92+
pub struct WorkspaceOption {
93+
pub workspace_uri: Uri,
94+
pub options: Options,
95+
}
96+
97+
#[cfg(test)]
98+
mod test {
99+
use serde_json::json;
100+
101+
use super::{Options, Run, WorkspaceOption};
102+
103+
#[test]
104+
fn test_valid_options_json() {
105+
let json = json!({
106+
"run": "onSave",
107+
"configPath": "./custom.json",
108+
"flags": {
109+
"disable_nested_config": "true",
110+
"fix_kind": "dangerous_fix"
111+
}
112+
});
113+
114+
let options = Options::try_from(json).unwrap();
115+
assert_eq!(options.run, Run::OnSave);
116+
assert_eq!(options.config_path, Some("./custom.json".into()));
117+
assert_eq!(options.flags.get("disable_nested_config"), Some(&"true".to_string()));
118+
assert_eq!(options.flags.get("fix_kind"), Some(&"dangerous_fix".to_string()));
119+
}
120+
121+
#[test]
122+
fn test_empty_options_json() {
123+
let json = json!({});
124+
125+
let options = Options::try_from(json).unwrap();
126+
assert_eq!(options.run, Run::OnType);
127+
assert_eq!(options.config_path, None);
128+
assert!(options.flags.is_empty());
129+
}
130+
131+
#[test]
132+
fn test_invalid_options_json() {
133+
let json = json!({
134+
"run": true,
135+
"configPath": "./custom.json"
136+
});
137+
138+
let options = Options::try_from(json).unwrap();
139+
assert_eq!(options.run, Run::OnType); // fallback
140+
assert_eq!(options.config_path, Some("./custom.json".into()));
141+
assert!(options.flags.is_empty());
142+
}
143+
144+
#[test]
145+
fn test_invalid_flags_options_json() {
146+
let json = json!({
147+
"configPath": "./custom.json",
148+
"flags": {
149+
"disable_nested_config": true, // should be string
150+
"fix_kind": "dangerous_fix"
151+
}
152+
});
153+
154+
let options = Options::try_from(json).unwrap();
155+
assert_eq!(options.run, Run::OnType); // fallback
156+
assert_eq!(options.config_path, Some("./custom.json".into()));
157+
assert_eq!(options.flags.get("disable_nested_config"), None);
158+
assert_eq!(options.flags.get("fix_kind"), Some(&"dangerous_fix".to_string()));
159+
}
160+
161+
#[test]
162+
fn test_invalid_workspace_options_json() {
163+
let json = json!([{
164+
"workspaceUri": "file:///root/",
165+
"options": {
166+
"run": true,
167+
"configPath": "./custom.json"
168+
}
169+
}]);
170+
171+
let workspace = serde_json::from_value::<Vec<WorkspaceOption>>(json).unwrap();
172+
173+
assert_eq!(workspace.len(), 1);
174+
assert_eq!(workspace[0].workspace_uri.path().as_str(), "/root/");
175+
176+
let options = &workspace[0].options;
177+
assert_eq!(options.run, Run::OnType); // fallback
178+
assert_eq!(options.config_path, Some("./custom.json".into()));
179+
assert!(options.flags.is_empty());
180+
}
181+
}

editors/vscode/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ Following configuration are supported via `settings.json` and can be changed for
5151
## Testing
5252

5353
Run `pnpm server:build:debug` to build the language server.
54-
After that, you can test the vscode plugin + E2E Tests with `pnm test`.
54+
After that, you can test the vscode plugin + E2E Tests with `pnpm test`.

0 commit comments

Comments
 (0)