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
103 changes: 103 additions & 0 deletions apps/oxlint/src/config_loader.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
/// Paths from extends directives
pub extended_paths: Vec<PathBuf>,
}

/// 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<LoadedConfig, ConfigLoadError> {
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<Item = impl AsRef<Path>>,
) -> (Vec<LoadedConfig>, Vec<ConfigLoadError>) {
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)
}
}
1 change: 1 addition & 0 deletions apps/oxlint/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#![cfg_attr(not(feature = "napi"), allow(dead_code))]

mod command;
mod config_loader;
mod init;
mod lint;
mod lsp;
Expand Down
90 changes: 31 additions & 59 deletions apps/oxlint/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -467,8 +468,8 @@ impl CliRunner {
fn get_nested_configs(
stdout: &mut dyn Write,
handler: &GraphicalReportHandler,
filters: &Vec<LintFilter>,
paths: &Vec<Arc<OsStr>>,
filters: &[LintFilter],
paths: &[Arc<OsStr>],
external_linter: Option<&ExternalLinter>,
external_plugin_store: &mut ExternalPluginStore,
nested_ignore_patterns: &mut Vec<(Vec<String>, PathBuf)>,
Expand Down Expand Up @@ -501,66 +502,37 @@ impl CliRunner {
}
}

let mut nested_configs = FxHashMap::<PathBuf, Config>::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::<PathBuf, Config>::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)
Expand Down
41 changes: 13 additions & 28 deletions apps/oxlint/src/lsp/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -276,36 +277,20 @@ impl ServerLinterBuilder {
extended_paths: &mut FxHashSet<PathBuf>,
) -> FxHashMap<PathBuf, Config> {
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
Expand Down
Loading