diff --git a/.cargo/config.toml b/.cargo/config.toml index c443f6b872210..010f8a8b06520 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -42,3 +42,11 @@ rustflags = [ # See https://x.com/Brooooook_lyn/status/1895848334692401270 [target.'cfg(target_env = "gnu")'] rustflags = ["-C", "link-args=-Wl,-z,nodelete"] + +[target.'cfg(all(target_os = "linux", target_env = "gnu"))'] +rustflags = [ + "-C", + "link-args=-Wl,--warn-unresolved-symbols", + "-C", + "link-args=-Wl,-z,nodelete", +] diff --git a/Cargo.lock b/Cargo.lock index f8e4ea75910cb..da534a60681ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1855,6 +1855,8 @@ dependencies = [ "lazy_static", "markdown", "memchr", + "napi", + "napi-derive", "nonmax", "oxc-schemars", "oxc_allocator", diff --git a/apps/oxlint/src/lib.rs b/apps/oxlint/src/lib.rs index 1a3c60bd8e598..6185b17dc9de1 100644 --- a/apps/oxlint/src/lib.rs +++ b/apps/oxlint/src/lib.rs @@ -10,6 +10,8 @@ pub mod cli { pub use crate::{command::*, lint::LintRunner, result::CliRunResult, runner::Runner}; } +pub use oxc_linter::{ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb}; + #[cfg(all(feature = "allocator", not(miri), not(target_family = "wasm")))] #[global_allocator] static GLOBAL: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc; @@ -17,7 +19,7 @@ static GLOBAL: mimalloc_safe::MiMalloc = mimalloc_safe::MiMalloc; use cli::{CliRunResult, LintRunner, Runner}; use std::{ffi::OsStr, io::BufWriter}; -pub fn lint() -> CliRunResult { +pub fn lint(external_linter: Option) -> CliRunResult { init_tracing(); init_miette(); @@ -49,7 +51,7 @@ pub fn lint() -> CliRunResult { // See `https://github.com/rust-lang/rust/issues/60673`. let mut stdout = BufWriter::new(std::io::stdout()); - LintRunner::new(command).run(&mut stdout) + LintRunner::new(command, external_linter).run(&mut stdout) } // Initialize the data which relies on `is_atty` system calls so they don't block subsequent threads. diff --git a/apps/oxlint/src/lint.rs b/apps/oxlint/src/lint.rs index d3a33619b052a..87431f9fe1685 100644 --- a/apps/oxlint/src/lint.rs +++ b/apps/oxlint/src/lint.rs @@ -13,8 +13,8 @@ use ignore::{gitignore::Gitignore, overrides::OverrideBuilder}; use oxc_allocator::AllocatorPool; use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler, OxcDiagnostic}; use oxc_linter::{ - AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, InvalidFilterKind, LintFilter, - LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc, + AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, InvalidFilterKind, + LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc, }; use rustc_hash::{FxHashMap, FxHashSet}; use serde_json::Value; @@ -29,13 +29,18 @@ use crate::{ pub struct LintRunner { options: LintCommand, cwd: PathBuf, + external_linter: Option, } impl Runner for LintRunner { type Options = LintCommand; - fn new(options: Self::Options) -> Self { - Self { options, cwd: env::current_dir().expect("Failed to get current working directory") } + fn new(options: Self::Options, external_linter: Option) -> Self { + Self { + options, + cwd: env::current_dir().expect("Failed to get current working directory"), + external_linter, + } } fn run(self, stdout: &mut dyn Write) -> CliRunResult { @@ -63,6 +68,8 @@ impl Runner for LintRunner { .. } = self.options; + let external_linter = self.external_linter.as_ref(); + let search_for_nested_configs = !disable_nested_config && // If the `--config` option is explicitly passed, we should not search for nested config files // as the passed config file takes absolute precedence. @@ -173,7 +180,7 @@ impl Runner for LintRunner { let handler = GraphicalReportHandler::new(); let nested_configs = if search_for_nested_configs { - match Self::get_nested_configs(stdout, &handler, &filters, &paths) { + match Self::get_nested_configs(stdout, &handler, &filters, &paths, external_linter) { Ok(v) => v, Err(v) => return v, } @@ -192,20 +199,21 @@ impl Runner for LintRunner { } else { None }; - let config_builder = match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc) { - Ok(builder) => builder, - Err(e) => { - print_and_flush_stdout( - stdout, - &format!( - "Failed to parse configuration file.\n{}\n", - render_report(&handler, &OxcDiagnostic::error(e.to_string())) - ), - ); - return CliRunResult::InvalidOptionConfig; + let config_builder = + match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, external_linter) { + Ok(builder) => builder, + Err(e) => { + print_and_flush_stdout( + stdout, + &format!( + "Failed to parse configuration file.\n{}\n", + render_report(&handler, &OxcDiagnostic::error(e.to_string())) + ), + ); + return CliRunResult::InvalidOptionConfig; + } } - } - .with_filters(&filters); + .with_filters(&filters); if let Some(basic_config_file) = oxlintrc_for_print { let config_file = config_builder.resolve_final_config_file(basic_config_file); @@ -387,6 +395,7 @@ impl LintRunner { handler: &GraphicalReportHandler, filters: &Vec, paths: &Vec>, + external_linter: Option<&ExternalLinter>, ) -> Result, 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. @@ -417,7 +426,8 @@ impl LintRunner { // iterate over each config and build the ConfigStore for (dir, oxlintrc) in nested_oxlintrc { // TODO(refactor): clean up all of the error handling in this function - let builder = match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc) { + let builder = match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, external_linter) + { Ok(builder) => builder, Err(e) => { print_and_flush_stdout( diff --git a/apps/oxlint/src/main.rs b/apps/oxlint/src/main.rs index ef4f2fc998ee2..a1da54bc4cb60 100644 --- a/apps/oxlint/src/main.rs +++ b/apps/oxlint/src/main.rs @@ -1,5 +1,5 @@ use oxlint::{cli::CliRunResult, lint}; fn main() -> CliRunResult { - lint() + lint(None) } diff --git a/apps/oxlint/src/runner.rs b/apps/oxlint/src/runner.rs index 2c48cd2e02280..5ee82c5689a1f 100644 --- a/apps/oxlint/src/runner.rs +++ b/apps/oxlint/src/runner.rs @@ -1,12 +1,14 @@ use std::io::Write; +use oxc_linter::ExternalLinter; + use crate::cli::CliRunResult; /// A trait for exposing functionality to the CLI. pub trait Runner { type Options; - fn new(matches: Self::Options) -> Self; + fn new(matches: Self::Options, external_linter: Option) -> Self; /// Executes the runner, providing some result to the CLI. fn run(self, stdout: &mut dyn Write) -> CliRunResult; diff --git a/apps/oxlint/src/tester.rs b/apps/oxlint/src/tester.rs index b922bc04db6be..6ee4c5413150a 100644 --- a/apps/oxlint/src/tester.rs +++ b/apps/oxlint/src/tester.rs @@ -37,7 +37,7 @@ impl Tester { let options = lint_command().run_inner(new_args.as_slice()).unwrap(); let mut output = Vec::new(); - let _ = LintRunner::new(options).with_cwd(self.cwd.clone()).run(&mut output); + let _ = LintRunner::new(options, None).with_cwd(self.cwd.clone()).run(&mut output); } pub fn test_and_snapshot(&self, args: &[&str]) { @@ -59,7 +59,7 @@ impl Tester { format!("working directory: {}\n", relative_dir.to_str().unwrap()).as_bytes(), ); output.extend_from_slice(b"----------\n"); - let result = LintRunner::new(options).with_cwd(self.cwd.clone()).run(&mut output); + let result = LintRunner::new(options, None).with_cwd(self.cwd.clone()).run(&mut output); output.extend_from_slice(b"----------\n"); output.extend_from_slice(format!("CLI result: {result:?}\n").as_bytes()); diff --git a/crates/oxc_language_server/src/linter/server_linter.rs b/crates/oxc_language_server/src/linter/server_linter.rs index eb677494de3ab..42b763ca9967e 100644 --- a/crates/oxc_language_server/src/linter/server_linter.rs +++ b/crates/oxc_language_server/src/linter/server_linter.rs @@ -54,7 +54,7 @@ impl ServerLinter { // clone because we are returning it for ignore builder let config_builder = - ConfigStoreBuilder::from_oxlintrc(false, oxlintrc.clone()).unwrap_or_default(); + ConfigStoreBuilder::from_oxlintrc(false, oxlintrc.clone(), None).unwrap_or_default(); // TODO(refactor): pull this into a shared function, because in oxlint we have the same functionality. let use_nested_config = options.use_nested_configs(); @@ -131,7 +131,7 @@ impl ServerLinter { warn!("Skipping invalid config file: {}", file_path.display()); continue; }; - let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc) + let Ok(config_store_builder) = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None) else { warn!("Skipping config (builder failed): {}", file_path.display()); continue; diff --git a/crates/oxc_linter/Cargo.toml b/crates/oxc_linter/Cargo.toml index 2c09fd630314d..70963c1620a11 100644 --- a/crates/oxc_linter/Cargo.toml +++ b/crates/oxc_linter/Cargo.toml @@ -57,6 +57,8 @@ language-tags = { workspace = true } lazy-regex = { workspace = true } lazy_static = { workspace = true } memchr = { workspace = true } +napi = { workspace = true } +napi-derive = { workspace = true } nonmax = { workspace = true } phf = { workspace = true, features = ["macros"] } rayon = { workspace = true } diff --git a/crates/oxc_linter/src/config/config_builder.rs b/crates/oxc_linter/src/config/config_builder.rs index e71b52c3c8380..49d8676fb083b 100644 --- a/crates/oxc_linter/src/config/config_builder.rs +++ b/crates/oxc_linter/src/config/config_builder.rs @@ -11,6 +11,7 @@ use oxc_span::{CompactStr, format_compact_str}; use crate::{ AllowWarnDeny, LintConfig, LintFilter, LintFilterKind, Oxlintrc, RuleCategory, RuleEnum, config::{ESLintRule, LintPlugins, OxlintOverrides, OxlintRules, overrides::OxlintOverride}, + external_linter::ExternalLinter, rules::RULES, }; @@ -83,6 +84,7 @@ impl ConfigStoreBuilder { pub fn from_oxlintrc( start_empty: bool, oxlintrc: Oxlintrc, + _external_linter: Option<&ExternalLinter>, ) -> Result { // TODO: this can be cached to avoid re-computing the same oxlintrc fn resolve_oxlintrc_config( @@ -375,7 +377,7 @@ impl TryFrom for ConfigStoreBuilder { #[inline] fn try_from(oxlintrc: Oxlintrc) -> Result { - Self::from_oxlintrc(false, oxlintrc) + Self::from_oxlintrc(false, oxlintrc, None) } } @@ -640,7 +642,7 @@ mod test { "#, ) .unwrap(); - let builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc).unwrap(); + let builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None).unwrap(); for (rule, severity) in &builder.rules { let name = rule.name(); let plugin = rule.plugin_name(); @@ -815,6 +817,7 @@ mod test { "fixtures/extends_config/extends_invalid_config.json", )) .unwrap(), + None, ); let err = invalid_config.unwrap_err(); assert!(matches!(err, ConfigBuilderError::InvalidConfigFile { .. })); @@ -943,12 +946,18 @@ mod test { } fn config_store_from_path(path: &str) -> Config { - ConfigStoreBuilder::from_oxlintrc(true, Oxlintrc::from_file(&PathBuf::from(path)).unwrap()) - .unwrap() - .build() + ConfigStoreBuilder::from_oxlintrc( + true, + Oxlintrc::from_file(&PathBuf::from(path)).unwrap(), + None, + ) + .unwrap() + .build() } fn config_store_from_str(s: &str) -> Config { - ConfigStoreBuilder::from_oxlintrc(true, serde_json::from_str(s).unwrap()).unwrap().build() + ConfigStoreBuilder::from_oxlintrc(true, serde_json::from_str(s).unwrap(), None) + .unwrap() + .build() } } diff --git a/crates/oxc_linter/src/external_linter.rs b/crates/oxc_linter/src/external_linter.rs new file mode 100644 index 0000000000000..53769f4ee2c39 --- /dev/null +++ b/crates/oxc_linter/src/external_linter.rs @@ -0,0 +1,37 @@ +use std::{fmt::Debug, sync::Arc}; + +use napi::{Status, bindgen_prelude::Promise, threadsafe_function::ThreadsafeFunction}; +use napi_derive::napi; + +#[napi] +pub type ExternalLinterCb = + Arc>; + +#[napi] +pub type ExternalLinterLoadPluginCb = + Arc, String, Status, false>>; + +#[napi] +pub enum PluginLoadResult { + Success, + Failure(String), +} + +#[derive(Clone)] +#[expect(dead_code)] +pub struct ExternalLinter { + pub(crate) load_plugin: ExternalLinterLoadPluginCb, + pub(crate) run: ExternalLinterCb, +} + +impl ExternalLinter { + pub fn new(run: ExternalLinterCb, load_plugin: ExternalLinterLoadPluginCb) -> Self { + ExternalLinter { load_plugin, run } + } +} + +impl Debug for ExternalLinter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ExternalLinter").finish() + } +} diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index f3c27a80e2de7..dd83afb192efe 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -8,6 +8,7 @@ mod ast_util; mod config; mod context; mod disable_directives; +mod external_linter; mod fixer; mod frameworks; mod globals; @@ -32,6 +33,9 @@ pub use crate::{ Oxlintrc, }, context::LintContext, + external_linter::{ + ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb, PluginLoadResult, + }, fixer::FixKind, frameworks::FrameworkFlags, loader::LINTABLE_EXTENSIONS, diff --git a/crates/oxc_linter/src/tester.rs b/crates/oxc_linter/src/tester.rs index fa288911f70b2..6ef7ef0a6880d 100644 --- a/crates/oxc_linter/src/tester.rs +++ b/crates/oxc_linter/src/tester.rs @@ -507,8 +507,12 @@ impl Tester { eslint_config .map_or_else(ConfigStoreBuilder::empty, |mut v| { v.as_object_mut().unwrap().insert("categories".into(), json!({})); - ConfigStoreBuilder::from_oxlintrc(true, Oxlintrc::deserialize(v).unwrap()) - .unwrap() + ConfigStoreBuilder::from_oxlintrc( + true, + Oxlintrc::deserialize(v).unwrap(), + None, + ) + .unwrap() }) .with_plugins(self.plugins.union(LintPlugins::from(self.plugin_name))) .with_rule(rule, AllowWarnDeny::Warn) diff --git a/napi/oxlint2/src/bindings.d.ts b/napi/oxlint2/src/bindings.d.ts index 5f0429ceb3aa3..1656abcdfa6e8 100644 --- a/napi/oxlint2/src/bindings.d.ts +++ b/napi/oxlint2/src/bindings.d.ts @@ -1,3 +1,12 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ -export declare function lint(): Promise +export type ExternalLinterCb = + (() => void) + +export type ExternalLinterLoadPluginCb = + ((arg: string) => Promise) + +export type PluginLoadResult = + | { type: 'Success' } + | { type: 'Failure', field0: string } +export declare function lint(loadPlugin: ExternalLinterLoadPluginCb, run: ExternalLinterCb): Promise diff --git a/napi/oxlint2/src/index.js b/napi/oxlint2/src/index.js index 8cca7f8fb33fa..81e52383298f4 100644 --- a/napi/oxlint2/src/index.js +++ b/napi/oxlint2/src/index.js @@ -2,8 +2,16 @@ import { lint } from './bindings.js'; class Linter { run() { - return lint(); + return lint(this.loadPlugin.bind(this), this.lint.bind(this)); } + + loadPlugin = async (_pluginName) => { + throw new Error('unimplemented'); + }; + + lint = async () => { + throw new Error('unimplemented'); + }; } async function main() { diff --git a/napi/oxlint2/src/lib.rs b/napi/oxlint2/src/lib.rs index c4c8eeeaf3efa..1f3c1cff18200 100644 --- a/napi/oxlint2/src/lib.rs +++ b/napi/oxlint2/src/lib.rs @@ -3,10 +3,11 @@ use std::process::{ExitCode, Termination}; use napi_derive::napi; use oxlint::lint as oxlint_lint; +pub use oxlint::{ExternalLinter, ExternalLinterCb, ExternalLinterLoadPluginCb}; #[expect(clippy::allow_attributes)] #[allow(clippy::trailing_empty_array, clippy::unused_async)] // https://github.com/napi-rs/napi-rs/issues/2758 #[napi] -pub async fn lint() -> bool { - oxlint_lint().report() == ExitCode::SUCCESS +pub async fn lint(load_plugin: ExternalLinterLoadPluginCb, run: ExternalLinterCb) -> bool { + oxlint_lint(Some(ExternalLinter::new(run, load_plugin))).report() == ExitCode::SUCCESS } diff --git a/napi/playground/index.d.ts b/napi/playground/index.d.ts index 65d14bc436e6b..f97e5c64ae0cf 100644 --- a/napi/playground/index.d.ts +++ b/napi/playground/index.d.ts @@ -1,5 +1,14 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ +export type ExternalLinterCb = + (() => void) + +export type ExternalLinterLoadPluginCb = + ((arg: string) => Promise) + +export type PluginLoadResult = + | { type: 'Success' } + | { type: 'Failure', field0: string } export interface Comment { type: 'Line' | 'Block' value: string diff --git a/napi/playground/src/lib.rs b/napi/playground/src/lib.rs index e1a7b47923a90..d4adca43319ba 100644 --- a/napi/playground/src/lib.rs +++ b/napi/playground/src/lib.rs @@ -291,7 +291,7 @@ impl Oxc { Oxlintrc::from_string(&linter_options.config.as_ref().unwrap().to_string()) .unwrap_or_default(); let config_builder = - ConfigStoreBuilder::from_oxlintrc(false, oxlintrc).unwrap_or_default(); + ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None).unwrap_or_default(); config_builder.build() } else { ConfigStoreBuilder::default().build()