Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/oxc_language_server/src/formatter/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod options;
5 changes: 5 additions & 0 deletions crates/oxc_language_server/src/formatter/options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct FormatOptions;
1 change: 1 addition & 0 deletions crates/oxc_language_server/src/linter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod config_walker;
pub mod error_with_position;
pub mod isolated_lint_handler;
pub mod options;
pub mod server_linter;
pub mod tsgo_linter;
201 changes: 201 additions & 0 deletions crates/oxc_language_server/src/linter/options.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use log::info;
use rustc_hash::{FxBuildHasher, FxHashMap};
use serde::{Deserialize, Deserializer, Serialize, de::Error};
use serde_json::Value;

use oxc_linter::FixKind;

#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "camelCase")]
pub enum UnusedDisableDirectives {
#[default]
Allow,
Warn,
Deny,
}

#[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 LintOptions {
pub run: Run, // TODO: the client wants maybe only the formatter, make it optional
pub config_path: Option<String>,
pub ts_config_path: Option<String>,
pub unused_disable_directives: UnusedDisableDirectives,
pub type_aware: bool,
pub flags: FxHashMap<String, String>,
}

impl LintOptions {
pub fn use_nested_configs(&self) -> bool {
!self.flags.contains_key("disable_nested_config") && self.config_path.is_none()
}

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 LintOptions {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = Value::deserialize(deserializer)?;
LintOptions::try_from(value).map_err(Error::custom)
}
}

impl TryFrom<Value> for LintOptions {
type Error = String;

fn try_from(value: Value) -> Result<Self, Self::Error> {
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>(run.clone()).unwrap_or_default())
.unwrap_or_default(),
unused_disable_directives: object
.get("unusedDisableDirectives")
.map(|key| {
serde_json::from_value::<UnusedDisableDirectives>(key.clone())
.unwrap_or_default()
})
.unwrap_or_default(),
config_path: object
.get("configPath")
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
ts_config_path: object
.get("tsConfigPath")
.and_then(|config_path| serde_json::from_value::<String>(config_path.clone()).ok()),
type_aware: object
.get("typeAware")
.is_some_and(|key| serde_json::from_value::<bool>(key.clone()).unwrap_or_default()),
flags,
})
}
}

