Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/oxlint/src-js/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export type JsLintFileCb =

/** JS callback to load a JS plugin. */
export type JsLoadPluginCb =
((arg: string) => Promise<string>)
((arg0: string, arg1?: string | undefined | null) => Promise<string>)

/**
* NAPI entry point.
Expand Down
4 changes: 2 additions & 2 deletions apps/oxlint/src-js/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import { lint } from './bindings.js';
let loadPlugin: typeof loadPluginWrapper | null = null;
let lintFile: typeof lintFileWrapper | null = null;

function loadPluginWrapper(path: string): Promise<string> {
function loadPluginWrapper(path: string, packageName?: string): Promise<string> {
if (loadPlugin === null) {
const require = createRequire(import.meta.url);
// `plugins.js` is in root of `dist`. See `tsdown.config.ts`.
({ loadPlugin, lintFile } = require('./plugins.js'));
}
return loadPlugin(path);
return loadPlugin(path, packageName);
}

function lintFileWrapper(filePath: string, bufferId: number, buffer: Uint8Array | null, ruleIds: number[]): string {
Expand Down
20 changes: 14 additions & 6 deletions apps/oxlint/src-js/plugins/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const ObjectKeys = Object.keys;

// Linter plugin, comprising multiple rules
export interface Plugin {
meta: {
name: string;
meta?: {
name?: string;
};
rules: {
[key: string]: Rule;
Expand Down Expand Up @@ -80,11 +80,12 @@ interface PluginDetails {
* containing try/catch.
*
* @param path - Absolute path of plugin file
* @param packageName - Optional package name from package.json (fallback if plugin.meta.name is missing)
* @returns JSON result
*/
export async function loadPlugin(path: string): Promise<string> {
export async function loadPlugin(path: string, packageName?: string): Promise<string> {
try {
const res = await loadPluginImpl(path);
const res = await loadPluginImpl(path, packageName);
return JSON.stringify({ Success: res });
} catch (err) {
return JSON.stringify({ Failure: getErrorMessage(err) });
Expand All @@ -95,12 +96,13 @@ export async function loadPlugin(path: string): Promise<string> {
* Load a plugin.
*
* @param path - Absolute path of plugin file
* @param packageName - Optional package name from package.json (fallback if plugin.meta.name is missing)
* @returns - Plugin details
* @throws {Error} If plugin has already been registered
* @throws {TypeError} If one of plugin's rules is malformed or its `createOnce` method returns invalid visitor
* @throws {*} If plugin throws an error during import
*/
async function loadPluginImpl(path: string): Promise<PluginDetails> {
async function loadPluginImpl(path: string, packageName?: string): Promise<PluginDetails> {
if (registeredPluginPaths.has(path)) {
throw new Error('This plugin has already been registered. This is a bug in Oxlint. Please report it.');
}
Expand All @@ -110,7 +112,13 @@ async function loadPluginImpl(path: string): Promise<PluginDetails> {
registeredPluginPaths.add(path);

// TODO: Use a validation library to assert the shape of the plugin, and of rules
const pluginName = plugin.meta.name;
// Get plugin name from plugin.meta.name, or fall back to package name from package.json
const pluginName = plugin.meta?.name ?? packageName;
if (!pluginName) {
throw new TypeError(
'Plugin must have either meta.name or be loaded from an npm package with a name field in package.json',
);
}
const offset = registeredRules.length;
const { rules } = plugin;
const ruleNames = ObjectKeys(rules);
Expand Down
8 changes: 6 additions & 2 deletions apps/oxlint/src/js_plugins/external_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ pub fn create_external_linter(
/// The returned function will panic if called outside of a Tokio runtime.
fn wrap_load_plugin(cb: JsLoadPluginCb) -> ExternalLinterLoadPluginCb {
let cb = Arc::new(cb);
Arc::new(move |plugin_path| {
Arc::new(move |plugin_path, package_name| {
let cb = Arc::clone(&cb);
tokio::task::block_in_place(move || {
tokio::runtime::Handle::current().block_on(async move {
let result = cb.call_async(plugin_path).await?.into_future().await?;
let result = cb
.call_async(FnArgs::from((plugin_path, package_name)))
.await?
.into_future()
.await?;
let plugin_load_result: PluginLoadResult = serde_json::from_str(&result)?;
Ok(plugin_load_result)
})
Expand Down
4 changes: 2 additions & 2 deletions apps/oxlint/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ use crate::{lint::CliRunner, result::CliRunResult};
#[napi]
pub type JsLoadPluginCb = ThreadsafeFunction<
// Arguments
String, // Absolute path of plugin file
FnArgs<(String, Option<String>)>, // Absolute path of plugin file, optional package name
// Return value
Promise<String>, // `PluginLoadResult`, serialized to JSON
// Arguments (repeated)
String,
FnArgs<(String, Option<String>)>,
// Error status
Status,
// CalleeHandled
Expand Down
6 changes: 4 additions & 2 deletions apps/oxlint/test/fixtures/load_paths/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"plugin10",
"plugin11",
"plugin12",
"plugin13"
"plugin13",
"plugin14"
],
"rules": {
"plugin1/no-debugger": "error",
Expand All @@ -27,6 +28,7 @@
"plugin10/no-debugger": "error",
"plugin11/no-debugger": "error",
"plugin12/no-debugger": "error",
"plugin13/no-debugger": "error"
"plugin13/no-debugger": "error",
"plugin14/no-debugger": "error"
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion apps/oxlint/test/fixtures/load_paths/output.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
: ^^^^^^^^^
`----

x plugin14(no-debugger): Unexpected Debugger Statement
,-[files/index.js:1:1]
1 | debugger;
: ^^^^^^^^^
`----

x plugin2(no-debugger): Unexpected Debugger Statement
,-[files/index.js:1:1]
1 | debugger;
Expand Down Expand Up @@ -88,7 +94,7 @@
: ^^^^^^^^^
`----

Found 1 warning and 13 errors.
Found 1 warning and 14 errors.
Finished in Xms on 1 file using X threads.
```

Expand Down
5 changes: 4 additions & 1 deletion crates/oxc_linter/src/config/config_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,9 +538,12 @@ impl ConfigStoreBuilder {
return Ok(());
}

// Extract package name from package.json if available
let package_name = resolved.package_json().and_then(|pkg| pkg.name().map(String::from));

let result = {
let plugin_path = plugin_path.clone();
(external_linter.load_plugin)(plugin_path).map_err(|e| {
(external_linter.load_plugin)(plugin_path, package_name).map_err(|e| {
ConfigBuilderError::PluginLoadFailed {
plugin_specifier: plugin_specifier.to_string(),
error: e.to_string(),
Expand Down
5 changes: 4 additions & 1 deletion crates/oxc_linter/src/external_linter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use serde::Deserialize;
use oxc_allocator::Allocator;

pub type ExternalLinterLoadPluginCb = Arc<
dyn Fn(String) -> Result<PluginLoadResult, Box<dyn std::error::Error + Send + Sync>>
dyn Fn(
String,
Option<String>,
) -> Result<PluginLoadResult, Box<dyn std::error::Error + Send + Sync>>
+ Send
+ Sync,
>;
Expand Down
Loading