Skip to content

Commit e2e7901

Browse files
committed
chore(language_server): split options for linting and formatting
1 parent 8659498 commit e2e7901

File tree

9 files changed

+267
-223
lines changed

9 files changed

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

crates/oxc_language_server/src/linter/server_linter.rs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ use tower_lsp_server::UriExt;
1818
use crate::linter::{
1919
error_with_position::DiagnosticReport,
2020
isolated_lint_handler::{IsolatedLintHandler, IsolatedLintHandlerOptions},
21+
options::{LintOptions as LSPLintOptions, Run, UnusedDisableDirectives},
2122
tsgo_linter::TsgoLinter,
2223
};
23-
use crate::options::{Run, UnusedDisableDirectives};
24-
use crate::{ConcurrentHashMap, OXC_CONFIG_FILE, Options};
24+
use crate::{ConcurrentHashMap, OXC_CONFIG_FILE};
2525

2626
use super::config_walker::ConfigWalker;
2727

@@ -80,7 +80,7 @@ impl ServerLinterDiagnostics {
8080
}
8181

8282
impl ServerLinter {
83-
pub fn new(root_uri: &Uri, options: &Options) -> Self {
83+
pub fn new(root_uri: &Uri, options: &LSPLintOptions) -> Self {
8484
let root_path = root_uri.to_file_path().unwrap();
8585
let mut nested_ignore_patterns = Vec::new();
8686
let (nested_configs, mut extended_paths) =
@@ -186,7 +186,7 @@ impl ServerLinter {
186186
/// and insert them inside the nested configuration
187187
fn create_nested_configs(
188188
root_path: &Path,
189-
options: &Options,
189+
options: &LSPLintOptions,
190190
nested_ignore_patterns: &mut Vec<(Vec<String>, PathBuf)>,
191191
) -> (ConcurrentHashMap<PathBuf, Config>, Vec<PathBuf>) {
192192
let mut extended_paths = Vec::new();
@@ -397,9 +397,10 @@ mod test {
397397
use std::path::{Path, PathBuf};
398398

399399
use crate::{
400-
Options,
401-
linter::server_linter::{ServerLinter, normalize_path},
402-
options::Run,
400+
linter::{
401+
options::{LintOptions, Run, UnusedDisableDirectives},
402+
server_linter::{ServerLinter, normalize_path},
403+
},
403404
tester::{Tester, get_file_path},
404405
};
405406
use rustc_hash::FxHashMap;
@@ -420,7 +421,7 @@ mod test {
420421
let mut nested_ignore_patterns = Vec::new();
421422
let (configs, _) = ServerLinter::create_nested_configs(
422423
Path::new("/root/"),
423-
&Options { flags, ..Options::default() },
424+
&LintOptions { flags, ..LintOptions::default() },
424425
&mut nested_ignore_patterns,
425426
);
426427

@@ -432,7 +433,7 @@ mod test {
432433
let mut nested_ignore_patterns = Vec::new();
433434
let (configs, _) = ServerLinter::create_nested_configs(
434435
&get_file_path("fixtures/linter/init_nested_configs"),
435-
&Options::default(),
436+
&LintOptions::default(),
436437
&mut nested_ignore_patterns,
437438
);
438439
let configs = configs.pin();
@@ -451,7 +452,7 @@ mod test {
451452
fn test_lint_on_run_on_type_on_type() {
452453
Tester::new(
453454
"fixtures/linter/lint_on_run/on_type",
454-
Some(Options { type_aware: true, run: Run::OnType, ..Default::default() }),
455+
Some(LintOptions { type_aware: true, run: Run::OnType, ..Default::default() }),
455456
)
456457
.test_and_snapshot_single_file_with_run_type("on-type.ts", Run::OnType);
457458
}
@@ -461,7 +462,7 @@ mod test {
461462
fn test_lint_on_run_on_type_on_save() {
462463
Tester::new(
463464
"fixtures/linter/lint_on_run/on_save",
464-
Some(Options { type_aware: true, run: Run::OnType, ..Default::default() }),
465+
Some(LintOptions { type_aware: true, run: Run::OnType, ..Default::default() }),
465466
)
466467
.test_and_snapshot_single_file_with_run_type("on-save.ts", Run::OnSave);
467468
}
@@ -471,7 +472,7 @@ mod test {
471472
fn test_lint_on_run_on_save_on_type() {
472473
Tester::new(
473474
"fixtures/linter/lint_on_run/on_save",
474-
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
475+
Some(LintOptions { type_aware: true, run: Run::OnSave, ..Default::default() }),
475476
)
476477
.test_and_snapshot_single_file_with_run_type("on-type.ts", Run::OnType);
477478
}
@@ -481,7 +482,7 @@ mod test {
481482
fn test_lint_on_run_on_save_on_save() {
482483
Tester::new(
483484
"fixtures/linter/lint_on_run/on_type",
484-
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
485+
Some(LintOptions { type_aware: true, run: Run::OnSave, ..Default::default() }),
485486
)
486487
.test_and_snapshot_single_file_with_run_type("on-save.ts", Run::OnSave);
487488
}
@@ -491,7 +492,7 @@ mod test {
491492
fn test_lint_on_run_on_type_on_save_without_type_aware() {
492493
Tester::new(
493494
"fixtures/linter/lint_on_run/on_type",
494-
Some(Options { type_aware: false, run: Run::OnType, ..Default::default() }),
495+
Some(LintOptions { type_aware: false, run: Run::OnType, ..Default::default() }),
495496
)
496497
.test_and_snapshot_single_file_with_run_type("on-save-no-type-aware.ts", Run::OnSave);
497498
}
@@ -566,23 +567,22 @@ mod test {
566567
fn test_multiple_suggestions() {
567568
Tester::new(
568569
"fixtures/linter/multiple_suggestions",
569-
Some(Options {
570+
Some(LintOptions {
570571
flags: FxHashMap::from_iter([(
571572
"fix_kind".to_string(),
572573
"safe_fix_or_suggestion".to_string(),
573574
)]),
574-
..Options::default()
575+
..Default::default()
575576
}),
576577
)
577578
.test_and_snapshot_single_file("forward_ref.ts");
578579
}
579580

580581
#[test]
581582
fn test_report_unused_directives() {
582-
use crate::options::UnusedDisableDirectives;
583583
Tester::new(
584584
"fixtures/linter/unused_disabled_directives",
585-
Some(Options {
585+
Some(LintOptions {
586586
unused_disable_directives: UnusedDisableDirectives::Deny,
587587
..Default::default()
588588
}),
@@ -601,7 +601,7 @@ mod test {
601601
fn test_ts_alias() {
602602
Tester::new(
603603
"fixtures/linter/ts_path_alias",
604-
Some(Options {
604+
Some(LintOptions {
605605
ts_config_path: Some("./deep/tsconfig.json".to_string()),
606606
..Default::default()
607607
}),
@@ -614,7 +614,7 @@ mod test {
614614
fn test_tsgo_lint() {
615615
let tester = Tester::new(
616616
"fixtures/linter/tsgolint",
617-
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
617+
Some(LintOptions { type_aware: true, run: Run::OnSave, ..Default::default() }),
618618
);
619619
tester.test_and_snapshot_single_file("no-floating-promises/index.ts");
620620
}

crates/oxc_language_server/src/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{str::FromStr, sync::Arc};
33
use futures::future::join_all;
44
use log::{debug, info, warn};
55
use rustc_hash::FxBuildHasher;
6+
use serde::Deserialize;
67
use serde_json::json;
78
use tokio::sync::{OnceCell, RwLock, SetError};
89
use tower_lsp_server::{
@@ -21,6 +22,7 @@ use tower_lsp_server::{
2122
mod capabilities;
2223
mod code_actions;
2324
mod commands;
25+
mod formatter;
2426
mod linter;
2527
mod options;
2628
#[cfg(test)]
@@ -64,7 +66,7 @@ impl LanguageServer for Backend {
6466
return Some(new_settings);
6567
}
6668

67-
let deprecated_settings = Options::try_from(value.get_mut("settings")?.take()).ok();
69+
let deprecated_settings = Options::deserialize(value.get_mut("settings")?.take()).ok();
6870

6971
// the client has deprecated settings and has a deprecated root uri.
7072
// handle all things like the old way

0 commit comments

Comments
 (0)