-
-
Notifications
You must be signed in to change notification settings - Fork 724
feat(linter/plugins): rule options #16215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements rule options support for external JavaScript plugins in the linter. It introduces an ExternalOptionsId type to track and pass rule-specific configuration options from Rust to JavaScript plugins, allowing JS plugin rules to receive configuration parameters similar to ESLint rule options.
Key changes:
- Added
ExternalOptionsIdtype and options storage toExternalPluginStorewith index 0 reserved for "no options" - Updated external rule signatures from
(ExternalRuleId, AllowWarnDeny)to(ExternalRuleId, ExternalOptionsId, AllowWarnDeny)throughout the codebase - Implemented options serialization/deserialization flow between Rust and JavaScript using
OnceLockfor thread-safe caching
Reviewed changes
Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
crates/oxc_linter/src/external_plugin_store.rs |
Added ExternalOptionsId type and options storage with index 0 reserved for empty/null options |
crates/oxc_linter/src/external_linter.rs |
Updated callback signature to include options_ids parameter |
crates/oxc_linter/src/lib.rs |
Updated external rule handling to pass options IDs to JS side |
crates/oxc_linter/src/config/rules.rs |
Modified override_rules to track options alongside severity; added comprehensive tests |
crates/oxc_linter/src/config/config_store.rs |
Updated rule storage and override logic to handle options IDs; added override precedence test |
crates/oxc_linter/src/config/config_builder.rs |
Updated builder to accept mutable reference to external plugin store for options registration |
crates/oxc_linter/src/config/mod.rs |
Updated test to use mutable external plugin store |
crates/oxc_linter/src/tester.rs |
Updated to pass mutable reference to external plugin store |
crates/oxc_language_server/src/linter/server_linter.rs |
Updated config building calls to use mutable external plugin store |
tasks/benchmark/benches/linter.rs |
Updated benchmark to use mutable external plugin store |
napi/playground/src/lib.rs |
Updated playground to properly share external plugin store between config building and linter creation |
apps/oxlint/src/run.rs |
Added OnceLock-based caching for serialized options JSON and export function for JS |
apps/oxlint/src/lint.rs |
Added serialization of external options after config building |
apps/oxlint/src/js_plugins/external_linter.rs |
Updated wrapper to forward options IDs to JS callback |
apps/oxlint/src-js/plugins/options.ts |
Added options initialization logic and storage |
apps/oxlint/src-js/plugins/load.ts |
Duplicated options initialization logic (should be removed) |
apps/oxlint/src-js/plugins/lint.ts |
Added lazy options initialization and options ID handling; duplicate ruleIndex assignment |
apps/oxlint/src-js/cli.ts |
Updated wrapper to forward options IDs parameter |
apps/oxlint/src-js/bindings.js |
Exported getExternalRuleOptions function |
apps/oxlint/src-js/bindings.d.ts |
Added TypeScript declaration for getExternalRuleOptions |
apps/oxlint/test/fixtures/custom_plugin_with_options/* |
Added integration test fixture for external plugin with options |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apps/oxlint/src/run.rs
Outdated
| /// JS callable function to retrieve the serialized external rule options. | ||
| /// Returns a JSON string of options arrays. Called once from JS after creating the external linter. | ||
| #[cfg(all(feature = "napi", target_pointer_width = "64", target_endian = "little"))] | ||
| #[napi] | ||
| pub fn get_external_rule_options() -> Option<String> { | ||
| EXTERNAL_OPTIONS_JSON.get().cloned() |
Copilot
AI
Nov 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documentation comment states "Returns a JSON string of options arrays" but the function returns Option<String>, which can be None. The comment should clarify when None is returned (e.g., "Returns a JSON string of options arrays, or None if options haven't been set yet").
CodSpeed Performance ReportMerging #16215 will not alter performanceComparing Summary
Footnotes
|
a7a3ca1 to
9c2d8a4
Compare
|
Just a note: As I mentioned on Discord, I don't think we should worry about merging options with default options in this first PR - can leave that to a follow-on. But FYI I've implemented the merging logic in #16217 because I needed it for the rule tester. |
d62762c to
3c46139
Compare
I know it was met that said this should be possible, but I could very well be wrong. If it's not possible, don't worry about it. |
3c46139 to
ae52e34
Compare
| // TODO: refactor this elsewhere. | ||
| // This code is in the oxlint app, not in oxc_linter crate | ||
| if let Some(ref external_linter) = external_linter { | ||
| if let Err(err) = external_plugin_store.setup_configs(external_linter) { | ||
| print_and_flush_stdout( | ||
| stdout, | ||
| &format!("Failed to setup external plugin options: {err}\n"), | ||
| ); | ||
| return CliRunResult::InvalidOptionConfig; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could use help here. I exposed setup_configs() for the oxlint crate by putting it on ExternalPluginStore only because it's the first place I found. I could do with a few pointers on separation of concerns regarding these structs.
ae52e34 to
3e53d55
Compare
dfb4439 to
1b4cd57
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 23 out of 24 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * Populates Rust-resolved configuration options on the JS side. | ||
| * Called from Rust side after all configuration options have been resolved. | ||
| * | ||
| * Note: the name `setupConfigs` is currently incorrect, as we only populate rule options. | ||
| * The intention is for this function to transfer all configurations in a multi-config workspace. | ||
| * The configuration relevant to each file would then be resolved on the JS side. | ||
| * | ||
| * @param optionsJSON - JSON serialization of an array containing all rule options across all configurations. | ||
| * @returns "ok" on success, or error message on failure | ||
| */ | ||
| export function setupConfigs(optionsJSON: string): string { |
Copilot
AI
Nov 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The comment states "Note: the name setupConfigs is currently incorrect, as we only populate rule options." While this is acknowledged as a TODO, consider renaming to setupOptions or setupRuleOptions to accurately reflect the current functionality. This would make the code clearer until multi-config workspace support is implemented.
| * Populates Rust-resolved configuration options on the JS side. | |
| * Called from Rust side after all configuration options have been resolved. | |
| * | |
| * Note: the name `setupConfigs` is currently incorrect, as we only populate rule options. | |
| * The intention is for this function to transfer all configurations in a multi-config workspace. | |
| * The configuration relevant to each file would then be resolved on the JS side. | |
| * | |
| * @param optionsJSON - JSON serialization of an array containing all rule options across all configurations. | |
| * @returns "ok" on success, or error message on failure | |
| */ | |
| export function setupConfigs(optionsJSON: string): string { | |
| * Populates Rust-resolved rule options on the JS side. | |
| * Called from Rust side after all rule options have been resolved. | |
| * | |
| * The intention is for this function to eventually transfer all configurations in a multi-config workspace. | |
| * The configuration relevant to each file would then be resolved on the JS side. | |
| * | |
| * @param optionsJSON - JSON serialization of an array containing all rule options across all configurations. | |
| * @returns "ok" on success, or error message on failure | |
| */ | |
| export function setupOptions(optionsJSON: string): string { |
|
|
||
| // If the rule has no user-provided options, use the plugin-provided default | ||
| // options (which falls back to `DEFAULT_OPTIONS`) | ||
| ruleDetails.options = | ||
| optionsId === DEFAULT_OPTIONS_ID ? ruleDetails.defaultOptions : allOptions[optionsId]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@overlookmotel You mentioned this could be simplified to allOptions[optionsId]. How would plugin-provided default options get used in that case?
| const el = parsed[i]; | ||
| if (!isArray(el)) throw new TypeError("Each options entry must be an array", { cause: el }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a debug check because I this validation is done probably done on the rust side as well. I need to confirm this.
Big changes from the original approach
ruleId-optionIdpairs as tuples in an array - several heap allocated objects and excessive pointer indirection.setupConfigs(). I expect we would use this callback for more plugin related data, including settings which are currently implemented in a hacky way.ExternalPluginStoreeverywhere.Smaller Changes