Skip to content

Commit 3e53d55

Browse files
committed
introduce setupConfigs()
1 parent 26513bb commit 3e53d55

File tree

11 files changed

+155
-43
lines changed

11 files changed

+155
-43
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ export type JsLintFileCb =
88
export type JsLoadPluginCb =
99
((arg0: string, arg1?: string | undefined | null) => Promise<string>)
1010

11+
/** JS callback to setup configs. */
12+
export type JsSetupConfigsCb =
13+
((arg0: string) => string)
14+
1115
/**
1216
* NAPI entry point.
1317
*
1418
* JS side passes in:
1519
* 1. `args`: Command line arguments (process.argv.slice(2))
1620
* 2. `load_plugin`: Load a JS plugin from a file path.
17-
* 3. `lint_file`: Lint a file.
21+
* 3. `setup_configs`: Setup configuration options.
22+
* 4. `lint_file`: Lint a file.
1823
*
1924
* Returns `true` if linting succeeded without errors, `false` otherwise.
2025
*/
21-
export declare function lint(args: Array<string>, loadPlugin: JsLoadPluginCb, lintFile: JsLintFileCb): Promise<boolean>
26+
export declare function lint(args: Array<string>, loadPlugin: JsLoadPluginCb, setupConfigs: JsSetupConfigsCb, lintFile: JsLintFileCb): Promise<boolean>

apps/oxlint/src-js/cli.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { debugAssertIsNonNull } from "./utils/asserts.js";
55
// Using `typeof wrapper` here makes TS check that the function signatures of `loadPlugin` and `loadPluginWrapper`
66
// are identical. Ditto `lintFile` and `lintFileWrapper`.
77
let loadPlugin: typeof loadPluginWrapper | null = null;
8+
let setupConfigs: typeof setupConfigsWrapper | null = null;
89
let lintFile: typeof lintFileWrapper | null = null;
910

1011
/**
@@ -21,14 +22,27 @@ function loadPluginWrapper(path: string, packageName: string | null): Promise<st
2122
// Use promises here instead of making `loadPluginWrapper` an async function,
2223
// to avoid a micro-tick and extra wrapper `Promise` in all later calls to `loadPluginWrapper`
2324
return import("./plugins/index.js").then((mod) => {
24-
({ loadPlugin, lintFile } = mod);
25+
({ loadPlugin, lintFile, setupConfigs } = mod);
2526
return loadPlugin(path, packageName);
2627
});
2728
}
2829
debugAssertIsNonNull(loadPlugin);
2930
return loadPlugin(path, packageName);
3031
}
3132

33+
/**
34+
* Bootstrap configuration options.
35+
*
36+
* Delegates to `setupConfigs`, which was lazy-loaded by `loadPluginWrapper`.
37+
*
38+
* @param optionsJSON - JSON serialization of an array containing all rule options across all configurations.
39+
* @returns "ok" on success or error message on failure
40+
*/
41+
function setupConfigsWrapper(optionsJSON: string): string {
42+
debugAssertIsNonNull(setupConfigs);
43+
return setupConfigs(optionsJSON);
44+
}
45+
3246
/**
3347
* Lint a file.
3448
*
@@ -59,8 +73,8 @@ function lintFileWrapper(
5973
// Get command line arguments, skipping first 2 (node binary and script path)
6074
const args = process.argv.slice(2);
6175

62-
// Call Rust, passing `loadPlugin` and `lintFile` as callbacks, and CLI arguments
63-
const success = await lint(args, loadPluginWrapper, lintFileWrapper);
76+
// Call Rust, passing `loadPlugin`, `setupConfigs`, and `lintFile` as callbacks, and CLI arguments
77+
const success = await lint(args, loadPluginWrapper, setupConfigsWrapper, lintFileWrapper);
6478

6579
// Note: It's recommended to set `process.exitCode` instead of calling `process.exit()`.
6680
// `process.exit()` kills the process immediately and `stdout` may not be flushed before process dies.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { setOptions } from "./options.js";
2+
3+
/**
4+
* Populates Rust-resolved configuration options on the JS side.
5+
* Called from Rust side after all configuration options have been resolved.
6+
*
7+
* Note: the name `setupConfigs` is currently incorrect, as we only populate rule options.
8+
* The intention is for this function to transfer all configurations in a multi-config workspace.
9+
* The configuration relevant to each file would then be resolved on the JS side.
10+
*
11+
* @param optionsJSON - JSON serialization of an array containing all rule options across all configurations.
12+
* @returns "ok" on success, or error message on failure
13+
*/
14+
export function setupConfigs(optionsJSON: string): string {
15+
// TODO: setup settings using this function
16+
try {
17+
setOptions(optionsJSON);
18+
// TODO: flesh out error handling.
19+
// Currently, the only procedure that may fail is `JSON.parse()`
20+
// in `setOptions()`, but it won't because the JSON string from
21+
// the rust side is serde serialized.
22+
return "ok";
23+
} catch (err) {
24+
return err instanceof Error ? err.message : String(err);
25+
}
26+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { lintFile } from "./lint.js";
22
export { loadPlugin } from "./load.js";
3+
export { setupConfigs } from "./config.js";

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

Lines changed: 1 addition & 1 deletion
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, areOptionsInitialized, DEFAULT_OPTIONS_ID, setOptions } from "./options.js";
3+
import { allOptions, DEFAULT_OPTIONS_ID } 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";

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

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -131,38 +131,21 @@ function mergeValues(configValue: JsonValue, defaultValue: JsonValue): JsonValue
131131
return freeze(merged);
132132
}
133133

