Skip to content

Commit a7a3ca1

Browse files
wagenetlilnasy
authored andcommitted
Add support for options for jsPlugins
1 parent d4a1581 commit a7a3ca1

File tree

23 files changed

+448
-65
lines changed

23 files changed

+448
-65
lines changed

apps/oxlint/src-js/bindings.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
/* auto-generated by NAPI-RS */
22
/* eslint-disable */
3+
/**
4+
* JS callable function to retrieve the serialized external rule options.
5+
* Returns a JSON string of options arrays. Called once from JS after creating the external linter.
6+
*/
7+
export declare function getExternalRuleOptions(): string | null
8+
39
/** JS callback to lint a file. */
410
export type JsLintFileCb =
5-
((arg0: string, arg1: number, arg2: Uint8Array | undefined | null, arg3: Array<number>, arg4: string) => string)
11+
((arg0: string, arg1: number, arg2: Uint8Array | undefined | null, arg3: Array<number>, arg4: Array<number>, arg5: string) => string)
612

713
/** JS callback to load a JS plugin. */
814
export type JsLoadPluginCb =

apps/oxlint/src-js/bindings.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,5 +575,6 @@ if (!nativeBinding) {
575575
throw new Error(`Failed to load native binding`)
576576
}
577577

578-
const { lint } = nativeBinding
578+
const { getExternalRuleOptions, lint } = nativeBinding
579+
export { getExternalRuleOptions }
579580
export { lint }

apps/oxlint/src-js/cli.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ function loadPluginWrapper(path: string, packageName: string | null): Promise<st
3838
* @param bufferId - ID of buffer containing file data
3939
* @param buffer - Buffer containing file data, or `null` if buffer with this ID was previously sent to JS
4040
* @param ruleIds - IDs of rules to run on this file
41+
* @param optionsIds - IDs of options to use for rules on this file
4142
* @param settingsJSON - Settings for file, as JSON
4243
* @returns Diagnostics or error serialized to JSON string
4344
*/
@@ -46,12 +47,13 @@ function lintFileWrapper(
4647
bufferId: number,
4748
buffer: Uint8Array | null,
4849
ruleIds: number[],
50+
optionsIds: number[],
4951
settingsJSON: string,
5052
): string {
5153
// `lintFileWrapper` is never called without `loadPluginWrapper` being called first,
5254
// so `lintFile` must be defined here
5355
debugAssertIsNonNull(lintFile);
54-
return lintFile(filePath, bufferId, buffer, ruleIds, settingsJSON);
56+
return lintFile(filePath, bufferId, buffer, ruleIds, optionsIds, settingsJSON);
5557
}
5658

5759
// Get command line arguments, skipping first 2 (node binary and script path)

apps/oxlint/src-js/plugins/lint.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { setupFileContext, resetFileContext } from "./context.js";
22
import { registeredRules } from "./load.js";
3-
import { allOptions, DEFAULT_OPTIONS_ID } from "./options.js";
3+
import { allOptions, areOptionsInitialized, DEFAULT_OPTIONS_ID, setOptions } from "./options.js";
44
import { diagnostics } from "./report.js";
55
import { setSettingsForFile, resetSettings } from "./settings.js";
66
import { ast, initAst, resetSourceAndAst, setupSourceForFile } from "./source_code.js";
@@ -12,6 +12,7 @@ import {
1212
finalizeCompiledVisitor,
1313
initCompiledVisitor,
1414
} from "./visitor.js";
15+
import { getExternalRuleOptions } from "../bindings.js";
1516

