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
25 changes: 20 additions & 5 deletions apps/oxlint/src/config_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,14 @@ pub fn discover_configs_in_ancestors<P: AsRef<Path>>(
}

/// 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<Item = DiscoveredConfig> {
pub fn discover_configs_in_tree(
root: &Path,
base_config_path: &Path,
) -> impl IntoIterator<Item = DiscoveredConfig> {
let walker = ignore::WalkBuilder::new(root)
.hidden(false) // don't skip hidden files
.parents(false) // disable gitignore from parent dirs
Expand All @@ -75,7 +79,8 @@ pub fn discover_configs_in_tree(root: &Path) -> impl IntoIterator<Item = Discove
.build_parallel();

let (sender, receiver) = mpsc::channel::<Vec<DiscoveredConfig>>();
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);

Expand Down Expand Up @@ -106,17 +111,23 @@ fn find_configs_in_directory(dir: &Path) -> Vec<DiscoveredConfig> {
// Helper types for parallel directory walking
struct ConfigWalkBuilder {
sender: mpsc::Sender<Vec<DiscoveredConfig>>,
base_config_path: PathBuf,
}

impl<'s> ignore::ParallelVisitorBuilder<'s> for ConfigWalkBuilder {
fn build(&mut self) -> Box<dyn ignore::ParallelVisitor + 's> {
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<DiscoveredConfig>,
sender: mpsc::Sender<Vec<DiscoveredConfig>>,
base_config_path: PathBuf,
}

impl Drop for ConfigWalkCollector {
Expand All @@ -130,7 +141,7 @@ impl ignore::ParallelVisitor for ConfigWalkCollector {
fn visit(&mut self, entry: Result<DirEntry, ignore::Error>) -> 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
Expand All @@ -140,11 +151,15 @@ impl ignore::ParallelVisitor for ConfigWalkCollector {
}
}

fn to_discovered_config(entry: &DirEntry) -> Option<DiscoveredConfig> {
fn to_discovered_config(entry: &DirEntry, base_config_path: &Path) -> Option<DiscoveredConfig> {
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()))
Expand Down
41 changes: 22 additions & 19 deletions apps/oxlint/src/lsp/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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<String>, PathBuf)>,
extended_paths: &mut FxHashSet<PathBuf>,
workspace_uri: Option<&str>,
) -> FxHashMap<PathBuf, Config> {
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(
Expand Down Expand Up @@ -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,
Expand All @@ -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]
Expand Down
Loading