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
117 changes: 116 additions & 1 deletion apps/oxlint/src/config_loader.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,124 @@
use std::path::{Path, PathBuf};
use std::{
ffi::OsStr,
path::{Path, PathBuf},
sync::{Arc, mpsc},
};

use ignore::DirEntry;
use oxc_diagnostics::OxcDiagnostic;
use oxc_linter::{
Config, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore, LintFilter, Oxlintrc,
};
use rustc_hash::FxHashSet;

use crate::DEFAULT_OXLINTRC_NAME;

/// Discover config files by walking UP from each file's directory to ancestors.
///
/// Used by CLI where we have specific files to lint and need to find configs
/// that apply to them.
///
/// Example: For files `/project/src/foo.js` and `/project/src/bar/baz.js`:
/// - Checks `/project/src/bar/`, `/project/src/`, `/project/`, `/`
/// - Returns paths to any `.oxlintrc.json` files found
pub fn discover_configs_in_ancestors<P: AsRef<Path>>(
files: &[P],
) -> impl IntoIterator<Item = PathBuf> {
let mut config_paths = FxHashSet::<PathBuf>::default();
let mut visited_dirs = FxHashSet::default();

for file in files {
let path = file.as_ref();
// Start from the file's parent directory and walk up the tree
let mut current = path.parent();
while let Some(dir) = current {
// Stop if we've already checked this directory (and its ancestors)
let inserted = visited_dirs.insert(dir.to_path_buf());
if !inserted {
break;
}
if let Some(config_path) = find_config_in_directory(dir) {
config_paths.insert(config_path);
}
current = dir.parent();
}
}

config_paths.into_iter()
}

/// Discover config files by walking DOWN from a root directory.
///
/// 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 = PathBuf> {
let walker = ignore::WalkBuilder::new(root)
.hidden(false) // don't skip hidden files
.parents(false) // disable gitignore from parent dirs
.ignore(false) // disable .ignore files
.git_global(false) // disable global gitignore
.follow_links(true)
.build_parallel();

let (sender, receiver) = mpsc::channel::<Vec<Arc<OsStr>>>();
let mut builder = ConfigWalkBuilder { sender };
walker.visit(&mut builder);
drop(builder);

receiver.into_iter().flatten().map(|p| PathBuf::from(p.as_ref()))
}

/// Check if a directory contains an oxlint config file.
fn find_config_in_directory(dir: &Path) -> Option<PathBuf> {
let config_path = dir.join(DEFAULT_OXLINTRC_NAME);
if config_path.is_file() { Some(config_path) } else { None }
}

// Helper types for parallel directory walking
struct ConfigWalkBuilder {
sender: mpsc::Sender<Vec<Arc<OsStr>>>,
}

impl<'s> ignore::ParallelVisitorBuilder<'s> for ConfigWalkBuilder {
fn build(&mut self) -> Box<dyn ignore::ParallelVisitor + 's> {
Box::new(ConfigWalkCollector { paths: vec![], sender: self.sender.clone() })
}
}

struct ConfigWalkCollector {
paths: Vec<Arc<OsStr>>,
sender: mpsc::Sender<Vec<Arc<OsStr>>>,
}

impl Drop for ConfigWalkCollector {
fn drop(&mut self) {
let paths = std::mem::take(&mut self.paths);
self.sender.send(paths).unwrap();
}
}

impl ignore::ParallelVisitor for ConfigWalkCollector {
fn visit(&mut self, entry: Result<DirEntry, ignore::Error>) -> ignore::WalkState {
match entry {
Ok(entry) => {
if is_config_file(&entry) {
self.paths.push(entry.path().as_os_str().into());
}
ignore::WalkState::Continue
}
Err(_) => ignore::WalkState::Skip,
}
}
}

fn is_config_file(entry: &DirEntry) -> bool {
let Some(file_type) = entry.file_type() else { return false };
if file_type.is_dir() {
return false;
}
let Some(file_name) = entry.path().file_name() else { return false };
file_name == DEFAULT_OXLINTRC_NAME
}

