Skip to content

Commit 2de9f39

Browse files
authored
fix(linter/plugins): fall back to package name if meta.name is missing (#14938)
Fixes #14937
1 parent b55df7f commit 2de9f39

File tree

11 files changed

+66
-18
lines changed

11 files changed

+66
-18
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export type JsLintFileCb =
66

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

1111
/**
1212
* NAPI entry point.

apps/oxlint/src-js/cli.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { lint } from './bindings.js';
66
let loadPlugin: typeof loadPluginWrapper | null = null;
77
let lintFile: typeof lintFileWrapper | null = null;
88

9-
function loadPluginWrapper(path: string): Promise<string> {
9+
function loadPluginWrapper(path: string, packageName?: string): Promise<string> {
1010
if (loadPlugin === null) {
1111
const require = createRequire(import.meta.url);
1212
// `plugins.js` is in root of `dist`. See `tsdown.config.ts`.
1313
({ loadPlugin, lintFile } = require('./plugins.js'));
1414
}
15-
return loadPlugin(path);
15+
return loadPlugin(path, packageName);
1616
}
1717

1818
function lintFileWrapper(filePath: string, bufferId: number, buffer: Uint8Array | null, ruleIds: number[]): string {

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ const ObjectKeys = Object.keys;
99

1010
// Linter plugin, comprising multiple rules
1111
export interface Plugin {
12-
meta: {
13-
name: string;
12+
meta?: {
13+
name?: string;
1414
};
1515
rules: {
1616
[key: string]: Rule;
@@ -80,11 +80,12 @@ interface PluginDetails {
8080
* containing try/catch.
8181
*
8282
* @param path - Absolute path of plugin file
83+
* @param packageName - Optional package name from package.json (fallback if plugin.meta.name is missing)
8384
* @returns JSON result
8485
*/
85-
export async function loadPlugin(path: string): Promise<string> {
86+
export async function loadPlugin(path: string, packageName?: string): Promise<string> {
8687
try {
87-
const res = await loadPluginImpl(path);
88+
const res = await loadPluginImpl(path, packageName);
8889
return JSON.stringify({ Success: res });
8990
} catch (err) {
9091
return JSON.stringify({ Failure: getErrorMessage(err) });
@@ -95,12 +96,13 @@ export async function loadPlugin(path: string): Promise<string> {
9596
* Load a plugin.
9697
*
9798
* @param path - Absolute path of plugin file
99+
* @param packageName - Optional package name from package.json (fallback if plugin.meta.name is missing)
98100
* @returns - Plugin details
99101
* @throws {Error} If plugin has already been registered
100102
* @throws {TypeError} If one of plugin's rules is malformed or its `createOnce` method returns invalid visitor
101103
* @throws {*} If plugin throws an error during import
102104
*/
103-
async function loadPluginImpl(path: string): Promise<PluginDetails> {
105+
async function loadPluginImpl(path: string, packageName?: string): Promise<PluginDetails> {
104106
if (registeredPluginPaths.has(path)) {
105107
throw new Error('This plugin has already been registered. This is a bug in Oxlint. Please report it.');
106108
}
@@ -110,7 +112,13 @@ async function loadPluginImpl(path: string): Promise<PluginDetails> {
110112
registeredPluginPaths.add(path);
111113

112114
// TODO: Use a validation library to assert the shape of the plugin, and of rules
113-
const pluginName = plugin.meta.name;
115+
// Get plugin name from plugin.meta.name, or fall back to package name from package.json
116+
const pluginName = plugin.meta?.name ?? packageName;
117+
if (!pluginName) {
118+
throw new TypeError(
119+
'Plugin must have either meta.name or be loaded from an npm package with a name field in package.json',
120+
);
121+
}
114122
const offset = registeredRules.length;
115123
const { rules } = plugin;
116124
const ruleNames = ObjectKeys(rules);

apps/oxlint/src/js_plugins/external_linter.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,15 @@ pub fn create_external_linter(
3737
/// The returned function will panic if called outside of a Tokio runtime.
3838
fn wrap_load_plugin(cb: JsLoadPluginCb) -> ExternalLinterLoadPluginCb {
3939
let cb = Arc::new(cb);
40-
Arc::new(move |plugin_path| {
40+
Arc::new(move |plugin_path, package_name| {
4141
let cb = Arc::clone(&cb);
4242
tokio::task::block_in_place(move || {
4343
tokio::runtime::Handle::current().block_on(async move {
44-
let result = cb.call_async(plugin_path).await?.into_future().await?;
44+
let result = cb
45+
.call_async(FnArgs::from((plugin_path, package_name)))
46+
.await?
47+
.into_future()
48+
.await?;
4549
let plugin_load_result: PluginLoadResult = serde_json::from_str(&result)?;
4650
Ok(plugin_load_result)
4751
})

apps/oxlint/src/run.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ use crate::{lint::CliRunner, result::CliRunResult};
1616
#[napi]
1717
pub type JsLoadPluginCb = ThreadsafeFunction<
1818
// Arguments
19-
String, // Absolute path of plugin file
19+
FnArgs<(String, Option<String>)>, // Absolute path of plugin file, optional package name
2020
// Return value
2121
Promise<String>, // `PluginLoadResult`, serialized to JSON
2222
// Arguments (repeated)
23-
String,
23+
FnArgs<(String, Option<String>)>,
2424
// Error status
2525
Status,
2626
// CalleeHandled

apps/oxlint/test/fixtures/load_paths/.oxlintrc.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"plugin10",
1313
"plugin11",
1414
"plugin12",
15-
"plugin13"
15+
"plugin13",
16+
"plugin14"
1617
],
1718
"rules": {
1819
"plugin1/no-debugger": "error",
@@ -27,6 +28,7 @@
2728
"plugin10/no-debugger": "error",
2829
"plugin11/no-debugger": "error",
2930
"plugin12/no-debugger": "error",
30-
"plugin13/no-debugger": "error"
31+
"plugin13/no-debugger": "error",
32+
"plugin14/no-debugger": "error"
3133
}
3234
}

apps/oxlint/test/fixtures/load_paths/node_modules/plugin14/index.js

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/oxlint/test/fixtures/load_paths/node_modules/plugin14/package.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/oxlint/test/fixtures/load_paths/output.snap.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
: ^^^^^^^^^
4141
`----
4242
43+
x plugin14(no-debugger): Unexpected Debugger Statement
44+
,-[files/index.js:1:1]
45+
1 | debugger;
46+
: ^^^^^^^^^
47+
`----
48+
4349
x plugin2(no-debugger): Unexpected Debugger Statement
4450
,-[files/index.js:1:1]
4551
1 | debugger;
@@ -88,7 +94,7 @@
8894
: ^^^^^^^^^
8995
`----
9096
91-
Found 1 warning and 13 errors.
97+
Found 1 warning and 14 errors.
9298
Finished in Xms on 1 file using X threads.
9399
```
94100

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,9 +538,12 @@ impl ConfigStoreBuilder {
538538
return Ok(());
539539
}
540540

541+
// Extract package name from package.json if available
542+
let package_name = resolved.package_json().and_then(|pkg| pkg.name().map(String::from));
543+
541544
let result = {
542545
let plugin_path = plugin_path.clone();
543-
(external_linter.load_plugin)(plugin_path).map_err(|e| {
546+
(external_linter.load_plugin)(plugin_path, package_name).map_err(|e| {
544547
ConfigBuilderError::PluginLoadFailed {
545548
plugin_specifier: plugin_specifier.to_string(),
546549
error: e.to_string(),

0 commit comments

Comments
 (0)