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
133 changes: 65 additions & 68 deletions crates/oxc_language_server/src/linter/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,73 @@ pub struct ServerLinter {
}

impl ServerLinter {
pub fn new(root_uri: &Uri, options: &Options) -> Self {
let nested_configs = Self::create_nested_configs(root_uri, options);
let root_path = root_uri.to_file_path().unwrap();
let relative_config_path = options.config_path.clone();
let oxlintrc = if relative_config_path.is_some() {
let config = root_path.join(relative_config_path.unwrap());
if config.try_exists().is_ok_and(|exists| exists) {
if let Ok(oxlintrc) = Oxlintrc::from_file(&config) {
oxlintrc
} else {
warn!("Failed to initialize oxlintrc config: {}", config.to_string_lossy());
Oxlintrc::default()
}
} else {
warn!(
"Config file not found: {}, fallback to default config",
config.to_string_lossy()
);
Oxlintrc::default()
}
} else {
Oxlintrc::default()
};

// clone because we are returning it for ignore builder
let config_builder =
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc.clone()).unwrap_or_default();

// TODO(refactor): pull this into a shared function, because in oxlint we have the same functionality.
let use_nested_config = options.use_nested_configs();

let use_cross_module = if use_nested_config {
nested_configs.pin().values().any(|config| config.plugins().has_import())
} else {
config_builder.plugins().has_import()
};

let config_store = config_builder.build().expect("Failed to build config store");

let lint_options = LintOptions { fix: options.fix_kind(), ..Default::default() };

let linter = if use_nested_config {
let nested_configs = nested_configs.pin();
let nested_configs_copy: FxHashMap<PathBuf, ConfigStore> = nested_configs
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<FxHashMap<_, _>>();

Linter::new_with_nested_configs(lint_options, config_store, nested_configs_copy)
} else {
Linter::new(lint_options, config_store)
};

let isolated_linter = IsolatedLintHandler::new(
linter,
IsolatedLintHandlerOptions { use_cross_module, root_path: root_path.to_path_buf() },
);

Self {
isolated_linter: Arc::new(isolated_linter),
gitignore_glob: Self::create_ignore_glob(root_uri, &oxlintrc),
}
}

