diff --git a/apps/oxlint/src-js/js_config.ts b/apps/oxlint/src-js/js_config.ts index eb3e0f90dcbf2..96170cba68db1 100644 --- a/apps/oxlint/src-js/js_config.ts +++ b/apps/oxlint/src-js/js_config.ts @@ -1,3 +1,16 @@ +import { getErrorMessage } from "./utils/utils.ts"; +import { JSONStringify } from "./utils/globals.ts"; + +interface JsConfigResult { + path: string; + config: unknown; // Will be validated as Oxlintrc on Rust side +} + +type LoadJsConfigsResult = + | { Success: JsConfigResult[] } + | { Failures: { path: string; error: string }[] } + | { Error: string }; + /** * Load JavaScript config files in parallel. * @@ -7,6 +20,47 @@ * @param paths - Array of absolute paths to oxlint.config.ts files * @returns JSON-stringified result with all configs or error */ -export async function loadJsConfigs(_paths: string[]): Promise { - throw new Error("Not implemented yet"); +export async function loadJsConfigs(paths: string[]): Promise { + try { + const results = await Promise.allSettled( + paths.map(async (path): Promise => { + const fileUrl = new URL(`file://${path}`); + const module = await import(fileUrl.href); + const config = module.default; + + if (config === undefined) { + throw new Error(`Configuration file has no default export.`); + } + + if (typeof config !== "object") { + throw new Error(`Configuration file must have a default export that is an object.`); + } + + return { path, config }; + }), + ); + + const successes: JsConfigResult[] = []; + const errors: { path: string; error: string }[] = []; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === "fulfilled") { + successes.push(result.value); + } else { + errors.push({ path: paths[i], error: getErrorMessage(result.reason) }); + } + } + + // If any config failed to load, report all errors + if (errors.length > 0) { + return JSONStringify({ Failures: errors } satisfies LoadJsConfigsResult); + } + + return JSONStringify({ Success: successes } satisfies LoadJsConfigsResult); + } catch (err) { + return JSONStringify({ + Error: getErrorMessage(err), + } satisfies LoadJsConfigsResult); + } } diff --git a/apps/oxlint/src/config_loader.rs b/apps/oxlint/src/config_loader.rs index 96f5bf6f1f15e..ad8aab482d2ef 100644 --- a/apps/oxlint/src/config_loader.rs +++ b/apps/oxlint/src/config_loader.rs @@ -5,17 +5,22 @@ use std::{ }; use ignore::DirEntry; + use oxc_diagnostics::OxcDiagnostic; use oxc_linter::{ Config, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore, LintFilter, Oxlintrc, }; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; -use crate::DEFAULT_OXLINTRC_NAME; +use crate::{DEFAULT_OXLINTRC_NAME, DEFAULT_TS_OXLINTRC_NAME}; + +#[cfg(feature = "napi")] +use crate::js_config; #[derive(Debug, Hash, PartialEq, Eq)] pub enum DiscoveredConfig { Json(PathBuf), + Js(PathBuf), } /// Discover config files by walking UP from each file's directory to ancestors. @@ -42,7 +47,7 @@ pub fn discover_configs_in_ancestors>( if !inserted { break; } - if let Some(config) = find_config_in_directory(dir) { + for config in find_configs_in_directory(dir) { config_paths.insert(config); } current = dir.parent(); @@ -74,9 +79,20 @@ pub fn discover_configs_in_tree(root: &Path) -> impl IntoIterator Option { - let config_path = dir.join(DEFAULT_OXLINTRC_NAME); - if config_path.is_file() { Some(DiscoveredConfig::Json(config_path)) } else { None } +fn find_configs_in_directory(dir: &Path) -> Vec { + let mut configs = Vec::new(); + + let json_path = dir.join(DEFAULT_OXLINTRC_NAME); + if json_path.is_file() { + configs.push(DiscoveredConfig::Json(json_path)); + } + + let ts_path = dir.join(DEFAULT_TS_OXLINTRC_NAME); + if ts_path.is_file() { + configs.push(DiscoveredConfig::Js(ts_path)); + } + + configs } // Helper types for parallel directory walking @@ -124,6 +140,8 @@ fn to_discovered_config(entry: &DirEntry) -> Option { let file_name = entry.path().file_name()?; if file_name == DEFAULT_OXLINTRC_NAME { Some(DiscoveredConfig::Json(entry.path().to_path_buf())) + } else if file_name == DEFAULT_TS_OXLINTRC_NAME { + Some(DiscoveredConfig::Js(entry.path().to_path_buf())) } else { None } @@ -144,16 +162,27 @@ pub struct LoadedConfig { #[derive(Debug)] pub enum ConfigLoadError { /// Failed to parse the config file - Parse { path: PathBuf, error: OxcDiagnostic }, + Parse { + path: PathBuf, + error: OxcDiagnostic, + }, /// Failed to build the ConfigStore - Build { path: PathBuf, error: String }, + Build { + path: PathBuf, + error: String, + }, + + TypeScriptConfigFileFoundButJsRuntimeNotAvailable, + + Diagnostic(OxcDiagnostic), } impl ConfigLoadError { /// Get the path of the config file that failed - pub fn path(&self) -> &Path { + pub fn path(&self) -> Option<&Path> { match self { - ConfigLoadError::Parse { path, .. } | ConfigLoadError::Build { path, .. } => path, + ConfigLoadError::Parse { path, .. } | ConfigLoadError::Build { path, .. } => Some(path), + _ => None, } } } @@ -186,6 +215,9 @@ pub struct ConfigLoader<'a> { external_plugin_store: &'a mut ExternalPluginStore, filters: &'a [LintFilter], workspace_uri: Option<&'a str>, + #[cfg(feature = "napi")] + #[expect(clippy::struct_field_names)] + js_config_loader: Option<&'a js_config::JsConfigLoaderCb>, } impl<'a> ConfigLoader<'a> { @@ -202,34 +234,62 @@ impl<'a> ConfigLoader<'a> { filters: &'a [LintFilter], workspace_uri: Option<&'a str>, ) -> Self { - Self { external_linter, external_plugin_store, filters, workspace_uri } + Self { + external_linter, + external_plugin_store, + filters, + workspace_uri, + #[cfg(feature = "napi")] + js_config_loader: None, + } + } + + #[cfg(feature = "napi")] + #[must_use] + pub fn with_js_config_loader( + mut self, + js_config_loader: Option<&'a js_config::JsConfigLoaderCb>, + ) -> Self { + if let Some(js_loader) = js_config_loader { + self.js_config_loader = Some(js_loader); + } + + self } /// 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, - self.workspace_uri, - ) - .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 }) + fn load(path: &Path) -> Result { + Oxlintrc::from_file(path) + .map_err(|error| ConfigLoadError::Parse { path: path.to_path_buf(), error }) + } + + pub fn load_js_configs( + &self, + paths: &[PathBuf], + ) -> Result, Vec> { + if paths.is_empty() { + return Ok(Vec::new()); + } + + #[cfg(not(feature = "napi"))] + { + return Err(vec![ConfigLoadError::TypeScriptConfigFileFoundButJsRuntimeNotAvailable]); + } + + #[cfg(feature = "napi")] + let Some(js_config_loader) = self.js_config_loader else { + return Err(vec![ConfigLoadError::TypeScriptConfigFileFoundButJsRuntimeNotAvailable]); + }; + + let paths_as_strings: Vec = + paths.iter().map(|p| p.to_string_lossy().to_string()).collect(); + + match js_config_loader(paths_as_strings) { + Ok(results) => Ok(results.into_iter().map(|c| c.config).collect()), + Err(diagnostics) => { + Err(diagnostics.into_iter().map(ConfigLoadError::Diagnostic).collect()) + } + } } /// Load multiple configs, returning successes and errors separately @@ -242,16 +302,93 @@ impl<'a> ConfigLoader<'a> { let mut configs = Vec::new(); let mut errors = Vec::new(); - for path in paths { - match path { - DiscoveredConfig::Json(path) => match self.load(&path) { + let mut by_dir = FxHashMap::, Option)>::default(); + + for config in paths { + match config { + DiscoveredConfig::Json(path) => { + let Some(dir) = path.parent().map(Path::to_path_buf) else { + continue; + }; + by_dir.entry(dir).or_default().0 = Some(path); + } + DiscoveredConfig::Js(path) => { + let Some(dir) = path.parent().map(Path::to_path_buf) else { + continue; + }; + by_dir.entry(dir).or_default().1 = Some(path); + } + } + } + + let mut js_configs = Vec::new(); + + for (dir, (json_path, ts_path)) in by_dir { + if json_path.is_some() && ts_path.is_some() { + errors.push(ConfigLoadError::Diagnostic(config_conflict_diagnostic(&dir))); + continue; + } + + if let Some(path) = json_path { + match Self::load(&path) { Ok(config) => configs.push(config), Err(e) => errors.push(e), - }, + } + } + + if let Some(path) = ts_path { + js_configs.push(path); } } - (configs, errors) + match self.load_js_configs(&js_configs) { + Ok(mut loaded_js_configs) => { + configs.append(&mut loaded_js_configs); + } + Err(mut js_errors) => { + errors.append(&mut js_errors); + } + } + + let mut built_configs = Vec::new(); + + for config in configs { + let path = config.path.clone(); + let dir = path.parent().unwrap().to_path_buf(); + let ignore_patterns = config.ignore_patterns.clone(); + + let builder = match ConfigStoreBuilder::from_oxlintrc( + false, + config, + self.external_linter, + self.external_plugin_store, + self.workspace_uri, + ) { + Ok(builder) => builder, + Err(e) => { + errors.push(ConfigLoadError::Build { path, error: e.to_string() }); + continue; + } + }; + + let extended_paths = builder.extended_paths.clone(); + + match builder + .with_filters(self.filters) + .build(self.external_plugin_store) + .map_err(|e| ConfigLoadError::Build { path: path.clone(), error: e.to_string() }) + { + Ok(config) => built_configs.push(LoadedConfig { + dir, + config, + ignore_patterns, + extended_paths, + }), + Err(e) => errors.push(e), + } + } + + (built_configs, errors) } pub(crate) fn load_discovered( @@ -261,6 +398,62 @@ impl<'a> ConfigLoader<'a> { self.load_many(configs) } + fn load_root_config( + &self, + cwd: &Path, + config_path: Option<&PathBuf>, + ) -> Result { + if let Some(config_path) = config_path { + let full_path = cwd.join(config_path); + if full_path.file_name() == Some(OsStr::new(DEFAULT_TS_OXLINTRC_NAME)) { + return self.load_root_ts_config(&full_path); + } + return Oxlintrc::from_file(&full_path); + } + + let json_path = cwd.join(DEFAULT_OXLINTRC_NAME); + let ts_path = cwd.join(DEFAULT_TS_OXLINTRC_NAME); + + let json_exists = json_path.is_file(); + let ts_exists = ts_path.is_file(); + + if json_exists && ts_exists { + return Err(config_conflict_diagnostic(cwd)); + } + + if ts_exists { + return self.load_root_ts_config(&ts_path); + } + + if json_exists { + return Oxlintrc::from_file(&json_path); + } + + Ok(Oxlintrc::default()) + } + + fn load_root_ts_config(&self, path: &Path) -> Result { + match self.load_js_configs(&[path.to_path_buf()]) { + Ok(mut configs) => Ok(configs.pop().unwrap_or_default()), + Err(errors) => { + if let Some(first) = errors.into_iter().next() { + match first { + ConfigLoadError::TypeScriptConfigFileFoundButJsRuntimeNotAvailable => { + Err(ts_config_not_supported_diagnostic(path)) + } + ConfigLoadError::Diagnostic(diag) => Err(diag), + // `load_js_configs` only returns the two variants above, but keep this + // resilient if that changes. + ConfigLoadError::Parse { error, .. } => Err(error), + ConfigLoadError::Build { error, .. } => Err(OxcDiagnostic::error(error)), + } + } else { + Err(OxcDiagnostic::error("Failed to load TypeScript config.")) + } + } + } + } + /// Load the root configuration and optionally discover and load nested configs. /// /// This is the main entry point for CLI config loading. It first loads the root @@ -283,7 +476,7 @@ impl<'a> ConfigLoader<'a> { paths: &[Arc], search_for_nested_configs: bool, ) -> Result { - let oxlintrc = match find_oxlint_config(cwd, config_path) { + let oxlintrc = match self.load_root_config(cwd, config_path) { Ok(config) => config, Err(err) => return Err(CliConfigLoadError::RootConfig(err)), }; @@ -345,39 +538,52 @@ pub fn build_nested_configs( nested_configs } -fn find_oxlint_config(cwd: &Path, config: Option<&PathBuf>) -> Result { - let path: &Path = config.map_or(DEFAULT_OXLINTRC_NAME.as_ref(), PathBuf::as_ref); - let full_path = cwd.join(path); +fn config_conflict_diagnostic(dir: &Path) -> OxcDiagnostic { + OxcDiagnostic::error(format!( + "Both '{}' and '{}' found in {}.", + DEFAULT_OXLINTRC_NAME, + DEFAULT_TS_OXLINTRC_NAME, + dir.display() + )) + .with_note("Only `.oxlintrc.json` or `oxlint.config.ts` are allowed, not both.") + .with_help("Delete one of the configuration files.") +} - if config.is_some() || full_path.exists() { - return Oxlintrc::from_file(&full_path); - } - Ok(Oxlintrc::default()) +fn ts_config_not_supported_diagnostic(path: &Path) -> OxcDiagnostic { + OxcDiagnostic::error(format!( + "TypeScript config files ({}) found but JS runtime not available.", + path.display() + )) + .with_help("Run oxlint via the npm package, or use JSON config files (.oxlintrc.json).") } #[cfg(test)] mod test { use std::path::PathBuf; - use super::find_oxlint_config; + use oxc_linter::ExternalPluginStore; + + use super::ConfigLoader; #[test] fn test_config_path_with_parent_references() { let cwd = std::env::current_dir().unwrap(); + let mut external_plugin_store = ExternalPluginStore::new(false); + let loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None); // Test case 1: Invalid path that should fail let invalid_config = PathBuf::from("child/../../fixtures/linter/eslintrc.json"); - let result = find_oxlint_config(&cwd, Some(&invalid_config)); + let result = loader.load_root_config(&cwd, Some(&invalid_config)); assert!(result.is_err(), "Expected config lookup to fail with invalid path"); // Test case 2: Valid path that should pass let valid_config = PathBuf::from("fixtures/linter/eslintrc.json"); - let result = find_oxlint_config(&cwd, Some(&valid_config)); + let result = loader.load_root_config(&cwd, Some(&valid_config)); assert!(result.is_ok(), "Expected config lookup to succeed with valid path"); // Test case 3: Valid path using parent directory (..) syntax that should pass let valid_parent_config = PathBuf::from("fixtures/linter/../linter/eslintrc.json"); - let result = find_oxlint_config(&cwd, Some(&valid_parent_config)); + let result = loader.load_root_config(&cwd, Some(&valid_parent_config)); assert!(result.is_ok(), "Expected config lookup to succeed with parent directory syntax"); // Verify the resolved path is correct diff --git a/apps/oxlint/src/js_config.rs b/apps/oxlint/src/js_config.rs new file mode 100644 index 0000000000000..b40cb857af2f1 --- /dev/null +++ b/apps/oxlint/src/js_config.rs @@ -0,0 +1,130 @@ +use serde::Deserialize; +use std::path::PathBuf; + +use oxc_diagnostics::OxcDiagnostic; +use oxc_linter::Oxlintrc; + +use crate::run::JsLoadJsConfigsCb; + +/// Callback type for loading JavaScript/TypeScript config files. +pub type JsConfigLoaderCb = + Box) -> Result, Vec> + Send + Sync>; + +/// Result of loading a single JavaScript/TypeScript config file. +#[derive(Debug, Clone)] +pub struct JsConfigResult { + pub path: PathBuf, + pub config: Oxlintrc, +} + +/// Response from JS side when loading JS configs. +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum LoadJsConfigsResponse { + Success { + #[serde(rename = "Success")] + success: Vec, + }, + Failure { + #[serde(rename = "Failures")] + failures: Vec, + }, + Error { + #[serde(rename = "Error")] + error: String, + }, +} + +#[derive(Debug, Deserialize)] +struct LoadJsConfigsResponseFailure { + path: String, + error: String, +} + +#[derive(Debug, Deserialize)] +struct JsConfigResultJson { + path: String, + config: serde_json::Value, +} + +/// Create a JS config loader callback from the JS callback. +/// +/// The returned function blocks the current thread until the JS callback resolves. +/// It will panic if called outside of a Tokio runtime. +pub fn create_js_config_loader(cb: JsLoadJsConfigsCb) -> JsConfigLoaderCb { + Box::new(move |paths: Vec| { + let cb = &cb; + let res = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(async move { cb.call_async(paths).await?.into_future().await }) + }); + + match res { + Ok(json) => parse_js_config_response(&json), + Err(err) => { + Err(vec![OxcDiagnostic::error(format!("`loadJsConfigs` threw an error: {err}"))]) + } + } + }) +} + +/// Parse the JSON response from JS side into `JsConfigResult` structs. +fn parse_js_config_response(json: &str) -> Result, Vec> { + let response: LoadJsConfigsResponse = serde_json::from_str(json).map_err(|e| { + vec![OxcDiagnostic::error(format!("Failed to parse JS config response: {e}"))] + })?; + + match response { + LoadJsConfigsResponse::Success { success } => { + let count = success.len(); + let (configs, errors) = success.into_iter().fold( + (Vec::with_capacity(count), Vec::new()), + |(mut configs, mut errors), entry| { + let path = PathBuf::from(&entry.path); + let mut oxlintrc: Oxlintrc = match serde_json::from_value(entry.config) { + Ok(config) => config, + Err(err) => { + debug_assert!(false, "All JS configs should be valid JSON"); + errors.push( + OxcDiagnostic::error(format!( + "Failed to parse config from {}", + entry.path + )) + .with_note(err.to_string()), + ); + return (configs, errors); + } + }; + + // Check if extends is used - not yet supported + if !oxlintrc.extends.is_empty() { + errors.push(OxcDiagnostic::error(format!( + "`extends` in JavaScript configs is not yet supported (found in {})", + entry.path + ))); + return (configs, errors); + } + + oxlintrc.path.clone_from(&path); + configs.push(JsConfigResult { path, config: oxlintrc }); + + (configs, errors) + }, + ); + + if errors.is_empty() { Ok(configs) } else { Err(errors) } + } + LoadJsConfigsResponse::Failure { failures } => Err(failures + .into_iter() + .map(|failure| { + OxcDiagnostic::error(format!( + "Failed to load config: {}\n\n{}", + failure.path, failure.error + )) + }) + .collect()), + LoadJsConfigsResponse::Error { error } => { + Err(vec![OxcDiagnostic::error(format!("Failed to load config files:\n\n{error}"))]) + } + } +} diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs index 93f7e6af24e76..e4a34ed757df5 100644 --- a/apps/oxlint/src/lib.rs +++ b/apps/oxlint/src/lib.rs @@ -23,6 +23,8 @@ pub mod cli { // Without this, `tasks/website` will not compile on Linux or Windows. // `tasks/website` depends on `oxlint` as a normal library, which causes linker errors if NAPI is enabled. #[cfg(feature = "napi")] +mod js_config; +#[cfg(feature = "napi")] mod run; #[cfg(feature = "napi")] pub use run::*; @@ -49,6 +51,7 @@ mod js_plugins; static GLOBAL: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc; const DEFAULT_OXLINTRC_NAME: &str = ".oxlintrc.json"; +const DEFAULT_TS_OXLINTRC_NAME: &str = "oxlint.config.ts"; /// Return a JSON blob containing metadata for all available oxlint rules. /// diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index 45ae7c26d36d9..67aac1b3bb2d1 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -1,6 +1,7 @@ use std::{ env, ffi::OsStr, + fmt::Debug, io::{ErrorKind, Write}, path::{Path, PathBuf, absolute}, sync::Arc, @@ -16,6 +17,8 @@ use oxc_linter::{ InvalidFilterKind, LintFilter, LintOptions, LintRunner, LintServiceOptions, Linter, }; +#[cfg(feature = "napi")] +use crate::js_config::JsConfigLoaderCb; use crate::{ cli::{CliRunResult, LintCommand, MiscOptions, ReportUnusedDirectives, WarningOptions}, config_loader::{CliConfigLoadError, ConfigLoadError, ConfigLoader}, @@ -24,11 +27,30 @@ use crate::{ }; use oxc_linter::LintIgnoreMatcher; -#[derive(Debug)] pub struct CliRunner { options: LintCommand, cwd: PathBuf, external_linter: Option, + /// Callback for loading JavaScript/TypeScript config files (experimental). + /// This is only available when running via Node.js with NAPI. + #[cfg(feature = "napi")] + js_config_loader: Option, +} + +impl Debug for CliRunner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut s = f.debug_struct("CliRunner"); + s.field("options", &self.options).field("cwd", &self.cwd).field( + "external_linter", + if self.external_linter.is_some() { &"Some(ExternalLinter)" } else { &"None" }, + ); + #[cfg(feature = "napi")] + s.field( + "js_config_loader", + if self.js_config_loader.is_some() { &"Some(JsConfigLoaderCb)" } else { &"None" }, + ); + s.finish() + } } impl CliRunner { @@ -38,6 +60,8 @@ impl CliRunner { options, cwd: env::current_dir().expect("Failed to get current working directory"), external_linter, + #[cfg(feature = "napi")] + js_config_loader: None, } } @@ -183,6 +207,10 @@ impl CliRunner { let config_result = { let mut config_loader = ConfigLoader::new(external_linter, &mut external_plugin_store, &filters, None); + #[cfg(feature = "napi")] + { + config_loader = config_loader.with_js_config_loader(self.js_config_loader.as_ref()); + } config_loader.load_root_and_nested( &self.cwd, basic_options.config.as_ref(), @@ -224,6 +252,15 @@ impl CliRunner { ) ) } + ConfigLoadError::TypeScriptConfigFileFoundButJsRuntimeNotAvailable => { + "Error: TypeScript config files (oxlint.config.ts) found but JS runtime not available.\n\ + This is an experimental feature that requires running oxlint via Node.js.\n\ + Please use JSON config files (.oxlintrc.json) instead, or run oxlint via the npm package.\n".to_string() + } + ConfigLoadError::Diagnostic(error) => { + let report = render_report(&handler, error); + format!("Failed to parse oxlint configuration file.\n{report}\n") + } }; print_and_flush_stdout(stdout, &message); } @@ -416,6 +453,13 @@ impl CliRunner { self } + #[cfg(feature = "napi")] + #[must_use] + pub fn with_config_loader(mut self, config_loader: Option) -> Self { + self.js_config_loader = config_loader; + self + } + fn get_diagnostic_service( reporter: &OutputFormatter, warning_options: &WarningOptions, diff --git a/apps/oxlint/src/lsp/server_linter.rs b/apps/oxlint/src/lsp/server_linter.rs index a020499a4cc05..4543b2cb91fb4 100644 --- a/apps/oxlint/src/lsp/server_linter.rs +++ b/apps/oxlint/src/lsp/server_linter.rs @@ -327,7 +327,11 @@ impl ServerLinterBuilder { let (configs, errors) = loader.load_discovered(config_paths); for error in errors { - warn!("Skipping config file {}: {:?}", error.path().display(), error); + if let Some(path) = error.path() { + warn!("Skipping config file {}: {:?}", path.display(), error); + } else { + warn!("Skipping config file: {:?}", error); + } } build_nested_configs(configs, nested_ignore_patterns, Some(extended_paths)) diff --git a/apps/oxlint/src/run.rs b/apps/oxlint/src/run.rs index b50d8b9048529..41b712a0b5915 100644 --- a/apps/oxlint/src/run.rs +++ b/apps/oxlint/src/run.rs @@ -125,6 +125,7 @@ pub type JsLoadJsConfigsCb = ThreadsafeFunction< // CalleeHandled false, >; + /// NAPI entry point. /// /// JS side passes in: @@ -196,8 +197,8 @@ async fn lint_impl( // JS plugins are only supported on 64-bit little-endian platforms at present #[cfg(all(target_pointer_width = "64", target_endian = "little"))] - let (external_linter, _) = { - let js_config_loader = Some(load_js_configs); + let (external_linter, js_config_loader) = { + let js_config_loader = Some(crate::js_config::create_js_config_loader(load_js_configs)); let external_linter = Some(crate::js_plugins::create_external_linter( load_plugin, setup_rule_configs, @@ -208,7 +209,7 @@ async fn lint_impl( (external_linter, js_config_loader) }; #[cfg(not(all(target_pointer_width = "64", target_endian = "little")))] - let (external_linter, _) = { + let (external_linter, js_config_loader) = { let (_, _, _, _, _, _) = ( load_plugin, setup_rule_configs, @@ -217,7 +218,7 @@ async fn lint_impl( destroy_workspace, load_js_configs, ); - (None, None::<()>) + (None, None) }; // If --lsp flag is set, run the language server @@ -238,7 +239,13 @@ async fn lint_impl( // See `https://github.com/rust-lang/rust/issues/60673`. let mut stdout = BufWriter::new(std::io::stdout()); - CliRunner::new(command, external_linter).run(&mut stdout) + let mut cli_runner = CliRunner::new(command, external_linter); + #[cfg(feature = "napi")] + { + cli_runner = cli_runner.with_config_loader(js_config_loader); + } + + cli_runner.run(&mut stdout) } #[cfg(all(target_pointer_width = "64", target_endian = "little"))] diff --git a/apps/oxlint/test/fixtures/js_config_basic/files/test.js b/apps/oxlint/test/fixtures/js_config_basic/files/test.js new file mode 100644 index 0000000000000..d4e2f029dd793 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_basic/files/test.js @@ -0,0 +1,3 @@ +debugger; +if (x == 1) { +} diff --git a/apps/oxlint/test/fixtures/js_config_basic/output.snap.md b/apps/oxlint/test/fixtures/js_config_basic/output.snap.md new file mode 100644 index 0000000000000..da3555aad9bcc --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_basic/output.snap.md @@ -0,0 +1,29 @@ +# Exit code +1 + +# stdout +``` + x eslint(no-debugger): `debugger` statement is not allowed + ,-[files/test.js:1:1] + 1 | debugger; + : ^^^^^^^^^ + 2 | if (x == 1) { + `---- + help: Remove the debugger statement + + ! eslint(eqeqeq): Expected === and instead saw == + ,-[files/test.js:2:7] + 1 | debugger; + 2 | if (x == 1) { + : ^^ + 3 | } + `---- + help: Prefer === operator + +Found 1 warning and 1 error. +Finished in Xms on 1 file with 92 rules using X threads. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_basic/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_basic/oxlint.config.ts new file mode 100644 index 0000000000000..eb3b55a5f1e73 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_basic/oxlint.config.ts @@ -0,0 +1,7 @@ +// Basic test for oxlint.config.ts support +export default { + rules: { + "no-debugger": "error", + eqeqeq: "warn", + }, +}; diff --git a/apps/oxlint/test/fixtures/js_config_conflict/.oxlintrc.json b/apps/oxlint/test/fixtures/js_config_conflict/.oxlintrc.json new file mode 100644 index 0000000000000..a63d73a1442b8 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_conflict/.oxlintrc.json @@ -0,0 +1,5 @@ +{ + "rules": { + "no-debugger": "error" + } +} diff --git a/apps/oxlint/test/fixtures/js_config_conflict/files/test.js b/apps/oxlint/test/fixtures/js_config_conflict/files/test.js new file mode 100644 index 0000000000000..eab74692130a6 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_conflict/files/test.js @@ -0,0 +1 @@ +debugger; diff --git a/apps/oxlint/test/fixtures/js_config_conflict/options.json b/apps/oxlint/test/fixtures/js_config_conflict/options.json new file mode 100644 index 0000000000000..c6d966f1b525b --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_conflict/options.json @@ -0,0 +1,3 @@ +{ + "singleThread": true +} diff --git a/apps/oxlint/test/fixtures/js_config_conflict/output.snap.md b/apps/oxlint/test/fixtures/js_config_conflict/output.snap.md new file mode 100644 index 0000000000000..ee67c57e0d40e --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_conflict/output.snap.md @@ -0,0 +1,15 @@ +# Exit code +1 + +# stdout +``` +Failed to parse oxlint configuration file. + + x Both '.oxlintrc.json' and 'oxlint.config.ts' found in /js_config_conflict. + help: Delete one of the configuration files. + note: Only `.oxlintrc.json` or `oxlint.config.ts` are allowed, not both. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_conflict/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_conflict/oxlint.config.ts new file mode 100644 index 0000000000000..f4ee575beaa93 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_conflict/oxlint.config.ts @@ -0,0 +1,5 @@ +export default { + rules: { + "no-debugger": "error", + }, +}; diff --git a/apps/oxlint/test/fixtures/js_config_invalid_export/files/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_invalid_export/files/oxlint.config.ts new file mode 100644 index 0000000000000..d6d1738de67ec --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_invalid_export/files/oxlint.config.ts @@ -0,0 +1 @@ +export default []; diff --git a/apps/oxlint/test/fixtures/js_config_invalid_export/files/test.js b/apps/oxlint/test/fixtures/js_config_invalid_export/files/test.js new file mode 100644 index 0000000000000..d914c6066c433 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_invalid_export/files/test.js @@ -0,0 +1 @@ +console.log("hi"); diff --git a/apps/oxlint/test/fixtures/js_config_invalid_export/output.snap.md b/apps/oxlint/test/fixtures/js_config_invalid_export/output.snap.md new file mode 100644 index 0000000000000..88eba1eaf8b5b --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_invalid_export/output.snap.md @@ -0,0 +1,15 @@ +# Exit code +1 + +# stdout +``` +Failed to parse oxlint configuration file. + + x Failed to load config: /oxlint.config.ts + | + | Error: Configuration file must have a default export that is an object. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_invalid_export/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_invalid_export/oxlint.config.ts new file mode 100644 index 0000000000000..38f5d5f312083 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_invalid_export/oxlint.config.ts @@ -0,0 +1 @@ +export default "nope"; diff --git a/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/.oxlintrc.json b/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/.oxlintrc.json new file mode 100644 index 0000000000000..5f229f5ea27f5 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/.oxlintrc.json @@ -0,0 +1 @@ +{ "rules": {} } diff --git a/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/oxlint.config.ts new file mode 100644 index 0000000000000..08717a09a7300 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/oxlint.config.ts @@ -0,0 +1 @@ +export default { rules: {} }; diff --git a/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/test.js b/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/test.js new file mode 100644 index 0000000000000..b506100a909fb --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_nested_conflict/files/nested/test.js @@ -0,0 +1 @@ +var x = 1; diff --git a/apps/oxlint/test/fixtures/js_config_nested_conflict/output.snap.md b/apps/oxlint/test/fixtures/js_config_nested_conflict/output.snap.md new file mode 100644 index 0000000000000..49644b5daeb01 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_nested_conflict/output.snap.md @@ -0,0 +1,15 @@ +# Exit code +1 + +# stdout +``` +Failed to parse oxlint configuration file. + + x Both '.oxlintrc.json' and 'oxlint.config.ts' found in /files/nested. + help: Delete one of the configuration files. + note: Only `.oxlintrc.json` or `oxlint.config.ts` are allowed, not both. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_overrides/files/test.js b/apps/oxlint/test/fixtures/js_config_overrides/files/test.js new file mode 100644 index 0000000000000..a5965e251c6c7 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_overrides/files/test.js @@ -0,0 +1,2 @@ +// no-debugger is off for JS files +debugger; diff --git a/apps/oxlint/test/fixtures/js_config_overrides/files/test.ts b/apps/oxlint/test/fixtures/js_config_overrides/files/test.ts new file mode 100644 index 0000000000000..0cfd8c797b875 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_overrides/files/test.ts @@ -0,0 +1,2 @@ +// no-debugger is error for TS files +debugger; diff --git a/apps/oxlint/test/fixtures/js_config_overrides/output.snap.md b/apps/oxlint/test/fixtures/js_config_overrides/output.snap.md new file mode 100644 index 0000000000000..767886ec68440 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_overrides/output.snap.md @@ -0,0 +1,20 @@ +# Exit code +1 + +# stdout +``` + x eslint(no-debugger): `debugger` statement is not allowed + ,-[files/test.ts:2:1] + 1 | // no-debugger is error for TS files + 2 | debugger; + : ^^^^^^^^^ + `---- + help: Remove the debugger statement + +Found 0 warnings and 1 error. +Finished in Xms on 2 files with 90 rules using X threads. +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_overrides/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_overrides/oxlint.config.ts new file mode 100644 index 0000000000000..6bac6874e56e0 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_overrides/oxlint.config.ts @@ -0,0 +1,14 @@ +// Test that overrides work in oxlint.config.ts +export default { + rules: { + "no-debugger": "off", + }, + overrides: [ + { + files: ["*.ts"], + rules: { + "no-debugger": "error", + }, + }, + ], +}; diff --git a/apps/oxlint/test/fixtures/js_config_throws_err/files/index.js b/apps/oxlint/test/fixtures/js_config_throws_err/files/index.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/apps/oxlint/test/fixtures/js_config_throws_err/output.snap.md b/apps/oxlint/test/fixtures/js_config_throws_err/output.snap.md new file mode 100644 index 0000000000000..815872f706b75 --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_throws_err/output.snap.md @@ -0,0 +1,16 @@ +# Exit code +1 + +# stdout +``` +Failed to parse oxlint configuration file. + + x Failed to load config: /oxlint.config.ts + | + | Error: This is a test error + | at /oxlint.config.ts:1:7 +``` + +# stderr +``` +``` diff --git a/apps/oxlint/test/fixtures/js_config_throws_err/oxlint.config.ts b/apps/oxlint/test/fixtures/js_config_throws_err/oxlint.config.ts new file mode 100644 index 0000000000000..ddd4800bd9c4a --- /dev/null +++ b/apps/oxlint/test/fixtures/js_config_throws_err/oxlint.config.ts @@ -0,0 +1,3 @@ +throw new Error("This is a test error"); + +export default {};