Skip to content

Commit ea7e3f0

Browse files
authoredMar 17, 2025··
feat(oxc_language_server): Support nested configs (#9739)
This PR is built on #9731.
1 parent 17a9320 commit ea7e3f0

File tree

2 files changed

+94
-24
lines changed

2 files changed

+94
-24
lines changed
 

‎crates/oxc_language_server/src/main.rs

+92-22
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use futures::future::join_all;
55
use globset::Glob;
66
use ignore::gitignore::Gitignore;
77
use log::{debug, error, info};
8-
use rustc_hash::FxBuildHasher;
8+
use oxc_linter::{ConfigStore, ConfigStoreBuilder, FixKind, LintOptions, Linter, Oxlintrc};
9+
use rustc_hash::{FxBuildHasher, FxHashMap};
910
use serde::{Deserialize, Serialize};
1011
use tokio::sync::{Mutex, OnceCell, RwLock, SetError};
1112
use tower_lsp::{
@@ -15,14 +16,12 @@ use tower_lsp::{
1516
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse,
1617
ConfigurationItem, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams,
1718
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
18-
DidSaveTextDocumentParams, ExecuteCommandParams, InitializeParams, InitializeResult,
19-
InitializedParams, NumberOrString, Position, Range, ServerInfo, TextEdit, Url,
20-
WorkspaceEdit,
19+
DidSaveTextDocumentParams, ExecuteCommandParams, FileChangeType, InitializeParams,
20+
InitializeResult, InitializedParams, NumberOrString, Position, Range, ServerInfo, TextEdit,
21+
Url, WorkspaceEdit,
2122
},
2223
};
2324

24-
use oxc_linter::{ConfigStoreBuilder, FixKind, LintOptions, Linter, Oxlintrc};
25-
2625
use crate::capabilities::{CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC, Capabilities};
2726
use crate::linter::error_with_position::DiagnosticReport;
2827
use crate::linter::server_linter::ServerLinter;
@@ -33,13 +32,16 @@ mod linter;
3332

3433
type ConcurrentHashMap<K, V> = papaya::HashMap<K, V, FxBuildHasher>;
3534

35+
const OXC_CONFIG_FILE: &str = ".oxlintrc.json";
36+
3637
struct Backend {
3738
client: Client,
3839
root_uri: OnceCell<Option<Url>>,
3940
server_linter: RwLock<ServerLinter>,
4041
diagnostics_report_map: ConcurrentHashMap<String, Vec<DiagnosticReport>>,
4142
options: Mutex<Options>,
4243
gitignore_glob: Mutex<Vec<Gitignore>>,
44+
nested_configs: ConcurrentHashMap<PathBuf, ConfigStore>,
4345
}
4446
#[derive(Debug, Serialize, Deserialize, Default, PartialEq, PartialOrd, Clone, Copy)]
4547
#[serde(rename_all = "camelCase")]
@@ -54,11 +56,17 @@ struct Options {
5456
run: Run,
5557
enable: bool,
5658
config_path: String,
59+
flags: FxHashMap<String, String>,
5760
}
5861

5962
impl Default for Options {
6063
fn default() -> Self {
61-
Self { enable: true, run: Run::default(), config_path: ".oxlintrc.json".into() }
64+
Self {
65+
enable: true,
66+
run: Run::default(),
67+
config_path: OXC_CONFIG_FILE.into(),
68+
flags: FxHashMap::default(),
69+
}
6270
}
6371
}
6472

@@ -77,6 +85,10 @@ impl Options {
7785
fn get_config_path(&self) -> Option<PathBuf> {
7886
if self.config_path.is_empty() { None } else { Some(PathBuf::from(&self.config_path)) }
7987
}
88+
89+
fn disable_nested_configs(&self) -> bool {
90+
self.flags.contains_key("disable_nested_config")
91+
}
8092
}
8193

8294
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
@@ -166,6 +178,10 @@ impl LanguageServer for Backend {
166178
self.publish_all_diagnostics(&cleared_diagnostics).await;
167179
}
168180

181+
if changed_options.disable_nested_configs() {
182+
self.nested_configs.pin().clear();
183+
}
184+
169185
*self.options.lock().await = changed_options.clone();
170186

171187
// revalidate the config and all open files, when lint level is not disabled and the config path is changed
@@ -180,8 +196,48 @@ impl LanguageServer for Backend {
180196
}
181197
}
182198