/// Searches inside root_uri recursively for the default oxlint config files
/// and insert them inside the nested configuration
pub fn create_nested_configs(
fn create_nested_configs(
root_uri: &Uri,
options: &Options,
) -> ConcurrentHashMap<PathBuf, ConfigStore> {
Expand Down Expand Up @@ -115,73 +179,6 @@ impl ServerLinter {
gitignore_globs
}

pub fn create_server_linter(
root_uri: &Uri,
options: &Options,
nested_configs: &ConcurrentHashMap<PathBuf, ConfigStore>,
) -> Self {
let root_path = root_uri.to_file_path().unwrap();
let relative_config_path = options.config_path.clone();
let oxlintrc = if relative_config_path.is_some() {
let config = root_path.join(relative_config_path.unwrap());
if config.try_exists().expect("Could not get fs metadata for config") {
if let Ok(oxlintrc) = Oxlintrc::from_file(&config) {
oxlintrc
} else {
warn!("Failed to initialize oxlintrc config: {}", config.to_string_lossy());
Oxlintrc::default()
}
} else {
warn!(
"Config file not found: {}, fallback to default config",
config.to_string_lossy()
);
Oxlintrc::default()
}
} else {
Oxlintrc::default()
};

// clone because we are returning it for ignore builder
let config_builder =
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc.clone()).unwrap_or_default();

// TODO(refactor): pull this into a shared function, because in oxlint we have the same functionality.
let use_nested_config = options.use_nested_configs();

let use_cross_module = if use_nested_config {
nested_configs.pin().values().any(|config| config.plugins().has_import())
} else {
config_builder.plugins().has_import()
};

let config_store = config_builder.build().expect("Failed to build config store");

let lint_options = LintOptions { fix: options.fix_kind(), ..Default::default() };

let linter = if use_nested_config {
let nested_configs = nested_configs.pin();
let nested_configs_copy: FxHashMap<PathBuf, ConfigStore> = nested_configs
.iter()
.map(|(key, value)| (key.clone(), value.clone()))
.collect::<FxHashMap<_, _>>();

Linter::new_with_nested_configs(lint_options, config_store, nested_configs_copy)
} else {
Linter::new(lint_options, config_store)
};

let isolated_linter = IsolatedLintHandler::new(
linter,
IsolatedLintHandlerOptions { use_cross_module, root_path: root_path.to_path_buf() },
);

Self {
isolated_linter: Arc::new(isolated_linter),
gitignore_glob: Self::create_ignore_glob(root_uri, &oxlintrc),
}
}

fn is_ignored(&self, uri: &Uri) -> bool {
for gitignore in &self.gitignore_glob {
if let Some(uri_path) = uri.to_file_path() {
Expand Down
75 changes: 7 additions & 68 deletions crates/oxc_language_server/src/worker.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
use std::{path::PathBuf, str::FromStr, vec};
use std::{str::FromStr, vec};

use log::{debug, info};
use oxc_linter::{ConfigStore, ConfigStoreBuilder, Oxlintrc};
use log::debug;
use rustc_hash::FxBuildHasher;
use tokio::sync::{Mutex, OnceCell, RwLock};
use tower_lsp_server::{
UriExt,
lsp_types::{CodeActionOrCommand, Diagnostic, FileChangeType, FileEvent, Range, TextEdit, Uri},
lsp_types::{CodeActionOrCommand, Diagnostic, FileEvent, Range, TextEdit, Uri},
};

use crate::{
ConcurrentHashMap, OXC_CONFIG_FILE, Options, Run,
ConcurrentHashMap, Options, Run,
code_actions::{
apply_all_fix_code_action, apply_fix_code_action, ignore_this_line_code_action,
ignore_this_rule_code_action,
Expand All @@ -23,22 +22,19 @@ pub struct WorkspaceWorker {
server_linter: RwLock<ServerLinter>,
diagnostics_report_map: RwLock<ConcurrentHashMap<String, Vec<DiagnosticReport>>>,
options: Mutex<Options>,
nested_configs: RwLock<ConcurrentHashMap<PathBuf, ConfigStore>>,
}

impl WorkspaceWorker {
pub fn new(root_uri: &Uri, options: Options) -> Self {
let root_uri_cell = OnceCell::new();
root_uri_cell.set(root_uri.clone()).unwrap();

let nested_configs = ServerLinter::create_nested_configs(root_uri, &options);
let server_linter = ServerLinter::create_server_linter(root_uri, &options, &nested_configs);
let server_linter = ServerLinter::new(root_uri, &options);
Self {
root_uri: root_uri_cell,
server_linter: RwLock::new(server_linter),
diagnostics_report_map: RwLock::new(ConcurrentHashMap::default()),
options: Mutex::new(options),
nested_configs: RwLock::const_new(nested_configs),
}
}

Expand All @@ -59,22 +55,9 @@ impl WorkspaceWorker {
self.diagnostics_report_map.read().await.pin().remove(&uri.to_string());
}

async fn refresh_nested_configs(&self) {
let options = self.options.lock().await;
let nested_configs =
ServerLinter::create_nested_configs(self.root_uri.get().unwrap(), &options);

*self.nested_configs.write().await = nested_configs;
}

async fn refresh_server_linter(&self) {
let options = self.options.lock().await;
let nested_configs = self.nested_configs.read().await;
let server_linter = ServerLinter::create_server_linter(
self.root_uri.get().unwrap(),
&options,
&nested_configs,
);
let server_linter = ServerLinter::new(self.root_uri.get().unwrap(), &options);

*self.server_linter.write().await = server_linter;
}
Expand Down Expand Up @@ -224,48 +207,8 @@ impl WorkspaceWorker {

pub async fn did_change_watched_files(
&self,
file_event: &FileEvent,
_file_event: &FileEvent,
) -> Option<ConcurrentHashMap<String, Vec<DiagnosticReport>>> {
if self.options.lock().await.use_nested_configs() {
let nested_configs = self.nested_configs.read().await;
let nested_configs = nested_configs.pin();
let Some(file_path) = file_event.uri.to_file_path() else {
info!("Unable to convert {:?} to a file path", file_event.uri);
return None;
};
let Some(file_name) = file_path.file_name() else {
info!("Unable to retrieve file name from {file_path:?}");
return None;
};

if file_name != OXC_CONFIG_FILE {
return None;
}

let Some(dir_path) = file_path.parent() else {
info!("Unable to retrieve parent from {file_path:?}");
return None;
};

// spellchecker:off -- "typ" is accurate
if file_event.typ == FileChangeType::CREATED
|| file_event.typ == FileChangeType::CHANGED
{
// spellchecker:on
let oxlintrc =
Oxlintrc::from_file(&file_path).expect("Failed to parse config file");
let config_store_builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc)
.expect("Failed to create config store builder");
let config_store =
config_store_builder.build().expect("Failed to build config store");
nested_configs.insert(dir_path.to_path_buf(), config_store);
// spellchecker:off -- "typ" is accurate
} else if file_event.typ == FileChangeType::DELETED {
// spellchecker:on
nested_configs.remove(&dir_path.to_path_buf());
}
}

self.refresh_server_linter().await;
Some(self.revalidate_diagnostics().await)
}
Expand All @@ -288,10 +231,6 @@ impl WorkspaceWorker {

*self.options.lock().await = changed_options.clone();

if changed_options.use_nested_configs() != current_option.use_nested_configs() {
self.refresh_nested_configs().await;
}

if Self::needs_linter_restart(current_option, &changed_options) {
self.refresh_server_linter().await;
return Some(self.revalidate_diagnostics().await);
Expand Down
Loading