From 2be3728dd4e53406b59f96d1378690335562e422 Mon Sep 17 00:00:00 2001 From: Sysix <3897725+Sysix@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:12:50 +0000 Subject: [PATCH] fix(oxlint/lsp): skip parsing base config again for nested config search (#20808) Added a skip check for the base config to avoid parsing it again. Very useful for JS config or JS plugins, where we need roundtrips to JS integrations. --- apps/oxlint/src/config_loader.rs | 25 +++++++++++++---- apps/oxlint/src/lsp/server_linter.rs | 41 +++++++++++++++------------- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/apps/oxlint/src/config_loader.rs b/apps/oxlint/src/config_loader.rs index f5be85b2f91b2..aab72077bf168 100644 --- a/apps/oxlint/src/config_loader.rs +++ b/apps/oxlint/src/config_loader.rs @@ -62,10 +62,14 @@ pub fn discover_configs_in_ancestors>( } /// Discover config files by walking DOWN from a root directory. +/// Will skip the base config file (e.g., root oxlintrc) to avoid duplicate loading. /// /// Used by LSP where we have a workspace root and need to discover all configs /// upfront for file watching and diagnostics. -pub fn discover_configs_in_tree(root: &Path) -> impl IntoIterator { +pub fn discover_configs_in_tree( + root: &Path, + base_config_path: &Path, +) -> impl IntoIterator { let walker = ignore::WalkBuilder::new(root) .hidden(false) // don't skip hidden files .parents(false) // disable gitignore from parent dirs @@ -75,7 +79,8 @@ pub fn discover_configs_in_tree(root: &Path) -> impl IntoIterator>(); - let mut builder = ConfigWalkBuilder { sender }; + let mut builder = + ConfigWalkBuilder { sender, base_config_path: base_config_path.to_path_buf() }; walker.visit(&mut builder); drop(builder); @@ -106,17 +111,23 @@ fn find_configs_in_directory(dir: &Path) -> Vec { // Helper types for parallel directory walking struct ConfigWalkBuilder { sender: mpsc::Sender>, + base_config_path: PathBuf, } impl<'s> ignore::ParallelVisitorBuilder<'s> for ConfigWalkBuilder { fn build(&mut self) -> Box { - Box::new(ConfigWalkCollector { configs: vec![], sender: self.sender.clone() }) + Box::new(ConfigWalkCollector { + configs: vec![], + sender: self.sender.clone(), + base_config_path: self.base_config_path.clone(), + }) } } struct ConfigWalkCollector { configs: Vec, sender: mpsc::Sender>, + base_config_path: PathBuf, } impl Drop for ConfigWalkCollector { @@ -130,7 +141,7 @@ impl ignore::ParallelVisitor for ConfigWalkCollector { fn visit(&mut self, entry: Result) -> ignore::WalkState { match entry { Ok(entry) => { - if let Some(config) = to_discovered_config(&entry) { + if let Some(config) = to_discovered_config(&entry, &self.base_config_path) { self.configs.push(config); } ignore::WalkState::Continue @@ -140,11 +151,15 @@ impl ignore::ParallelVisitor for ConfigWalkCollector { } } -fn to_discovered_config(entry: &DirEntry) -> Option { +fn to_discovered_config(entry: &DirEntry, base_config_path: &Path) -> Option { let file_type = entry.file_type()?; if file_type.is_dir() { return None; } + if entry.path() == base_config_path { + // Skip the base config file (e.g., root oxlintrc) to avoid duplicate loading + return None; + } let file_name = entry.path().file_name()?; if file_name == DEFAULT_OXLINTRC_NAME { Some(DiscoveredConfig::Json(entry.path().to_path_buf())) diff --git a/apps/oxlint/src/lsp/server_linter.rs b/apps/oxlint/src/lsp/server_linter.rs index e522a651bf836..9344c0e751944 100644 --- a/apps/oxlint/src/lsp/server_linter.rs +++ b/apps/oxlint/src/lsp/server_linter.rs @@ -90,20 +90,6 @@ impl ServerLinterBuilder { } } - let mut nested_ignore_patterns = Vec::new(); - let mut extended_paths = FxHashSet::default(); - let nested_configs = if options.use_nested_configs() { - self.create_nested_configs( - &root_path, - &mut external_plugin_store, - &mut nested_ignore_patterns, - &mut extended_paths, - Some(root_uri.as_str()), - ) - } else { - FxHashMap::default() - }; - let config_path = options.config_path.as_ref().filter(|p| !p.is_empty()).map(PathBuf::from); let loader = ConfigLoader::new( external_linter, @@ -123,6 +109,21 @@ impl ServerLinterBuilder { } }; + let mut nested_ignore_patterns = Vec::new(); + let mut extended_paths = FxHashSet::default(); + let nested_configs = if options.use_nested_configs() { + self.create_nested_configs( + &root_path, + &oxlintrc.path, + &mut external_plugin_store, + &mut nested_ignore_patterns, + &mut extended_paths, + Some(root_uri.as_str()), + ) + } else { + FxHashMap::default() + }; + let base_patterns = oxlintrc.ignore_patterns.clone(); let config_builder = match ConfigStoreBuilder::from_oxlintrc( @@ -342,12 +343,13 @@ impl ServerLinterBuilder { fn create_nested_configs( &self, root_path: &Path, + base_config_path: &Path, external_plugin_store: &mut ExternalPluginStore, nested_ignore_patterns: &mut Vec<(Vec, PathBuf)>, extended_paths: &mut FxHashSet, workspace_uri: Option<&str>, ) -> FxHashMap { - let config_paths = discover_configs_in_tree(root_path); + let config_paths = discover_configs_in_tree(root_path, base_config_path); #[cfg_attr(not(feature = "napi"), allow(unused_mut))] let mut loader = ConfigLoader::new( @@ -1253,8 +1255,10 @@ mod test { let mut nested_ignore_patterns = Vec::new(); let mut external_plugin_store = ExternalPluginStore::new(false); let mut extended_paths = FxHashSet::default(); + let base_config_path = get_file_path("fixtures/lsp/init_nested_configs/.oxlintrc.json"); let configs = builder.create_nested_configs( &get_file_path("fixtures/lsp/init_nested_configs"), + &base_config_path, &mut external_plugin_store, &mut nested_ignore_patterns, &mut extended_paths, @@ -1264,10 +1268,9 @@ mod test { // sorting the key because for consistent tests results configs_dirs.sort(); - assert!(configs_dirs.len() == 3); - assert!(configs_dirs[2].ends_with("deep2")); - assert!(configs_dirs[1].ends_with("deep1")); - assert!(configs_dirs[0].ends_with("init_nested_configs")); + assert_eq!(configs_dirs.len(), 2); + assert!(configs_dirs[1].ends_with("deep2")); + assert!(configs_dirs[0].ends_with("deep1")); } #[test]