#[cfg(test)]
mod test {
use rustc_hash::FxHashMap;
use serde_json::json;

use super::{LintOptions, Run, UnusedDisableDirectives};

#[test]
fn test_valid_options_json() {
let json = json!({
"run": "onSave",
"configPath": "./custom.json",
"unusedDisableDirectives": "warn",
"typeAware": true,
"flags": {
"disable_nested_config": "true",
"fix_kind": "dangerous_fix"
}
});

let options = LintOptions::try_from(json).unwrap();
assert_eq!(options.run, Run::OnSave);
assert_eq!(options.config_path, Some("./custom.json".into()));
assert_eq!(options.unused_disable_directives, UnusedDisableDirectives::Warn);
assert!(options.type_aware);
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 = LintOptions::try_from(json).unwrap();
assert_eq!(options.run, Run::OnType);
assert_eq!(options.config_path, None);
assert_eq!(options.unused_disable_directives, UnusedDisableDirectives::Allow);
assert!(!options.type_aware);
assert!(options.flags.is_empty());
}

#[test]
fn test_invalid_options_json() {
let json = json!({
"run": true,
"configPath": "./custom.json"
});

let options = LintOptions::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 = LintOptions::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_use_nested_configs() {
let options = LintOptions::default();
assert!(options.use_nested_configs());

let options =
LintOptions { config_path: Some("config.json".to_string()), ..Default::default() };
assert!(!options.use_nested_configs());

let mut flags = FxHashMap::default();
flags.insert("disable_nested_config".to_string(), "true".to_string());

let options = LintOptions { flags, ..Default::default() };
assert!(!options.use_nested_configs());
}
}
40 changes: 20 additions & 20 deletions crates/oxc_language_server/src/linter/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ use tower_lsp_server::UriExt;
use crate::linter::{
error_with_position::DiagnosticReport,
isolated_lint_handler::{IsolatedLintHandler, IsolatedLintHandlerOptions},
options::{LintOptions as LSPLintOptions, Run, UnusedDisableDirectives},
tsgo_linter::TsgoLinter,
};
use crate::options::{Run, UnusedDisableDirectives};
use crate::{ConcurrentHashMap, OXC_CONFIG_FILE, Options};
use crate::{ConcurrentHashMap, OXC_CONFIG_FILE};

use super::config_walker::ConfigWalker;

Expand Down Expand Up @@ -80,7 +80,7 @@ impl ServerLinterDiagnostics {
}

impl ServerLinter {
pub fn new(root_uri: &Uri, options: &Options) -> Self {
pub fn new(root_uri: &Uri, options: &LSPLintOptions) -> Self {
let root_path = root_uri.to_file_path().unwrap();
let mut nested_ignore_patterns = Vec::new();
let (nested_configs, mut extended_paths) =
Expand Down Expand Up @@ -186,7 +186,7 @@ impl ServerLinter {
/// and insert them inside the nested configuration
fn create_nested_configs(
root_path: &Path,
options: &Options,
options: &LSPLintOptions,
nested_ignore_patterns: &mut Vec<(Vec<String>, PathBuf)>,
) -> (ConcurrentHashMap<PathBuf, Config>, Vec<PathBuf>) {
let mut extended_paths = Vec::new();
Expand Down Expand Up @@ -397,9 +397,10 @@ mod test {
use std::path::{Path, PathBuf};

use crate::{
Options,
linter::server_linter::{ServerLinter, normalize_path},
options::Run,
linter::{
options::{LintOptions, Run, UnusedDisableDirectives},
server_linter::{ServerLinter, normalize_path},
},
tester::{Tester, get_file_path},
};
use rustc_hash::FxHashMap;
Expand All @@ -420,7 +421,7 @@ mod test {
let mut nested_ignore_patterns = Vec::new();
let (configs, _) = ServerLinter::create_nested_configs(
Path::new("/root/"),
&Options { flags, ..Options::default() },
&LintOptions { flags, ..LintOptions::default() },
&mut nested_ignore_patterns,
);

Expand All @@ -432,7 +433,7 @@ mod test {
let mut nested_ignore_patterns = Vec::new();
let (configs, _) = ServerLinter::create_nested_configs(
&get_file_path("fixtures/linter/init_nested_configs"),
&Options::default(),
&LintOptions::default(),
&mut nested_ignore_patterns,
);
let configs = configs.pin();
Expand All @@ -451,7 +452,7 @@ mod test {
fn test_lint_on_run_on_type_on_type() {
Tester::new(
"fixtures/linter/lint_on_run/on_type",
Some(Options { type_aware: true, run: Run::OnType, ..Default::default() }),
Some(LintOptions { type_aware: true, run: Run::OnType, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-type.ts", Run::OnType);
}
Expand All @@ -461,7 +462,7 @@ mod test {
fn test_lint_on_run_on_type_on_save() {
Tester::new(
"fixtures/linter/lint_on_run/on_save",
Some(Options { type_aware: true, run: Run::OnType, ..Default::default() }),
Some(LintOptions { type_aware: true, run: Run::OnType, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-save.ts", Run::OnSave);
}
Expand All @@ -471,7 +472,7 @@ mod test {
fn test_lint_on_run_on_save_on_type() {
Tester::new(
"fixtures/linter/lint_on_run/on_save",
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
Some(LintOptions { type_aware: true, run: Run::OnSave, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-type.ts", Run::OnType);
}
Expand All @@ -481,7 +482,7 @@ mod test {
fn test_lint_on_run_on_save_on_save() {
Tester::new(
"fixtures/linter/lint_on_run/on_type",
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
Some(LintOptions { type_aware: true, run: Run::OnSave, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-save.ts", Run::OnSave);
}
Expand All @@ -491,7 +492,7 @@ mod test {
fn test_lint_on_run_on_type_on_save_without_type_aware() {
Tester::new(
"fixtures/linter/lint_on_run/on_type",
Some(Options { type_aware: false, run: Run::OnType, ..Default::default() }),
Some(LintOptions { type_aware: false, run: Run::OnType, ..Default::default() }),
)
.test_and_snapshot_single_file_with_run_type("on-save-no-type-aware.ts", Run::OnSave);
}
Expand Down Expand Up @@ -566,23 +567,22 @@ mod test {
fn test_multiple_suggestions() {
Tester::new(
"fixtures/linter/multiple_suggestions",
Some(Options {
Some(LintOptions {
flags: FxHashMap::from_iter([(
"fix_kind".to_string(),
"safe_fix_or_suggestion".to_string(),
)]),
..Options::default()
..Default::default()
}),
)
.test_and_snapshot_single_file("forward_ref.ts");
}

#[test]
fn test_report_unused_directives() {
use crate::options::UnusedDisableDirectives;
Tester::new(
"fixtures/linter/unused_disabled_directives",
Some(Options {
Some(LintOptions {
unused_disable_directives: UnusedDisableDirectives::Deny,
..Default::default()
}),
Expand All @@ -601,7 +601,7 @@ mod test {
fn test_ts_alias() {
Tester::new(
"fixtures/linter/ts_path_alias",
Some(Options {
Some(LintOptions {
ts_config_path: Some("./deep/tsconfig.json".to_string()),
..Default::default()
}),
Expand All @@ -614,7 +614,7 @@ mod test {
fn test_tsgo_lint() {
let tester = Tester::new(
"fixtures/linter/tsgolint",
Some(Options { type_aware: true, run: Run::OnSave, ..Default::default() }),
Some(LintOptions { type_aware: true, run: Run::OnSave, ..Default::default() }),
);
tester.test_and_snapshot_single_file("no-floating-promises/index.ts");
}
Expand Down
4 changes: 3 additions & 1 deletion crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{str::FromStr, sync::Arc};
use futures::future::join_all;
use log::{debug, info, warn};
use rustc_hash::FxBuildHasher;
use serde::Deserialize;
use serde_json::json;
use tokio::sync::{OnceCell, RwLock, SetError};
use tower_lsp_server::{
Expand All @@ -21,6 +22,7 @@ use tower_lsp_server::{
mod capabilities;
mod code_actions;
mod commands;
mod formatter;
mod linter;
mod options;
#[cfg(test)]
Expand Down Expand Up @@ -64,7 +66,7 @@ impl LanguageServer for Backend {
return Some(new_settings);
}

let deprecated_settings = Options::try_from(value.get_mut("settings")?.take()).ok();
let deprecated_settings = Options::deserialize(value.get_mut("settings")?.take()).ok();

// the client has deprecated settings and has a deprecated root uri.
// handle all things like the old way
Expand Down
Loading
Loading