diff --git a/apps/oxlint/src/config_loader.rs b/apps/oxlint/src/config_loader.rs new file mode 100644 index 0000000000000..f30e565e035a1 --- /dev/null +++ b/apps/oxlint/src/config_loader.rs @@ -0,0 +1,103 @@ +use std::path::{Path, PathBuf}; + +use oxc_diagnostics::OxcDiagnostic; +use oxc_linter::{ + Config, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore, LintFilter, Oxlintrc, +}; + +pub struct LoadedConfig { + /// The directory this config applies to + pub dir: PathBuf, + /// The built configuration + pub config: Config, + /// Ignore patterns from this config + pub ignore_patterns: Vec, + /// Paths from extends directives + pub extended_paths: Vec, +} + +/// Errors that can occur when loading configs +#[derive(Debug)] +pub enum ConfigLoadError { + /// Failed to parse the config file + Parse { path: PathBuf, error: OxcDiagnostic }, + /// Failed to build the ConfigStore + Build { path: PathBuf, error: String }, +} + +impl ConfigLoadError { + /// Get the path of the config file that failed + pub fn path(&self) -> &Path { + match self { + ConfigLoadError::Parse { path, .. } | ConfigLoadError::Build { path, .. } => path, + } + } +} + +pub struct ConfigLoader<'a> { + external_linter: Option<&'a ExternalLinter>, + external_plugin_store: &'a mut ExternalPluginStore, + filters: &'a [LintFilter], +} + +impl<'a> ConfigLoader<'a> { + /// Create a new ConfigLoader + /// + /// # Arguments + /// * `external_linter` - Optional external linter for plugin support + /// * `external_plugin_store` - Store for external plugins + /// * `filters` - Lint filters to apply to configs + pub fn new( + external_linter: Option<&'a ExternalLinter>, + external_plugin_store: &'a mut ExternalPluginStore, + filters: &'a [LintFilter], + ) -> Self { + Self { external_linter, external_plugin_store, filters } + } + + /// Load a single config from a file path + fn load(&mut self, path: &Path) -> Result { + let oxlintrc = Oxlintrc::from_file(path) + .map_err(|error| ConfigLoadError::Parse { path: path.to_path_buf(), error })?; + + let dir = oxlintrc.path.parent().unwrap().to_path_buf(); + let ignore_patterns = oxlintrc.ignore_patterns.clone(); + + let builder = ConfigStoreBuilder::from_oxlintrc( + false, + oxlintrc, + self.external_linter, + self.external_plugin_store, + ) + .map_err(|e| ConfigLoadError::Build { path: path.to_path_buf(), error: e.to_string() })?; + + let extended_paths = builder.extended_paths.clone(); + + let config = + builder.with_filters(self.filters).build(self.external_plugin_store).map_err(|e| { + ConfigLoadError::Build { path: path.to_path_buf(), error: e.to_string() } + })?; + + Ok(LoadedConfig { dir, config, ignore_patterns, extended_paths }) + } + + /// Load multiple configs, returning successes and errors separately + /// + /// This allows callers to decide how to handle errors (fail fast vs continue) + pub fn load_many( + &mut self, + paths: impl IntoIterator>, + ) -> (Vec, Vec) { + let mut configs = Vec::new(); + let mut errors = Vec::new(); + + for path in paths { + match self.load(path.as_ref()) { + Ok(config) => configs.push(config), + Err(e) => errors.push(e), + } + } + + (configs, errors) + } +} diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs index b1ea431d996ac..93f7e6af24e76 100644 --- a/apps/oxlint/src/lib.rs +++ b/apps/oxlint/src/lib.rs @@ -2,6 +2,7 @@ #![cfg_attr(not(feature = "napi"), allow(dead_code))] mod command; +mod config_loader; mod init; mod lint; mod lsp; diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index cfd9aff399d79..549c81ab4caf0 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -20,6 +20,7 @@ use oxc_linter::{ use crate::{ DEFAULT_OXLINTRC_NAME, cli::{CliRunResult, LintCommand, MiscOptions, ReportUnusedDirectives, WarningOptions}, + config_loader::{ConfigLoadError, ConfigLoader}, output_formatter::{LintCommandInfo, OutputFormatter}, walk::Walk, }; @@ -467,8 +468,8 @@ impl CliRunner { fn get_nested_configs( stdout: &mut dyn Write, handler: &GraphicalReportHandler, - filters: &Vec, - paths: &Vec>, + filters: &[LintFilter], + paths: &[Arc], external_linter: Option<&ExternalLinter>, external_plugin_store: &mut ExternalPluginStore, nested_ignore_patterns: &mut Vec<(Vec, PathBuf)>, @@ -501,66 +502,37 @@ impl CliRunner { } } - let mut nested_configs = FxHashMap::::with_capacity_and_hasher( - nested_oxlintrc.len(), - FxBuildHasher, - ); - - // iterate over each config path and build the ConfigStore - for path in nested_oxlintrc { - let oxlintrc = match Oxlintrc::from_file(&path) { - Ok(oxlintrc) => oxlintrc, - Err(e) => { - print_and_flush_stdout( - stdout, - &format!( - "Failed to parse oxlint configuration file at {}.\n{}\n", - path.to_string_lossy().cow_replace('\\', "/"), - render_report(handler, &e) - ), - ); - return Err(CliRunResult::InvalidOptionConfig); + // Load all discovered configs + let mut loader = ConfigLoader::new(external_linter, external_plugin_store, filters); + let (configs, errors) = loader.load_many(nested_oxlintrc); + + if let Some(error) = errors.first() { + let message = match error { + ConfigLoadError::Parse { path, error } => { + format!( + "Failed to parse oxlint configuration file at {}.\n{}\n", + path.to_string_lossy().cow_replace('\\', "/"), + render_report(handler, error) + ) } - }; - let dir = oxlintrc.path.parent().unwrap().to_path_buf(); - // Collect ignore patterns and their root - nested_ignore_patterns.push((oxlintrc.ignore_patterns.clone(), dir.clone())); - - // TODO(refactor): clean up all of the error handling in this function - let builder = match ConfigStoreBuilder::from_oxlintrc( - false, - oxlintrc, - external_linter, - external_plugin_store, - ) { - Ok(builder) => builder, - Err(e) => { - print_and_flush_stdout( - stdout, - &format!( - "Failed to parse oxlint configuration file.\n{}\n", - render_report(handler, &OxcDiagnostic::error(e.to_string())) - ), - ); - return Err(CliRunResult::InvalidOptionConfig); - } - } - .with_filters(filters); - - let config = match builder.build(external_plugin_store) { - Ok(config) => config, - Err(e) => { - print_and_flush_stdout( - stdout, - &format!( - "Failed to build configuration.\n{}\n", - render_report(handler, &OxcDiagnostic::error(e.to_string())) - ), - ); - return Err(CliRunResult::InvalidOptionConfig); + ConfigLoadError::Build { path, error } => { + format!( + "Failed to build oxlint configuration file at {}.\n{}\n", + path.to_string_lossy().cow_replace('\\', "/"), + render_report(handler, &OxcDiagnostic::error(error.clone())) + ) } }; - nested_configs.insert(dir, config); + + print_and_flush_stdout(stdout, &message); + return Err(CliRunResult::InvalidOptionConfig); + } + + let mut nested_configs = + FxHashMap::::with_capacity_and_hasher(configs.len(), FxBuildHasher); + for loaded in configs { + nested_ignore_patterns.push((loaded.ignore_patterns, loaded.dir.clone())); + nested_configs.insert(loaded.dir, loaded.config); } Ok(nested_configs) diff --git a/apps/oxlint/src/lsp/server_linter.rs b/apps/oxlint/src/lsp/server_linter.rs index 4a11514b3014c..70a85a900037b 100644 --- a/apps/oxlint/src/lsp/server_linter.rs +++ b/apps/oxlint/src/lsp/server_linter.rs @@ -28,6 +28,7 @@ use oxc_language_server::{ use crate::{ DEFAULT_OXLINTRC_NAME, + config_loader::ConfigLoader, lsp::{ code_actions::{ CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC, apply_all_fix_code_action, apply_fix_code_actions, @@ -276,36 +277,20 @@ impl ServerLinterBuilder { extended_paths: &mut FxHashSet, ) -> FxHashMap { let paths = ConfigWalker::new(root_path).paths(); - let mut nested_configs = - FxHashMap::with_capacity_and_hasher(paths.capacity(), FxBuildHasher); - for path in paths { - let file_path = Path::new(&path); - let Some(dir_path) = file_path.parent() else { - continue; - }; + let mut loader = ConfigLoader::new(external_linter, external_plugin_store, &[]); + let (configs, errors) = loader.load_many(paths.iter().map(Path::new)); - let Ok(oxlintrc) = Oxlintrc::from_file(file_path) else { - warn!("Skipping invalid config file: {}", file_path.display()); - continue; - }; - // Collect ignore patterns and their root - nested_ignore_patterns.push((oxlintrc.ignore_patterns.clone(), dir_path.to_path_buf())); - let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc( - false, - oxlintrc, - external_linter, - external_plugin_store, - ) else { - warn!("Skipping config (builder failed): {}", file_path.display()); - continue; - }; - extended_paths.extend(config_store_builder.extended_paths.clone()); - let config = config_store_builder.build(external_plugin_store).unwrap_or_else(|err| { - warn!("Failed to build nested config for {}: {:?}", dir_path.display(), err); - ConfigStoreBuilder::empty().build(external_plugin_store).unwrap() - }); - nested_configs.insert(dir_path.to_path_buf(), config); + for error in errors { + warn!("Skipping config file {}: {:?}", error.path().display(), error); + } + + let mut nested_configs = FxHashMap::with_capacity_and_hasher(configs.len(), FxBuildHasher); + + for loaded in configs { + nested_ignore_patterns.push((loaded.ignore_patterns, loaded.dir.clone())); + extended_paths.extend(loaded.extended_paths); + nested_configs.insert(loaded.dir, loaded.config); } nested_configs