Skip to content

Comments

feat(linter): Add support for options for jsPlugins#15822

Closed
wagenet wants to merge 1 commit intooxc-project:mainfrom
wagenet:js-plugin-options
Closed

feat(linter): Add support for options for jsPlugins#15822
wagenet wants to merge 1 commit intooxc-project:mainfrom
wagenet:js-plugin-options

Conversation

@wagenet
Copy link
Contributor

@wagenet wagenet commented Nov 18, 2025

Fixes #14825

@graphite-app
Copy link
Contributor

graphite-app bot commented Nov 18, 2025

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • 0-merge - adds this PR to the back of the merge queue
  • hotfix - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

@github-actions github-actions bot added A-linter Area - Linter A-cli Area - CLI A-editor Area - Editor and Language Server A-linter-plugins Area - Linter JS plugins labels Nov 18, 2025
@codspeed-hq
Copy link

codspeed-hq bot commented Nov 18, 2025

CodSpeed Performance Report

Merging #15822 will not alter performance

Comparing wagenet:js-plugin-options (6bb6c2b) with main (4fe3aac)

Summary

✅ 42 untouched
⏩ 3 skipped1

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@wagenet wagenet force-pushed the js-plugin-options branch 4 times, most recently from f483727 to c96b043 Compare November 18, 2025 04:12
@wagenet wagenet changed the title Add support for options for jsPlugins feat(linter) Add support for options for jsPlugins Nov 18, 2025
@wagenet wagenet marked this pull request as ready for review November 18, 2025 04:31
Copilot AI review requested due to automatic review settings November 18, 2025 04:31
@wagenet wagenet changed the title feat(linter) Add support for options for jsPlugins feat(linter): Add support for options for jsPlugins Nov 18, 2025
Copy link
Contributor

Copilot AI left a 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 adds support for passing options to JavaScript plugin rules, enabling external linting rules to receive configuration parameters from .oxlintrc.json files.

Key changes:

  • Introduces ExternalOptionsId to track rule options alongside rule IDs
  • Modifies the external linter interface to pass (ruleId, optionsId) tuples instead of just rule IDs
  • Adds serialization of options from Rust to JavaScript via a global JSON string

Reviewed Changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
crates/oxc_linter/src/external_plugin_store.rs Adds ExternalOptionsId type and options storage/serialization methods
crates/oxc_linter/src/config/rules.rs Updates rule override logic to store and pass options IDs
crates/oxc_linter/src/config/config_store.rs Updates configuration structures to include options IDs with external rules
crates/oxc_linter/src/config/config_builder.rs Modifies builder to handle options IDs and mutable plugin store
crates/oxc_linter/src/lib.rs Updates external rule execution to pass options IDs
crates/oxc_linter/src/external_linter.rs Changes callback signature to accept rule/options ID pairs
apps/oxlint/src/run.rs Adds global storage and NAPI functions for options JSON
apps/oxlint/src/lint.rs Serializes options after config building
apps/oxlint/src-js/plugins/load.ts Adds options storage and initialization functions
apps/oxlint/src-js/plugins/lint.ts Updates linting logic to fetch and apply options per rule
Test files Adds comprehensive test coverage for options functionality

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions github-actions bot added the C-enhancement Category - New feature or request label Nov 18, 2025
use super::super::server_linter::ServerLinterBuilder;

#[test]
fn test_new_with_js_plugins_in_config_does_not_crash() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really testing anything, only if the ServerLinterBuilder::build did not panic.
jsPlugins for the language server is not implemented, but a milestone goal: #14826.

Can you remove this file?

Copy link
Member

@overlookmotel overlookmotel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks very much for working on this. This'll be a big ✅ off our list of requirements for JS plugins getting to alpha status.

Comments below are mostly small things.

My broader, more substantive points are:

Push not pull

Currently there's a rather convoluted method of getting options from Rust to JS. The options get stored in a static, then every time JS lints a file it asks "do I have the options yet?", and if not then JS calls back into Rust to get the options back out of that static.

I think this could be simplified considerably by using a "push" model instead of "pull".

After all configs have been parsed, if there are any JS plugins registered, send the options JSON over to JS. Done.

In practice, this means getting rid of getExternalRuleOptions, and instead passing a 3rd callback into Rust at the start (cli.ts):

- const success = await lint(args, loadPluginWrapper, lintFileWrapper);
+ const success = await lint(args, loadPluginWrapper, setupWrapper, lintFileWrapper);

Rust would call setup and pass the options JSON before it starts linting any files.

ConfigStoreBuilder::build

Much of the diff in this PR is just due to ConfigStoreBuilder::build now taking a &mut ExternalPluginStore instead of &ExternalPluginStore.

I have a feeling that this shouldn't be required. But I find the config logic so labyrinthine that I can't put my finger on what'd need to change to enable that (or whether my intuition is incorrect).

So let's leave that one for now, address the other stuff first, and come back to it.

CI failure

The snapshot differences are due to an annoyance with character encoding. If you run cd apps/oxlint; pnpm run build-test; (build-test not build), that should fix them - and also fix bindings.d.ts.

// All external rule options serialized from Rust.
// Indexed by ExternalOptionsId (numeric) sent alongside ruleId for each file.
// Element 0 is always an empty array (reserved for rules with no options).
export let allOptions: unknown[][] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export let allOptions: unknown[][] = [];
export let allOptions: Options[] = [];

Then can get rid of an as Options in lint.ts.