134-
// Track if options have been initialized to avoid re-initialization
135-
let optionsInitialized = false;
136-
137134
/**
138135
* Set all external rule options.
139136
* Called once from Rust after config building, before any linting occurs.
140137
* @param optionsJson - JSON string of outer array of per-options arrays.
141138
*/
142139
export function setOptions(optionsJson: string): void {
143-
try {
144-
const parsed = JSON.parse(optionsJson);
145-
if (!Array.isArray(parsed)) throw new TypeError("Expected optionsJson to decode to an array");
140+
const parsed = JSON.parse(optionsJson);
141+
if (DEBUG) {
142+
if (!isArray(parsed))
143+
throw new TypeError("Expected optionsJson to decode to an array", { cause: parsed });
146144
// Basic shape validation: each element must be an array (options tuple array)
147145
for (let i = 0; i < parsed.length; i++) {
148146
const el = parsed[i];
149-
if (!Array.isArray(el)) throw new TypeError("Each options entry must be an array");
147+
if (!isArray(el)) throw new TypeError("Each options entry must be an array", { cause: el });
150148
}
151-
allOptions = parsed as Readonly<Options>[];
152-
optionsInitialized = true;
153-
} catch (err) {
154-
// Re-throw with clearer message for Rust side logging.
155-
throw new Error(
156-
"Failed to parse external rule options JSON: " +
157-
(err instanceof Error ? err.message : String(err)),
158-
);
159149
}
160-
}
161-
162-
/**
163-
* Check if options have been initialized.
164-
* @returns `true` if options have been set, `false` otherwise.
165-
*/
166-
export function areOptionsInitialized(): boolean {
167-
return optionsInitialized;
150+
allOptions = parsed as Readonly<Options>[];
168151
}

apps/oxlint/src/js_plugins/external_linter.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@ use oxc_linter::{
1515

1616
use crate::{
1717
generated::raw_transfer_constants::{BLOCK_ALIGN, BUFFER_SIZE},
18-
run::{JsLintFileCb, JsLoadPluginCb},
18+
run::{JsLintFileCb, JsLoadPluginCb, JsSetupConfigsCb},
1919
};
2020

2121
/// Wrap JS callbacks as normal Rust functions, and create [`ExternalLinter`].
2222
pub fn create_external_linter(
2323
load_plugin: JsLoadPluginCb,
24+
setup_configs: JsSetupConfigsCb,
2425
lint_file: JsLintFileCb,
2526
) -> ExternalLinter {
2627
let rust_load_plugin = wrap_load_plugin(load_plugin);
28+
let rust_setup_configs = wrap_setup_configs(setup_configs);
2729
let rust_lint_file = wrap_lint_file(lint_file);
2830

29-
ExternalLinter::new(rust_load_plugin, rust_lint_file)
31+
ExternalLinter::new(rust_load_plugin, rust_setup_configs, rust_lint_file)
3032
}
3133

3234
/// Wrap `loadPlugin` JS callback as a normal Rust function.
@@ -59,6 +61,45 @@ pub enum LintFileReturnValue {
5961
Failure(String),
6062
}
6163

64+
/// Wrap `setupConfigs` JS callback as a normal Rust function.
65+
///
66+
/// Use an `mpsc::channel` to wait for the result from JS side, and block current thread until `lintFile`
67+
/// completes execution.
68+
fn wrap_setup_configs(
69+
cb: JsSetupConfigsCb,
70+
) -> Box<dyn Fn(String) -> Result<(), String> + Send + Sync> {
71+
Box::new(move |options_json: String| {
72+
let (tx, rx) = channel();
73+
74+
// Send data to JS
75+
let status = cb.call_with_return_value(
76+
FnArgs::from((options_json,)),
77+
ThreadsafeFunctionCallMode::Blocking,
78+
move |result, _env| {
79+
let _ = match &result {
80+
Ok(r) => tx.send(Ok(r.clone())),
81+
Err(e) => tx.send(Err(e.to_string())),
82+
};
83+
result.map(|_| ())
84+
},
85+
);
86+
87+
assert!(status == Status::Ok, "Failed to schedule setupConfigs callback: {status:?}");
88+
89+
match rx.recv() {
90+
Ok(Ok(result)) => {
91+
if result == "ok" {
92+
Ok(())
93+
} else {
94+
Err(result)
95+
}
96+
}
97+
Ok(Err(err)) => Err(err),
98+
Err(err) => panic!("setupConfigs callback did not respond: {err}"),
99+
}
100+
})
101+
}
102+
62103
/// Wrap `lintFile` JS callback as a normal Rust function.
63104
///
64105
/// The returned function creates a `Uint8Array` referencing the memory of the given `Allocator`,

apps/oxlint/src/lint.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,17 @@ impl CliRunner {
291291
}
292292
};
293293

294+
// TODO: refactor this elsewhere.
295+
// This code is in the oxlint app, not in oxc_linter crate
296+
if let Some(ref external_linter) = external_linter
297+
&& let Err(err) = external_plugin_store.setup_configs(external_linter) {
298+
print_and_flush_stdout(
299+
stdout,
300+
&format!("Failed to setup external plugin options: {err}\n"),
301+
);
302+
return CliRunResult::InvalidOptionConfig;
303+
}
304+
294305
let report_unused_directives = match inline_config_options.report_unused_directives {
295306
ReportUnusedDirectives::WithoutSeverity(true) => Some(AllowWarnDeny::Warn),
296307
ReportUnusedDirectives::WithSeverity(Some(severity)) => Some(severity),

apps/oxlint/src/run.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,47 @@ pub type JsLintFileCb = ThreadsafeFunction<
5353
false,
5454
>;
5555

56+
/// JS callback to setup configs.
57+
#[napi]
58+
pub type JsSetupConfigsCb = ThreadsafeFunction<
59+
// Arguments
60+
FnArgs<(String,)>, // Stringified options array
61+
// Return value
62+
String, // Result ("ok" or error message)
63+
// Arguments (repeated)
64+
FnArgs<(String,)>,
65+
// Error status
66+
Status,
67+
// CalleeHandled
68+
false,
69+
>;
70+
5671
/// NAPI entry point.
5772
///
5873
/// JS side passes in:
5974
/// 1. `args`: Command line arguments (process.argv.slice(2))
6075
/// 2. `load_plugin`: Load a JS plugin from a file path.
61-
/// 3. `lint_file`: Lint a file.
76+
/// 3. `setup_configs`: Setup configuration options.
77+
/// 4. `lint_file`: Lint a file.
6278
///
6379
/// Returns `true` if linting succeeded without errors, `false` otherwise.
6480
#[expect(clippy::allow_attributes)]
6581
#[allow(clippy::trailing_empty_array, clippy::unused_async)] // https://github.com/napi-rs/napi-rs/issues/2758
6682
#[napi]
67-
pub async fn lint(args: Vec<String>, load_plugin: JsLoadPluginCb, lint_file: JsLintFileCb) -> bool {
68-
lint_impl(args, load_plugin, lint_file).await.report() == ExitCode::SUCCESS
83+
pub async fn lint(
84+
args: Vec<String>,
85+
load_plugin: JsLoadPluginCb,
86+
setup_configs: JsSetupConfigsCb,
87+
lint_file: JsLintFileCb,
88+
) -> bool {
89+
lint_impl(args, load_plugin, setup_configs, lint_file).await.report() == ExitCode::SUCCESS
6990
}
7091

7192
/// Run the linter.
7293
async fn lint_impl(
7394
args: Vec<String>,
7495
load_plugin: JsLoadPluginCb,
96+
setup_configs: JsSetupConfigsCb,
7597
lint_file: JsLintFileCb,
7698
) -> CliRunResult {
7799
// Convert String args to OsString for compatibility with bpaf
@@ -105,10 +127,11 @@ async fn lint_impl(
105127

106128
// JS plugins are only supported on 64-bit little-endian platforms at present
107129
#[cfg(all(target_pointer_width = "64", target_endian = "little"))]
108-
let external_linter = Some(crate::js_plugins::create_external_linter(load_plugin, lint_file));
130+
let external_linter =
131+
Some(crate::js_plugins::create_external_linter(load_plugin, setup_configs, lint_file));
109132
#[cfg(not(all(target_pointer_width = "64", target_endian = "little")))]
110133
let external_linter = {
111-
let (_, _) = (load_plugin, lint_file);
134+
let (_, _, _) = (load_plugin, setup_configs, lint_file);
112135
None
113136
};
114137

crates/oxc_linter/src/external_linter.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ pub type ExternalLinterLoadPluginCb = Box<
1010
+ Sync,
1111
>;
1212

13+
pub type ExternalLinterSetupConfigsCb = Box<dyn Fn(String) -> Result<(), String> + Send + Sync>;
14+
1315
pub type ExternalLinterLintFileCb = Box<
1416
dyn Fn(String, Vec<u32>, Vec<u32>, String, &Allocator) -> Result<Vec<LintFileResult>, String>
1517
+ Sync
@@ -46,15 +48,17 @@ pub struct JsFix {
4648

4749
pub struct ExternalLinter {
4850
pub(crate) load_plugin: ExternalLinterLoadPluginCb,
51+
pub(crate) setup_configs: ExternalLinterSetupConfigsCb,
4952
pub(crate) lint_file: ExternalLinterLintFileCb,
5053
}
5154

5255
impl ExternalLinter {
5356
pub fn new(
5457
load_plugin: ExternalLinterLoadPluginCb,
58+
setup_configs: ExternalLinterSetupConfigsCb,
5559
lint_file: ExternalLinterLintFileCb,
5660
) -> Self {
57-
Self { load_plugin, lint_file }
61+
Self { load_plugin, setup_configs, lint_file }
5862
}
5963
}
6064

0 commit comments

Comments
 (0)