pub struct LoadedConfig {
/// The directory this config applies to
Expand Down
53 changes: 12 additions & 41 deletions apps/oxlint/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::{

use cow_utils::CowUtils;
use ignore::{gitignore::Gitignore, overrides::OverrideBuilder};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use rustc_hash::{FxBuildHasher, FxHashMap};

use oxc_diagnostics::{DiagnosticSender, DiagnosticService, GraphicalReportHandler, OxcDiagnostic};
use oxc_linter::{
Expand All @@ -20,7 +20,7 @@ use oxc_linter::{
use crate::{
DEFAULT_OXLINTRC_NAME,
cli::{CliRunResult, LintCommand, MiscOptions, ReportUnusedDirectives, WarningOptions},
config_loader::{ConfigLoadError, ConfigLoader},
config_loader::{ConfigLoadError, ConfigLoader, discover_configs_in_ancestors},
output_formatter::{LintCommandInfo, OutputFormatter},
walk::Walk,
};
Expand Down Expand Up @@ -474,40 +474,18 @@ impl CliRunner {
external_plugin_store: &mut ExternalPluginStore,
nested_ignore_patterns: &mut Vec<(Vec<String>, PathBuf)>,
) -> Result<FxHashMap<PathBuf, Config>, CliRunResult> {
// TODO(perf): benchmark whether or not it is worth it to store the configurations on a
// per-file or per-directory basis, to avoid calling `.parent()` on every path.
let mut nested_oxlintrc = FxHashSet::<PathBuf>::default();
// get all of the unique directories among the paths to use for search for
// oxlint config files in those directories and their ancestors
// e.g. `/some/file.js` will check `/some` and `/`
// `/some/other/file.js` will check `/some/other`, `/some`, and `/`
let mut directories = FxHashSet::default();
for path in paths {
let path = Path::new(path);
// Start from the file's parent directory and walk up the tree
let mut current = path.parent();
while let Some(dir) = current {
// NOTE: Initial benchmarking showed that it was faster to iterate over the directories twice
// rather than constructing the configs in one iteration. It's worth re-benchmarking that though.
let inserted = directories.insert(dir);
if !inserted {
break;
}
current = dir.parent();
}
}
for directory in directories {
if let Some(path) = Self::find_oxlint_config_path_in_directory(directory) {
nested_oxlintrc.insert(path);
}
}
// Discover config files by walking up from each file's directory
let config_paths: Vec<_> =
paths.iter().map(|p| Path::new(p.as_ref()).to_path_buf()).collect();
let discovered_configs = discover_configs_in_ancestors(&config_paths);

// Load all discovered configs
let mut loader = ConfigLoader::new(external_linter, external_plugin_store, filters);
let (configs, errors) = loader.load_many(nested_oxlintrc);
let (configs, errors) = loader.load_many(discovered_configs);

if let Some(error) = errors.first() {
let message = match error {
// Fail on first error (CLI requires all configs to be valid)
if let Some(error) = errors.into_iter().next() {
let message = match &error {
ConfigLoadError::Parse { path, error } => {
format!(
"Failed to parse oxlint configuration file at {}.\n{}\n",
Expand All @@ -517,17 +495,17 @@ impl CliRunner {
}
ConfigLoadError::Build { path, error } => {
format!(
"Failed to build oxlint configuration file at {}.\n{}\n",
"Failed to build configuration from {}.\n{}\n",
path.to_string_lossy().cow_replace('\\', "/"),
render_report(handler, &OxcDiagnostic::error(error.clone()))
)
}
};

print_and_flush_stdout(stdout, &message);
return Err(CliRunResult::InvalidOptionConfig);
}

// Convert loaded configs to nested config format
let mut nested_configs =
FxHashMap::<PathBuf, Config>::with_capacity_and_hasher(configs.len(), FxBuildHasher);
for loaded in configs {
Expand All @@ -551,13 +529,6 @@ impl CliRunner {
}
Ok(Oxlintrc::default())
}

/// Looks in a directory for an oxlint config file and returns the path if it exists.
/// Does not validate the file or apply the default config file.
fn find_oxlint_config_path_in_directory(dir: &Path) -> Option<PathBuf> {
let possible_config_path = dir.join(DEFAULT_OXLINTRC_NAME);
if possible_config_path.is_file() { Some(possible_config_path) } else { None }
}
}

pub fn print_and_flush_stdout(stdout: &mut dyn Write, message: &str) {
Expand Down
87 changes: 0 additions & 87 deletions apps/oxlint/src/lsp/config_walker.rs

This file was deleted.

1 change: 0 additions & 1 deletion apps/oxlint/src/lsp/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
mod code_actions;
mod commands;
mod config_walker;
mod error_with_position;
mod lsp_file_system;
mod options;
Expand Down
6 changes: 3 additions & 3 deletions apps/oxlint/src/lsp/server_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ use oxc_language_server::{
use crate::{
DEFAULT_OXLINTRC_NAME,
config_loader::ConfigLoader,
config_loader::discover_configs_in_tree,
lsp::{
code_actions::{
CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC, apply_all_fix_code_action, apply_fix_code_actions,
fix_all_text_edit,
},
commands::{FIX_ALL_COMMAND_ID, FixAllCommandArgs},
config_walker::ConfigWalker,
error_with_position::{
DiagnosticReport, LinterCodeAction, create_unused_directives_messages,
generate_inverted_diagnostics, message_to_lsp_diagnostic,
Expand Down Expand Up @@ -276,10 +276,10 @@ impl ServerLinterBuilder {
nested_ignore_patterns: &mut Vec<(Vec<String>, PathBuf)>,
extended_paths: &mut FxHashSet<PathBuf>,
) -> FxHashMap<PathBuf, Config> {
let paths = ConfigWalker::new(root_path).paths();
let config_paths = discover_configs_in_tree(root_path);

let mut loader = ConfigLoader::new(external_linter, external_plugin_store, &[]);
let (configs, errors) = loader.load_many(paths.iter().map(Path::new));
let (configs, errors) = loader.load_many(config_paths);

for error in errors {
warn!("Skipping config file {}: {:?}", error.path().display(), error);
Expand Down
Loading