// Set `ruleIndex` for rule. It's used when sending diagnostics back to Rust.
for (let i = 0; i < ruleData.length; i++) {
const [ruleId, optionsId] = ruleData[i];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with:

Suggested change
const [ruleId, optionsId] = ruleData[i];
const ruleId = ruleData[0],
optionsId = ruleData[1];

I know the original is more idiomatic, but in apps/oxlint we prioritize perf over readability! Array destructuring is much more expensive for the JS engine.

Comment on lines +141 to +143
const options = allOptions[optionsId];
ruleDetails.ruleIndex = i;
ruleDetails.options = options === undefined || !Array.isArray(options) ? [] : (options as Options);
Copy link
Member

@overlookmotel overlookmotel Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be able to compress this to:

ruleDetails.ruleIndex = i;
ruleDetails.options = allOptions[optionsId];

We need to make sure allOptions[0] is always an empty array. Rust already passes 0 as optionsId when there are no options for the rule.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, we can get rid of DEFAULT_OPTIONS in load.ts, and initialize ruleDetails.options as null when the RuleDetails is originally created. ruleDetails.options is now set before running the rule on each file, so this default is always overwritten.

noOptions: 'No options provided',
},
},
createOnce(context) {
Copy link
Member

@overlookmotel overlookmotel Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use create (not createOnce) in tests, for consistency.

This will also simplify the implementation - can do the check for options in body of create method, instead of in before.

Comment on lines +17 to +53
return {
before() {
// Options are now available since setupContextForFile was called
const options = context.options;

// Check if options were passed correctly
if (!options || options.length === 0) {
context.report({
messageId: 'noOptions',
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 1 } },
});
return false;
}
return true;
},

DebuggerStatement(node) {
// Options are available in visitor methods
const options = context.options;

// First option is a boolean
const shouldReport = options[0];
// Second option is an object
const config = options[1] || {};

if (shouldReport) {
context.report({
messageId: 'wrongValue',
data: {
expected: String(config.expected || 'enabled'),
actual: 'disabled',
},
node,
});
}
},
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can greatly simplify all this. Just unconditionally report JSON.stringify(context.options). Then eyeball the snapshot to make sure it looks right.

Comment on lines +40 to +42
let mut options = IndexVec::default();
// Index 0 is reserved for "no options" (empty array)
options.push(serde_json::json!([]));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
let mut options = IndexVec::default();
// Index 0 is reserved for "no options" (empty array)
options.push(serde_json::json!([]));
// Index 0 is reserved for "no options" (empty array)
let options = index_vec![serde_json::json!([])];

(import index_vec macro from oxc_index crate)

@overlookmotel
Copy link
Member

overlookmotel commented Nov 24, 2025

Hi @wagenet. Not intended as hassling you, just a request for information: Do you think you're going to have any time to address the comments above? If not, that's totally fine, just it'd be useful to know so I can set aside time to finish this up, if you need to hand it over. I'm keen to get it merged ideally within the next week.

@overlookmotel
Copy link
Member

Hi @wagenet. Have not heard from you in a few days (no problem, I imagine you're busy). @lilnasy has volunteered to take over the PR and finish it off. Hope that's Ok with you.

@lilnasy I'm not familiar with the language server, so I don't know how to evaluate the changes there, or @Sysix's comment above. But if you'd be able to address my feedback above, that'd be amazing.

@overlookmotel overlookmotel marked this pull request as draft November 27, 2025 00:05
@lilnasy lilnasy mentioned this pull request Nov 27, 2025
3 tasks
overlookmotel added a commit that referenced this pull request Dec 1, 2025
- Implements #14825
- Rebased #15822. Refactored. Pruned the slop. Tests passing.

### Big changes from the original approach
- [x] Options IDs are stored in their own arrays. This was laid out in
#16158. The original approach was to store `ruleId`-`optionId` pairs as
tuples in an array - several heap allocated objects and excessive
pointer indirection.
- [x] The rust code populates the options by calling a new JS callback
named `setupConfigs()`. I expect we would use this callback for more
plugin related data, including settings which are currently implemented
in a hacky way.
- [ ] Investigate a refactor to avoid requiring a mutable reference to
`ExternalPluginStore` everywhere.

<details>
<summary>

#### Smaller Changes

</summary>

- Removed the extra fixture ("working version"). There were no
meaningful changes compared to the first fixture. Probably slop.

</details>

---------

Co-authored-by: Peter Wagenet <peter@wagenet.us>
Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
@overlookmotel
Copy link
Member

#16215 built on this and completed it.

taearls pushed a commit to taearls/oxc that referenced this pull request Dec 11, 2025
- Implements oxc-project#14825
- Rebased oxc-project#15822. Refactored. Pruned the slop. Tests passing.

### Big changes from the original approach
- [x] Options IDs are stored in their own arrays. This was laid out in
oxc-project#16158. The original approach was to store `ruleId`-`optionId` pairs as
tuples in an array - several heap allocated objects and excessive
pointer indirection.
- [x] The rust code populates the options by calling a new JS callback
named `setupConfigs()`. I expect we would use this callback for more
plugin related data, including settings which are currently implemented
in a hacky way.
- [ ] Investigate a refactor to avoid requiring a mutable reference to
`ExternalPluginStore` everywhere.

<details>
<summary>

#### Smaller Changes

</summary>

- Removed the extra fixture ("working version"). There were no
meaningful changes compared to the first fixture. Probably slop.

</details>

---------

Co-authored-by: Peter Wagenet <peter@wagenet.us>
Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-editor Area - Editor and Language Server A-linter Area - Linter A-linter-plugins Area - Linter JS plugins C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linter plugins: Support options for JS rules

3 participants