1617
// Lazy implementation
1718
/*
@@ -46,6 +47,7 @@ const PARSER_SERVICES_DEFAULT: Record<string, unknown> = Object.freeze({});
4647
* @param bufferId - ID of buffer containing file data
4748
* @param buffer - Buffer containing file data, or `null` if buffer with this ID was previously sent to JS
4849
* @param ruleIds - IDs of rules to run on this file
50+
* @param optionsIds - IDs of options to use for rules on this file
4951
* @param settingsJSON - Settings for file, as JSON
5052
* @returns Diagnostics or error serialized to JSON string
5153
*/
@@ -54,11 +56,9 @@ export function lintFile(
5456
bufferId: number,
5557
buffer: Uint8Array | null,
5658
ruleIds: number[],
59+
optionsIds: number[],
5760
settingsJSON: string,
5861
): string {
59-
// TODO: Get `optionsIds` from Rust side
60-
const optionsIds = ruleIds.map((_) => DEFAULT_OPTIONS_ID);
61-
6262
try {
6363
lintFileImpl(filePath, bufferId, buffer, ruleIds, optionsIds, settingsJSON);
6464
return JSON.stringify({ Success: diagnostics });
@@ -145,6 +145,14 @@ function lintFileImpl(
145145
"Rule IDs and options IDs arrays must be the same length",
146146
);
147147

148+
// Initialize external rule options if not already initialized
149+
if (!areOptionsInitialized()) {
150+
const optionsJson = getExternalRuleOptions();
151+
if (optionsJson !== null && optionsJson.length > 0) {
152+
setOptions(optionsJson);
153+
}
154+
}
155+
148156
for (let i = 0, len = ruleIds.length; i < len; i++) {
149157
const ruleId = ruleIds[i];
150158
debugAssert(ruleId < registeredRules.length, "Rule ID out of bounds");

apps/oxlint/src-js/plugins/options.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,46 @@ export type Options = JsonValue[];
1212
// Default rule options
1313
export const DEFAULT_OPTIONS: Readonly<Options> = Object.freeze([]);
1414

15-
// All rule options
16-
export const allOptions: Readonly<Options>[] = [DEFAULT_OPTIONS];
15+
// All rule options.
16+
// Indexed by options ID sent alongside ruleId for each file.
17+
// Element 0 is always the default options (empty array).
18+
export let allOptions: Readonly<Options>[] = [DEFAULT_OPTIONS];
1719

1820
// Index into `allOptions` for default options
1921
export const DEFAULT_OPTIONS_ID = 0;
22+
23+
// Track if options have been initialized to avoid re-initialization
24+
let optionsInitialized = false;
25+
26+
/**
27+
* Set all external rule options.
28+
* Called once from Rust after config building, before any linting occurs.
29+
* @param optionsJson - JSON string of outer array of per-options arrays.
30+
*/
31+
export function setOptions(optionsJson: string): void {
32+
try {
33+
const parsed = JSON.parse(optionsJson);
34+
if (!Array.isArray(parsed)) throw new TypeError("Expected optionsJson to decode to an array");
35+
// Basic shape validation: each element must be an array (options tuple array)
36+
for (let i = 0; i < parsed.length; i++) {
37+
const el = parsed[i];
38+
if (!Array.isArray(el)) throw new TypeError("Each options entry must be an array");
39+
}
40+
allOptions = parsed as Readonly<Options>[];
41+
optionsInitialized = true;
42+
} catch (err) {
43+
// Re-throw with clearer message for Rust side logging.
44+
throw new Error(
45+
"Failed to parse external rule options JSON: " +
46+
(err instanceof Error ? err.message : String(err)),
47+
);
48+
}
49+
}
50+
51+
/**
52+
* Check if options have been initialized.
53+
* @returns `true` if options have been set, `false` otherwise.
54+
*/
55+
export function areOptionsInitialized(): boolean {
56+
return optionsInitialized;
57+
}

apps/oxlint/src/js_plugins/external_linter.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ fn wrap_lint_file(cb: JsLintFileCb) -> ExternalLinterLintFileCb {
7272
Box::new(
7373
move |file_path: String,
7474
rule_ids: Vec<u32>,
75+
options_ids: Vec<u32>,
7576
settings_json: String,
7677
allocator: &Allocator| {
7778
let (tx, rx) = channel();
@@ -87,7 +88,7 @@ fn wrap_lint_file(cb: JsLintFileCb) -> ExternalLinterLintFileCb {
8788

8889
// Send data to JS
8990
let status = cb.call_with_return_value(
90-
FnArgs::from((file_path, buffer_id, buffer, rule_ids, settings_json)),
91+
FnArgs::from((file_path, buffer_id, buffer, rule_ids, options_ids, settings_json)),
9192
ThreadsafeFunctionCallMode::NonBlocking,
9293
move |result, _env| {
9394
let _ = match &result {

apps/oxlint/src/lint.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ impl CliRunner {
277277
|| nested_configs.values().any(|config| config.plugins().has_import());
278278
let mut options = LintServiceOptions::new(self.cwd).with_cross_module(use_cross_module);
279279

280-
let lint_config = match config_builder.build(&external_plugin_store) {
280+
let lint_config = match config_builder.build(&mut external_plugin_store) {
281281
Ok(config) => config,
282282
Err(e) => {
283283
print_and_flush_stdout(
@@ -329,6 +329,13 @@ impl CliRunner {
329329
return CliRunResult::None;
330330
}
331331

332+
// After building config, serialize external rule options for JS side.
333+
#[cfg(all(feature = "napi", target_pointer_width = "64", target_endian = "little"))]
334+
{
335+
let store = config_store.external_plugin_store();
336+
crate::set_external_options_json(store);
337+
}
338+
332339
let files_to_lint = paths
333340
.into_iter()
334341
.filter(|path| !ignore_matcher.should_ignore(Path::new(path)))

apps/oxlint/src/run.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,30 @@ use crate::{
1616
result::CliRunResult,
1717
};
1818

19+
#[cfg(all(feature = "napi", target_pointer_width = "64", target_endian = "little"))]
20+
use oxc_linter::ExternalPluginStore;
21+
22+
#[cfg(all(feature = "napi", target_pointer_width = "64", target_endian = "little"))]
23+
use std::sync::OnceLock;
24+
25+
#[cfg(all(feature = "napi", target_pointer_width = "64", target_endian = "little"))]
26+
static EXTERNAL_OPTIONS_JSON: OnceLock<String> = OnceLock::new();
27+
28+
/// Set serialized external rule options JSON after building configs.
29+
/// Called from Rust side (internal) before any linting, then consumed on first call to `lint`.
30+
#[cfg(all(feature = "napi", target_pointer_width = "64", target_endian = "little"))]
31+
pub fn set_external_options_json(plugin_store: &ExternalPluginStore) {
32+
let _ = EXTERNAL_OPTIONS_JSON.set(plugin_store.serialize_all_options());
33+
}
34+
35+
/// JS callable function to retrieve the serialized external rule options.
36+
/// Returns a JSON string of options arrays. Called once from JS after creating the external linter.
37+
#[cfg(all(feature = "napi", target_pointer_width = "64", target_endian = "little"))]
38+
#[napi]
39+
pub fn get_external_rule_options() -> Option<String> {
40+
EXTERNAL_OPTIONS_JSON.get().cloned()
41+
}
42+
1943
/// JS callback to load a JS plugin.
2044
#[napi]
2145
pub type JsLoadPluginCb = ThreadsafeFunction<
@@ -40,12 +64,13 @@ pub type JsLintFileCb = ThreadsafeFunction<
4064
u32, // Buffer ID
4165
Option<Uint8Array>, // Buffer (optional)
4266
Vec<u32>, // Array of rule IDs
67+
Vec<u32>, // Array of options IDs
4368
String, // Stringified settings effective for the file
4469
)>,
4570
// Return value
4671
String, // `Vec<LintFileResult>`, serialized to JSON
4772
// Arguments (repeated)
48-
FnArgs<(String, u32, Option<Uint8Array>, Vec<u32>, String)>,
73+
FnArgs<(String, u32, Option<Uint8Array>, Vec<u32>, Vec<u32>, String)>,
4974
// Error status
5075
Status,
5176
// CalleeHandled
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"categories": {
3+
"correctness": "off"
4+
},
5+
"jsPlugins": ["./plugin.ts"],
6+
"rules": {
7+
"test-plugin-options/check-options": ["error", true, { "expected": "production" }]
8+
}
9+
}
10+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Test file with debugger statement
2+
debugger;
3+
4+
console.log("Hello, world!");

0 commit comments

Comments
 (0)