diff --git a/apps/oxlint/src/config_loader.rs b/apps/oxlint/src/config_loader.rs index ad8aab482d2ef..7289f05a932af 100644 --- a/apps/oxlint/src/config_loader.rs +++ b/apps/oxlint/src/config_loader.rs @@ -398,7 +398,7 @@ impl<'a> ConfigLoader<'a> { self.load_many(configs) } - fn load_root_config( + pub(crate) fn load_root_config( &self, cwd: &Path, config_path: Option<&PathBuf>, diff --git a/apps/oxlint/src/lsp/mod.rs b/apps/oxlint/src/lsp/mod.rs index ee5e683292cf9..2a0140ef43b06 100644 --- a/apps/oxlint/src/lsp/mod.rs +++ b/apps/oxlint/src/lsp/mod.rs @@ -1,5 +1,8 @@ use oxc_linter::ExternalLinter; +#[cfg(feature = "napi")] +use crate::js_config::JsConfigLoaderCb; + mod code_actions; mod commands; mod error_with_position; @@ -11,11 +14,18 @@ mod tester; mod utils; /// Run the language server -pub async fn run_lsp(external_linter: Option) { +pub async fn run_lsp( + external_linter: Option, + #[cfg(feature = "napi")] js_config_loader: Option, +) { oxc_language_server::run_server( "oxlint".to_string(), env!("CARGO_PKG_VERSION").to_string(), - vec![Box::new(crate::lsp::server_linter::ServerLinterBuilder::new(external_linter))], + vec![Box::new(crate::lsp::server_linter::ServerLinterBuilder::new( + external_linter, + #[cfg(feature = "napi")] + js_config_loader, + ))], ) .await; } diff --git a/apps/oxlint/src/lsp/server_linter.rs b/apps/oxlint/src/lsp/server_linter.rs index 4543b2cb91fb4..439bb831ca9a4 100644 --- a/apps/oxlint/src/lsp/server_linter.rs +++ b/apps/oxlint/src/lsp/server_linter.rs @@ -27,7 +27,6 @@ use oxc_language_server::{ }; use crate::{ - DEFAULT_OXLINTRC_NAME, config_loader::{ConfigLoader, build_nested_configs, discover_configs_in_tree}, lsp::{ code_actions::{ @@ -48,11 +47,20 @@ use crate::{ #[derive(Default)] pub struct ServerLinterBuilder { external_linter: Option, + #[cfg(feature = "napi")] + js_config_loader: Option, } impl ServerLinterBuilder { - pub fn new(external_linter: Option) -> Self { - Self { external_linter } + pub fn new( + external_linter: Option, + #[cfg(feature = "napi")] js_config_loader: Option, + ) -> Self { + Self { + external_linter, + #[cfg(feature = "napi")] + js_config_loader, + } } /// # Panics @@ -82,9 +90,8 @@ 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( + self.create_nested_configs( &root_path, - self.external_linter.as_ref(), &mut external_plugin_store, &mut nested_ignore_patterns, &mut extended_paths, @@ -93,24 +100,23 @@ impl ServerLinterBuilder { } else { FxHashMap::default() }; - let config_path = match options.config_path.as_deref() { - Some("") | None => DEFAULT_OXLINTRC_NAME, - Some(v) => v, - }; - let config = normalize_path(root_path.join(config_path)); - let oxlintrc = if config.try_exists().is_ok_and(|exists| exists) { - if let Ok(oxlintrc) = Oxlintrc::from_file(&config) { - oxlintrc - } else { - warn!("Failed to initialize oxlintrc config: {}", config.to_string_lossy()); + + let config_path = options.config_path.as_ref().filter(|p| !p.is_empty()).map(PathBuf::from); + let loader = ConfigLoader::new( + self.external_linter.as_ref(), + &mut external_plugin_store, + &[], + Some(root_uri.as_str()), + ); + #[cfg(feature = "napi")] + let loader = loader.with_js_config_loader(self.js_config_loader.as_ref()); + + let oxlintrc = match loader.load_root_config(&root_path, config_path.as_ref()) { + Ok(config) => config, + Err(e) => { + warn!("Failed to load config: {e}"); Oxlintrc::default() } - } else { - warn!( - "Config file not found: {}, fallback to default config", - config.to_string_lossy() - ); - Oxlintrc::default() }; let base_patterns = oxlintrc.ignore_patterns.clone(); @@ -313,8 +319,8 @@ impl ServerLinterBuilder { /// Searches inside root_uri recursively for the default oxlint config files /// and insert them inside the nested configuration fn create_nested_configs( + &self, root_path: &Path, - external_linter: Option<&ExternalLinter>, external_plugin_store: &mut ExternalPluginStore, nested_ignore_patterns: &mut Vec<(Vec, PathBuf)>, extended_paths: &mut FxHashSet, @@ -322,8 +328,19 @@ impl ServerLinterBuilder { ) -> FxHashMap { let config_paths = discover_configs_in_tree(root_path); - let mut loader = - ConfigLoader::new(external_linter, external_plugin_store, &[], workspace_uri); + #[cfg_attr(not(feature = "napi"), allow(unused_mut))] + let mut loader = ConfigLoader::new( + self.external_linter.as_ref(), + external_plugin_store, + &[], + workspace_uri, + ); + + #[cfg(feature = "napi")] + { + loader = loader.with_js_config_loader(self.js_config_loader.as_ref()); + } + let (configs, errors) = loader.load_discovered(config_paths); for error in errors { @@ -449,15 +466,19 @@ impl Tool for ServerLinter { LSPLintOptions::default() } }; - let config_pattern = match options.config_path.as_deref() { - Some("") | None => "**/.oxlintrc.json".to_string(), - Some(v) => v.to_string(), + let mut watchers = match options.config_path.as_deref() { + Some("") | None => { + // Watch both JSON and TS config files + vec!["**/.oxlintrc.json".to_string(), "**/oxlint.config.ts".to_string()] + } + Some(v) => vec![v.to_string()], }; - let mut watchers = vec![config_pattern]; for path in &self.extended_paths { - // ignore .oxlintrc.json files when using nested configs - if path.ends_with(".oxlintrc.json") && options.use_nested_configs() { + // ignore .oxlintrc.json and oxlint.config.ts files when using nested configs + if (path.ends_with(".oxlintrc.json") || path.ends_with("oxlint.config.ts")) + && options.use_nested_configs() + { continue; } @@ -998,8 +1019,9 @@ mod test_watchers { let patterns = Tester::new("fixtures/lsp/watchers/default", json!({})).get_watcher_patterns(); - assert_eq!(patterns.len(), 1); + assert_eq!(patterns.len(), 2); assert_eq!(patterns[0], "**/.oxlintrc.json".to_string()); + assert_eq!(patterns[1], "**/oxlint.config.ts".to_string()); } #[test] @@ -1012,8 +1034,9 @@ mod test_watchers { ) .get_watcher_patterns(); - assert_eq!(patterns.len(), 1); + assert_eq!(patterns.len(), 2); assert_eq!(patterns[0], "**/.oxlintrc.json".to_string()); + assert_eq!(patterns[1], "**/oxlint.config.ts".to_string()); } #[test] @@ -1035,10 +1058,11 @@ mod test_watchers { let patterns = Tester::new("fixtures/lsp/watchers/linter_extends", json!({})) .get_watcher_patterns(); - // The `.oxlintrc.json` extends `./lint.json -> 2 watchers - assert_eq!(patterns.len(), 2); + // The `.oxlintrc.json` extends `./lint.json` -> 3 watchers (json, ts, lint.json) + assert_eq!(patterns.len(), 3); assert_eq!(patterns[0], "**/.oxlintrc.json".to_string()); - assert_eq!(patterns[1], "lint.json".to_string()); + assert_eq!(patterns[1], "**/oxlint.config.ts".to_string()); + assert_eq!(patterns[2], "lint.json".to_string()); } #[test] @@ -1066,9 +1090,10 @@ mod test_watchers { ) .get_watcher_patterns(); - assert_eq!(patterns.len(), 2); + assert_eq!(patterns.len(), 3); assert_eq!(patterns[0], "**/.oxlintrc.json".to_string()); - assert_eq!(patterns[1], "**/tsconfig*.json".to_string()); + assert_eq!(patterns[1], "**/oxlint.config.ts".to_string()); + assert_eq!(patterns[2], "**/tsconfig*.json".to_string()); } } @@ -1119,9 +1144,10 @@ mod test_watchers { "typeAware": true })); assert!(watch_patterns.is_some()); - assert_eq!(watch_patterns.as_ref().unwrap().len(), 2); + assert_eq!(watch_patterns.as_ref().unwrap().len(), 3); assert_eq!(watch_patterns.as_ref().unwrap()[0], "**/.oxlintrc.json".to_string()); - assert_eq!(watch_patterns.as_ref().unwrap()[1], "**/tsconfig*.json".to_string()); + assert_eq!(watch_patterns.as_ref().unwrap()[1], "**/oxlint.config.ts".to_string()); + assert_eq!(watch_patterns.as_ref().unwrap()[2], "**/tsconfig*.json".to_string()); } } } @@ -1141,12 +1167,12 @@ mod test { #[test] fn test_create_nested_configs() { + let builder = ServerLinterBuilder::default(); let mut nested_ignore_patterns = Vec::new(); let mut external_plugin_store = ExternalPluginStore::new(false); let mut extended_paths = FxHashSet::default(); - let configs = ServerLinterBuilder::create_nested_configs( + let configs = builder.create_nested_configs( &get_file_path("fixtures/lsp/init_nested_configs"), - None, &mut external_plugin_store, &mut nested_ignore_patterns, &mut extended_paths, diff --git a/apps/oxlint/src/main.rs b/apps/oxlint/src/main.rs index a2f6007455a5d..51f7a1a3c0e09 100644 --- a/apps/oxlint/src/main.rs +++ b/apps/oxlint/src/main.rs @@ -12,7 +12,12 @@ async fn main() -> CliRunResult { // If --lsp flag is set, run the language server if command.lsp { - run_lsp(None).await; + run_lsp( + None, + #[cfg(feature = "napi")] + None, + ) + .await; return CliRunResult::LintSucceeded; } diff --git a/apps/oxlint/src/run.rs b/apps/oxlint/src/run.rs index 41b712a0b5915..fcdb5ab3553c1 100644 --- a/apps/oxlint/src/run.rs +++ b/apps/oxlint/src/run.rs @@ -227,7 +227,7 @@ async fn lint_impl( // TODO: Remove this line once we have solidified the implementation and thoroughly tested it. let external_linter = if cfg!(feature = "testing") { external_linter } else { None }; - crate::lsp::run_lsp(external_linter).await; + crate::lsp::run_lsp(external_linter, js_config_loader).await; return CliRunResult::LintSucceeded; }