183-
async fn did_change_watched_files(&self, _params: DidChangeWatchedFilesParams) {
199+
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
184200
debug!("watched file did change");
201+
if !self.options.lock().await.disable_nested_configs() {
202+
let nested_configs = self.nested_configs.pin();
203+
204+
params.changes.iter().for_each(|x| {
205+
let Ok(file_path) = x.uri.to_file_path() else {
206+
info!("Unable to convert {:?} to a file path", x.uri);
207+
return;
208+
};
209+
let Some(file_name) = file_path.file_name() else {
210+
info!("Unable to retrieve file name from {:?}", file_path);
211+
return;
212+
};
213+
214+
if file_name != OXC_CONFIG_FILE {
215+
return;
216+
}
217+
218+
let Some(dir_path) = file_path.parent() else {
219+
info!("Unable to retrieve parent from {:?}", file_path);
220+
return;
221+
};
222+
223+
// spellchecker:off -- "typ" is accurate
224+
if x.typ == FileChangeType::CREATED || x.typ == FileChangeType::CHANGED {
225+
// spellchecker:on
226+
let oxlintrc =
227+
Oxlintrc::from_file(&file_path).expect("Failed to parse config file");
228+
let config_store_builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc)
229+
.expect("Failed to create config store builder");
230+
let config_store =
231+
config_store_builder.build().expect("Failed to build config store");
232+
nested_configs.insert(dir_path.to_path_buf(), config_store);
233+
// spellchecker:off -- "typ" is accurate
234+
} else if x.typ == FileChangeType::DELETED {
235+
// spellchecker:on
236+
nested_configs.remove(&dir_path.to_path_buf());
237+
}
238+
});
239+
}
240+
185241
self.init_linter_config().await;
186242
self.revalidate_open_files().await;
187243
}
@@ -492,21 +548,34 @@ impl Backend {
492548
if config.exists() {
493549
config_path = Some(config);
494550
}
495-
if let Some(config_path) = config_path {
496-
let mut linter = self.server_linter.write().await;
497-
let config = Oxlintrc::from_file(&config_path)
498-
.expect("should have initialized linter with new options");
499-
let config_store = ConfigStoreBuilder::from_oxlintrc(true, config.clone())
500-
.expect("failed to build config")
501-
.build()
502-
.expect("failed to build config");
503-
*linter = ServerLinter::new_with_linter(
504-
Linter::new(LintOptions::default(), config_store).with_fix(FixKind::SafeFix),
505-
);
506-
return Some(config);
507-
}
508551

509-
None
552+
let config_path = config_path?;
553+
let oxlintrc = Oxlintrc::from_file(&config_path)
554+
.expect("should have initialized linter with new options");
555+
let config_store = ConfigStoreBuilder::from_oxlintrc(true, oxlintrc.clone())
556+
.expect("failed to build config")
557+
.build()
558+
.expect("failed to build config");
559+
560+
let linter = if self.options.lock().await.disable_nested_configs() {
561+
Linter::new(LintOptions::default(), config_store).with_fix(FixKind::SafeFix)
562+
} else {
563+
let nested_configs = self.nested_configs.pin();
564+
let nested_configs_copy: FxHashMap<PathBuf, ConfigStore> = nested_configs
565+
.iter()
566+
.map(|(key, value)| (key.clone(), value.clone()))
567+
.collect::<FxHashMap<_, _>>();
568+
569+
Linter::new_with_nested_configs(
570+
LintOptions::default(),
571+
config_store,
572+
nested_configs_copy,
573+
)
574+
};
575+
576+
*self.server_linter.write().await = ServerLinter::new_with_linter(linter);
577+
578+
Some(oxlintrc.clone())
510579
}
511580

512581
async fn handle_file_update(&self, uri: Url, content: Option<String>, version: Option<i32>) {
@@ -568,6 +637,7 @@ async fn main() {
568637
diagnostics_report_map,
569638
options: Mutex::new(Options::default()),
570639
gitignore_glob: Mutex::new(vec![]),
640+
nested_configs: ConcurrentHashMap::default(),
571641
})
572642
.finish();
573643

‎crates/oxc_linter/src/config/config_store.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ impl Clone for ResolvedLinterState {
1919
}
2020
}
2121

22-
#[derive(Debug)]
22+
#[derive(Debug, Clone)]
2323
struct Config {
2424
/// The basic linter state for this configuration.
2525
base: ResolvedLinterState,
@@ -29,7 +29,7 @@ struct Config {
2929
}
3030

3131
/// Resolves a lint configuration for a given file, by applying overrides based on the file's path.
32-
#[derive(Debug)]
32+
#[derive(Debug, Clone)]
3333
pub struct ConfigStore {
3434
base: Config,
3535
}

0 commit comments

Comments
 (0)
Please sign in to comment.