From 4e4136f8a8dc9f7dd4b3101ccccce5b404b33813 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:59:34 +0000 Subject: [PATCH 1/4] Initial plan From ca5a9205e9db02587fa18cb891a2934079af6086 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:35:23 +0000 Subject: [PATCH 2/4] Add clearLoadedPlugin function for NAPI integration - Add clearLoadedPlugin() JS function in src-js/plugins/load.ts to clear registeredRules and registeredPluginPaths - Export clearLoadedPlugin from src-js/plugins/index.ts and src-js/index.ts - Add tests for clearLoadedPlugin functionality - Function will be used by language server to reset plugin state Co-authored-by: Sysix <3897725+Sysix@users.noreply.github.com> --- apps/oxlint/src-js/index.ts | 16 +++++++++++++ apps/oxlint/src-js/plugins/index.ts | 4 ++-- apps/oxlint/src-js/plugins/load.ts | 12 ++++++++++ apps/oxlint/src/run.rs | 2 ++ apps/oxlint/test/clear-loaded-plugin.test.ts | 24 ++++++++++++++++++++ 5 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 apps/oxlint/test/clear-loaded-plugin.test.ts diff --git a/apps/oxlint/src-js/index.ts b/apps/oxlint/src-js/index.ts index f71d5ab18de6c..28d9744313e50 100644 --- a/apps/oxlint/src-js/index.ts +++ b/apps/oxlint/src-js/index.ts @@ -274,3 +274,19 @@ function createContextAndVisitor(rule: CreateOnceRule): { return { context, visitor, beforeHook }; } + +/** + * Clear all loaded plugins and rules. + * + * This function clears the internal state of registered plugins and rules, + * allowing plugins to be reloaded from scratch. This is useful for the + * language server when restarting or reloading configuration. + * + * Note: This function is lazy-loaded and will only be available after + * the first plugin has been loaded. It will not have any effect if no + * plugins have been loaded yet. + */ +export async function clearLoadedPlugin(): Promise { + const mod = await import('./plugins/index.js'); + mod.clearLoadedPlugin(); +} diff --git a/apps/oxlint/src-js/plugins/index.ts b/apps/oxlint/src-js/plugins/index.ts index 4627c7e0cfb54..2347d6f99df5d 100644 --- a/apps/oxlint/src-js/plugins/index.ts +++ b/apps/oxlint/src-js/plugins/index.ts @@ -1,4 +1,4 @@ import { lintFile } from './lint.js'; -import { loadPlugin } from './load.js'; +import { clearLoadedPlugin, loadPlugin } from './load.js'; -export { lintFile, loadPlugin }; +export { clearLoadedPlugin, lintFile, loadPlugin }; diff --git a/apps/oxlint/src-js/plugins/load.ts b/apps/oxlint/src-js/plugins/load.ts index 8d3e294c7cc4c..2b1c55e5e21e4 100644 --- a/apps/oxlint/src-js/plugins/load.ts +++ b/apps/oxlint/src-js/plugins/load.ts @@ -271,3 +271,15 @@ function conformHookFn(hookFn: H | null | undefined, hookName: string): H | n if (typeof hookFn !== 'function') throw new TypeError(`\`${hookName}\` hook must be a function if provided`); return hookFn; } + +/** + * Clear all loaded plugins and rules. + * + * This function clears the internal state of registered plugins and rules, + * allowing plugins to be reloaded from scratch. This is useful for the + * language server when restarting or reloading configuration. + */ +export function clearLoadedPlugin(): void { + registeredPluginPaths.clear(); + registeredRules.length = 0; +} diff --git a/apps/oxlint/src/run.rs b/apps/oxlint/src/run.rs index f77b974545128..68ec15ed2ee9f 100644 --- a/apps/oxlint/src/run.rs +++ b/apps/oxlint/src/run.rs @@ -52,6 +52,8 @@ pub type JsLintFileCb = ThreadsafeFunction< false, >; + + /// NAPI entry point. /// /// JS side passes in: diff --git a/apps/oxlint/test/clear-loaded-plugin.test.ts b/apps/oxlint/test/clear-loaded-plugin.test.ts new file mode 100644 index 0000000000000..79e14752761ba --- /dev/null +++ b/apps/oxlint/test/clear-loaded-plugin.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import { clearLoadedPlugin } from '../dist/index.js'; + +/** + * Test the clearLoadedPlugin function + */ +describe('clearLoadedPlugin', () => { + it('should be exported from the main module', () => { + expect(clearLoadedPlugin).toBeDefined(); + expect(typeof clearLoadedPlugin).toBe('function'); + }); + + it('should execute without errors', async () => { + // Call the clear function - it should not throw + await expect(clearLoadedPlugin()).resolves.toBeUndefined(); + }); + + it('should work when called multiple times', async () => { + // Call it multiple times to ensure it doesn't error + await expect(clearLoadedPlugin()).resolves.toBeUndefined(); + await expect(clearLoadedPlugin()).resolves.toBeUndefined(); + await expect(clearLoadedPlugin()).resolves.toBeUndefined(); + }); +}); From 3df227299600e7befcd71796126cc2034e1b0c68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:58:34 +0000 Subject: [PATCH 3/4] Call clearLoadedPlugin in ExternalLinter destructor - Add JsClearLoadedPluginCb type to handle plugin state clearing - Add clearLoadedPlugin callback parameter to create_external_linter - Implement Drop trait for ExternalLinter to call clearLoadedPlugin - Update lint and lint_impl to accept and pass clearLoadedPlugin callback - Update cli.ts to pass clearLoadedPluginWrapper callback - Export ExternalLinterClearLoadedPluginCb from oxc_linter The clearLoadedPlugin function is now automatically called when the ExternalLinter is destroyed, ensuring plugin state is properly cleaned up after linting completes. Co-authored-by: Sysix <3897725+Sysix@users.noreply.github.com> --- apps/oxlint/src-js/bindings.d.ts | 7 +++++- apps/oxlint/src-js/cli.ts | 19 +++++++++++--- apps/oxlint/src/js_plugins/external_linter.rs | 16 ++++++++++-- apps/oxlint/src/run.rs | 25 +++++++++++++++---- crates/oxc_linter/src/external_linter.rs | 13 +++++++++- crates/oxc_linter/src/lib.rs | 4 +-- 6 files changed, 70 insertions(+), 14 deletions(-) diff --git a/apps/oxlint/src-js/bindings.d.ts b/apps/oxlint/src-js/bindings.d.ts index 93921c1f0a557..92ff6e75b3501 100644 --- a/apps/oxlint/src-js/bindings.d.ts +++ b/apps/oxlint/src-js/bindings.d.ts @@ -1,5 +1,9 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ +/** JS callback to clear loaded plugins. */ +export type JsClearLoadedPluginCb = + (() => void) + /** JS callback to lint a file. */ export type JsLintFileCb = ((arg0: string, arg1: number, arg2: Uint8Array | undefined | null, arg3: Array, arg4: string) => string) @@ -15,7 +19,8 @@ export type JsLoadPluginCb = * 1. `args`: Command line arguments (process.argv.slice(2)) * 2. `load_plugin`: Load a JS plugin from a file path. * 3. `lint_file`: Lint a file. + * 4. `clear_loaded_plugin`: Clear loaded plugin state. * * Returns `true` if linting succeeded without errors, `false` otherwise. */ -export declare function lint(args: Array, loadPlugin: JsLoadPluginCb, lintFile: JsLintFileCb): Promise +export declare function lint(args: Array, loadPlugin: JsLoadPluginCb, lintFile: JsLintFileCb, clearLoadedPlugin: JsClearLoadedPluginCb): Promise diff --git a/apps/oxlint/src-js/cli.ts b/apps/oxlint/src-js/cli.ts index d14cb7c1920b8..a25bb36fbfed4 100644 --- a/apps/oxlint/src-js/cli.ts +++ b/apps/oxlint/src-js/cli.ts @@ -6,6 +6,7 @@ import { debugAssertIsNonNull } from './utils/asserts.js'; // are identical. Ditto `lintFile` and `lintFileWrapper`. let loadPlugin: typeof loadPluginWrapper | null = null; let lintFile: typeof lintFileWrapper | null = null; +let clearLoadedPlugin: typeof clearLoadedPluginWrapper | null = null; /** * Load a plugin. @@ -21,7 +22,7 @@ function loadPluginWrapper(path: string, packageName: string | null): Promise { - ({ loadPlugin, lintFile } = mod); + ({ loadPlugin, lintFile, clearLoadedPlugin } = mod); return loadPlugin(path, packageName); }); } @@ -54,11 +55,23 @@ function lintFileWrapper( return lintFile(filePath, bufferId, buffer, ruleIds, settingsJSON); } +/** + * Clear loaded plugin state. + * + * Delegates to `clearLoadedPlugin`, which was lazy-loaded by `loadPluginWrapper`. + * If plugins haven't been loaded yet, this is a no-op. + */ +function clearLoadedPluginWrapper(): void { + if (clearLoadedPlugin !== null) { + clearLoadedPlugin(); + } +} + // Get command line arguments, skipping first 2 (node binary and script path) const args = process.argv.slice(2); -// Call Rust, passing `loadPlugin` and `lintFile` as callbacks, and CLI arguments -const success = await lint(args, loadPluginWrapper, lintFileWrapper); +// Call Rust, passing `loadPlugin`, `lintFile`, and `clearLoadedPlugin` as callbacks, and CLI arguments +const success = await lint(args, loadPluginWrapper, lintFileWrapper, clearLoadedPluginWrapper); // Note: It's recommended to set `process.exitCode` instead of calling `process.exit()`. // `process.exit()` kills the process immediately and `stdout` may not be flushed before process dies. diff --git a/apps/oxlint/src/js_plugins/external_linter.rs b/apps/oxlint/src/js_plugins/external_linter.rs index 670552ee3ad81..5dd902b870f5b 100644 --- a/apps/oxlint/src/js_plugins/external_linter.rs +++ b/apps/oxlint/src/js_plugins/external_linter.rs @@ -15,18 +15,20 @@ use oxc_linter::{ use crate::{ generated::raw_transfer_constants::{BLOCK_ALIGN, BUFFER_SIZE}, - run::{JsLintFileCb, JsLoadPluginCb}, + run::{JsClearLoadedPluginCb, JsLintFileCb, JsLoadPluginCb}, }; /// Wrap JS callbacks as normal Rust functions, and create [`ExternalLinter`]. pub fn create_external_linter( load_plugin: JsLoadPluginCb, lint_file: JsLintFileCb, + clear_loaded_plugin: JsClearLoadedPluginCb, ) -> ExternalLinter { let rust_load_plugin = wrap_load_plugin(load_plugin); let rust_lint_file = wrap_lint_file(lint_file); + let rust_clear_loaded_plugin = wrap_clear_loaded_plugin(clear_loaded_plugin); - ExternalLinter::new(rust_load_plugin, rust_lint_file) + ExternalLinter::new(rust_load_plugin, rust_lint_file, rust_clear_loaded_plugin) } /// Wrap `loadPlugin` JS callback as a normal Rust function. @@ -59,6 +61,16 @@ pub enum LintFileReturnValue { Failure(String), } +/// Wrap `clearLoadedPlugin` JS callback as a normal Rust function. +/// +/// This function is called when the `ExternalLinter` is dropped to clear loaded plugin state. +fn wrap_clear_loaded_plugin(cb: JsClearLoadedPluginCb) -> oxc_linter::ExternalLinterClearLoadedPluginCb { + Box::new(move || { + // Call the JavaScript callback to clear loaded plugin state + let _ = cb.call((), ThreadsafeFunctionCallMode::Blocking); + }) +} + /// Wrap `lintFile` JS callback as a normal Rust function. /// /// The returned function creates a `Uint8Array` referencing the memory of the given `Allocator`, diff --git a/apps/oxlint/src/run.rs b/apps/oxlint/src/run.rs index 68ec15ed2ee9f..968ce7071f287 100644 --- a/apps/oxlint/src/run.rs +++ b/apps/oxlint/src/run.rs @@ -52,7 +52,20 @@ pub type JsLintFileCb = ThreadsafeFunction< false, >; - +/// JS callback to clear loaded plugins. +#[napi] +pub type JsClearLoadedPluginCb = ThreadsafeFunction< + // Arguments + (), + // Return value + (), + // Arguments (repeated) + (), + // Error status + Status, + // CalleeHandled + false, +>; /// NAPI entry point. /// @@ -60,13 +73,14 @@ pub type JsLintFileCb = ThreadsafeFunction< /// 1. `args`: Command line arguments (process.argv.slice(2)) /// 2. `load_plugin`: Load a JS plugin from a file path. /// 3. `lint_file`: Lint a file. +/// 4. `clear_loaded_plugin`: Clear loaded plugin state. /// /// Returns `true` if linting succeeded without errors, `false` otherwise. #[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(args: Vec, load_plugin: JsLoadPluginCb, lint_file: JsLintFileCb) -> bool { - lint_impl(args, load_plugin, lint_file).await.report() == ExitCode::SUCCESS +pub async fn lint(args: Vec, load_plugin: JsLoadPluginCb, lint_file: JsLintFileCb, clear_loaded_plugin: JsClearLoadedPluginCb) -> bool { + lint_impl(args, load_plugin, lint_file, clear_loaded_plugin).await.report() == ExitCode::SUCCESS } /// Run the linter. @@ -74,6 +88,7 @@ async fn lint_impl( args: Vec, load_plugin: JsLoadPluginCb, lint_file: JsLintFileCb, + clear_loaded_plugin: JsClearLoadedPluginCb, ) -> CliRunResult { // Convert String args to OsString for compatibility with bpaf let args: Vec = args.into_iter().map(std::ffi::OsString::from).collect(); @@ -106,10 +121,10 @@ 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 = Some(super::js_plugins::create_external_linter(load_plugin, lint_file)); + let external_linter = Some(super::js_plugins::create_external_linter(load_plugin, lint_file, clear_loaded_plugin)); #[cfg(not(all(target_pointer_width = "64", target_endian = "little")))] let external_linter = { - let (_, _) = (load_plugin, lint_file); + let (_, _, _) = (load_plugin, lint_file, clear_loaded_plugin); None }; diff --git a/crates/oxc_linter/src/external_linter.rs b/crates/oxc_linter/src/external_linter.rs index d9f3a213444fe..7042ca7dd8b27 100644 --- a/crates/oxc_linter/src/external_linter.rs +++ b/crates/oxc_linter/src/external_linter.rs @@ -16,6 +16,8 @@ pub type ExternalLinterLintFileCb = Box< + Send, >; +pub type ExternalLinterClearLoadedPluginCb = Box; + #[derive(Clone, Debug, Deserialize)] pub enum PluginLoadResult { #[serde(rename_all = "camelCase")] @@ -47,14 +49,23 @@ pub struct JsFix { pub struct ExternalLinter { pub(crate) load_plugin: ExternalLinterLoadPluginCb, pub(crate) lint_file: ExternalLinterLintFileCb, + clear_loaded_plugin: ExternalLinterClearLoadedPluginCb, } impl ExternalLinter { pub fn new( load_plugin: ExternalLinterLoadPluginCb, lint_file: ExternalLinterLintFileCb, + clear_loaded_plugin: ExternalLinterClearLoadedPluginCb, ) -> Self { - Self { load_plugin, lint_file } + Self { load_plugin, lint_file, clear_loaded_plugin } + } +} + +impl Drop for ExternalLinter { + fn drop(&mut self) { + // Clear loaded plugin state when the linter is destroyed + (self.clear_loaded_plugin)(); } } diff --git a/crates/oxc_linter/src/lib.rs b/crates/oxc_linter/src/lib.rs index 3bd5efe74b653..0fd1feba063c1 100644 --- a/crates/oxc_linter/src/lib.rs +++ b/crates/oxc_linter/src/lib.rs @@ -60,8 +60,8 @@ pub use crate::{ }, context::{ContextSubHost, LintContext}, external_linter::{ - ExternalLinter, ExternalLinterLintFileCb, ExternalLinterLoadPluginCb, JsFix, - LintFileResult, PluginLoadResult, + ExternalLinter, ExternalLinterClearLoadedPluginCb, ExternalLinterLintFileCb, + ExternalLinterLoadPluginCb, JsFix, LintFileResult, PluginLoadResult, }, external_plugin_store::{ExternalPluginStore, ExternalRuleId}, fixer::{Fix, FixKind, Message, PossibleFixes}, From b99863410dd6173eb62709bc9e13bdf457ef883c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:41:31 +0000 Subject: [PATCH 4/4] [autofix.ci] apply automated fixes --- apps/oxlint/src-js/index.ts | 4 ++-- apps/oxlint/src-js/plugins/load.ts | 2 +- apps/oxlint/src/js_plugins/external_linter.rs | 4 +++- apps/oxlint/src/run.rs | 13 +++++++++++-- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/oxlint/src-js/index.ts b/apps/oxlint/src-js/index.ts index 28d9744313e50..892c73bd92591 100644 --- a/apps/oxlint/src-js/index.ts +++ b/apps/oxlint/src-js/index.ts @@ -277,11 +277,11 @@ function createContextAndVisitor(rule: CreateOnceRule): { /** * Clear all loaded plugins and rules. - * + * * This function clears the internal state of registered plugins and rules, * allowing plugins to be reloaded from scratch. This is useful for the * language server when restarting or reloading configuration. - * + * * Note: This function is lazy-loaded and will only be available after * the first plugin has been loaded. It will not have any effect if no * plugins have been loaded yet. diff --git a/apps/oxlint/src-js/plugins/load.ts b/apps/oxlint/src-js/plugins/load.ts index 2b1c55e5e21e4..863c6f9a8ce71 100644 --- a/apps/oxlint/src-js/plugins/load.ts +++ b/apps/oxlint/src-js/plugins/load.ts @@ -274,7 +274,7 @@ function conformHookFn(hookFn: H | null | undefined, hookName: string): H | n /** * Clear all loaded plugins and rules. - * + * * This function clears the internal state of registered plugins and rules, * allowing plugins to be reloaded from scratch. This is useful for the * language server when restarting or reloading configuration. diff --git a/apps/oxlint/src/js_plugins/external_linter.rs b/apps/oxlint/src/js_plugins/external_linter.rs index 5dd902b870f5b..13555a4db8cc6 100644 --- a/apps/oxlint/src/js_plugins/external_linter.rs +++ b/apps/oxlint/src/js_plugins/external_linter.rs @@ -64,7 +64,9 @@ pub enum LintFileReturnValue { /// Wrap `clearLoadedPlugin` JS callback as a normal Rust function. /// /// This function is called when the `ExternalLinter` is dropped to clear loaded plugin state. -fn wrap_clear_loaded_plugin(cb: JsClearLoadedPluginCb) -> oxc_linter::ExternalLinterClearLoadedPluginCb { +fn wrap_clear_loaded_plugin( + cb: JsClearLoadedPluginCb, +) -> oxc_linter::ExternalLinterClearLoadedPluginCb { Box::new(move || { // Call the JavaScript callback to clear loaded plugin state let _ = cb.call((), ThreadsafeFunctionCallMode::Blocking); diff --git a/apps/oxlint/src/run.rs b/apps/oxlint/src/run.rs index 968ce7071f287..187f6f027b31c 100644 --- a/apps/oxlint/src/run.rs +++ b/apps/oxlint/src/run.rs @@ -79,7 +79,12 @@ pub type JsClearLoadedPluginCb = ThreadsafeFunction< #[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(args: Vec, load_plugin: JsLoadPluginCb, lint_file: JsLintFileCb, clear_loaded_plugin: JsClearLoadedPluginCb) -> bool { +pub async fn lint( + args: Vec, + load_plugin: JsLoadPluginCb, + lint_file: JsLintFileCb, + clear_loaded_plugin: JsClearLoadedPluginCb, +) -> bool { lint_impl(args, load_plugin, lint_file, clear_loaded_plugin).await.report() == ExitCode::SUCCESS } @@ -121,7 +126,11 @@ 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 = Some(super::js_plugins::create_external_linter(load_plugin, lint_file, clear_loaded_plugin)); + let external_linter = Some(super::js_plugins::create_external_linter( + load_plugin, + lint_file, + clear_loaded_plugin, + )); #[cfg(not(all(target_pointer_width = "64", target_endian = "little")))] let external_linter = { let (_, _, _) = (load_plugin, lint_file, clear_loaded_plugin);