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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"plugins": ["jsdoc"],
"jsPlugins": [{ "name": "jsPluginJsDoc", "specifier": "./plugin.ts" }],
"categories": { "correctness": "off" },
"rules": {
"jsPluginJsDoc/no-debugger": "error",
"jsdoc/rule-name": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
debugger;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Exit code
1

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

Found 0 warnings and 1 error.
Finished in Xms on 1 file using X threads.
```

# stderr
```
WARNING: JS plugins are experimental and not subject to semver.
Breaking changes are possible while JS plugins support is under development.
```
23 changes: 23 additions & 0 deletions apps/oxlint/test/fixtures/custom_plugin_name_alias/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Plugin } from "../../../dist/index.js";

const plugin: Plugin = {
meta: {
name: "jsdoc",
},
rules: {
"no-debugger": {
create(context) {
return {
DebuggerStatement(debuggerStatement) {
context.report({
message: "Unexpected Debugger Statement",
node: debuggerStatement,
});
},
};
},
},
},
};

export default plugin;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"plugins": ["jsdoc"],
"jsPlugins": [{ "name": "jsdoc", "specifier": "./plugin.ts" }],
"rules": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
debugger;
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Exit code
1

# stdout
```
Failed to parse configuration file.

x Plugin name 'jsdoc' is reserved, and cannot be used for JS plugins.
|
| The 'jsdoc' plugin is already implemented natively in Rust within oxlint.
| Using both the native and JS versions would create ambiguity about which rules to use.
|
| To use an external 'jsdoc' plugin instead, provide a custom alias:
|
| "jsPlugins": [{ "name": "jsdoc-js", "specifier": "eslint-plugin-jsdoc" }]
|
| Then reference rules using your alias:
|
| "rules": {
| "jsdoc-js/rule-name": "error"
| }
|
| See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html
```

# stderr
```
WARNING: JS plugins are experimental and not subject to semver.
Breaking changes are possible while JS plugins support is under development.
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { Plugin } from "../../../dist/index.js";

const plugin: Plugin = {
meta: {
name: "jsdoc",
},
rules: {
"no-debugger": {
create(context) {
return {
DebuggerStatement(debuggerStatement) {
context.report({
message: "Unexpected Debugger Statement",
node: debuggerStatement,
});
},
};
},
},
},
};

export default plugin;
17 changes: 16 additions & 1 deletion apps/oxlint/test/fixtures/reserved_name/output.snap.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,22 @@
```
Failed to parse configuration file.

x Plugin name 'import' is reserved, and cannot be used for JS plugins
x Plugin name 'import' is reserved, and cannot be used for JS plugins.
|
| The 'import' plugin is already implemented natively in Rust within oxlint.
| Using both the native and JS versions would create ambiguity about which rules to use.
|
| To use an external 'import' plugin instead, provide a custom alias:
|
| "jsPlugins": [{ "name": "import-js", "specifier": "eslint-plugin-import" }]
|
| Then reference rules using your alias:
|
| "rules": {
| "import-js/rule-name": "error"
| }
|
| See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html
```

# stderr
Expand Down
46 changes: 34 additions & 12 deletions crates/oxc_linter/src/config/config_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use crate::{
RuleCategory, RuleEnum,
config::{
ESLintRule, OxlintOverrides, OxlintRules,
external_plugins::ExternalPluginEntry,
overrides::OxlintOverride,
plugins::{LintPlugins, normalize_plugin_name},
},
Expand Down Expand Up @@ -150,16 +151,15 @@ impl ConfigStoreBuilder {
let (oxlintrc, extended_paths) = resolve_oxlintrc_config(oxlintrc)?;

// Collect external plugins from both base config and overrides
let mut external_plugins: FxHashSet<(&PathBuf, &str)> = FxHashSet::default();
let mut external_plugins: FxHashSet<&ExternalPluginEntry> = FxHashSet::default();

if let Some(base_external_plugins) = &oxlintrc.external_plugins {
external_plugins.extend(base_external_plugins.iter().map(|(k, v)| (k, v.as_str())));
external_plugins.extend(base_external_plugins.iter());
}

for r#override in &oxlintrc.overrides {
if let Some(override_external_plugins) = &r#override.external_plugins {
external_plugins
.extend(override_external_plugins.iter().map(|(k, v)| (k, v.as_str())));
external_plugins.extend(override_external_plugins.iter());
}
}

Expand All @@ -169,9 +169,9 @@ impl ConfigStoreBuilder {
if !external_plugins.is_empty() && external_plugin_store.is_enabled() {
let Some(external_linter) = external_linter else {
#[expect(clippy::missing_panics_doc, reason = "infallible")]
let (_, original_specifier) = external_plugins.iter().next().unwrap();
let first_plugin = external_plugins.iter().next().unwrap();
return Err(ConfigBuilderError::NoExternalLinterConfigured {
plugin_specifier: (*original_specifier).to_string(),
plugin_specifier: first_plugin.specifier.clone(),
});
};

Expand All @@ -180,10 +180,11 @@ impl ConfigStoreBuilder {
..Default::default()
});

for (config_path, specifier) in &external_plugins {
for entry in &external_plugins {
Self::load_external_plugin(
config_path,
specifier,
&entry.config_dir,
&entry.specifier,
entry.name.as_deref(),
external_linter,
&resolver,
external_plugin_store,
Expand Down Expand Up @@ -523,6 +524,7 @@ impl ConfigStoreBuilder {
fn load_external_plugin(
resolve_dir: &Path,
plugin_specifier: &str,
alias: Option<&str>,
external_linter: &ExternalLinter,
resolver: &Resolver,
external_plugin_store: &mut ExternalPluginStore,
Expand Down Expand Up @@ -562,8 +564,13 @@ impl ConfigStoreBuilder {
}
})?;

// Normalize plugin name (e.g., "eslint-plugin-foo" -> "foo", "@foo/eslint-plugin" -> "@foo")
let plugin_name = normalize_plugin_name(&result.name).into_owned();
// Use alias if provided, otherwise normalize plugin name
let plugin_name = if let Some(alias_name) = alias {
alias_name.to_string()
} else {
// Normalize plugin name (e.g., "eslint-plugin-foo" -> "foo", "@foo/eslint-plugin" -> "@foo")
normalize_plugin_name(&result.name).into_owned()
};

if LintPlugins::try_from(plugin_name.as_str()).is_err() {
external_plugin_store.register_plugin(
Expand Down Expand Up @@ -652,7 +659,22 @@ impl Display for ConfigBuilderError {
ConfigBuilderError::ReservedExternalPluginName { plugin_name } => {
write!(
f,
"Plugin name '{plugin_name}' is reserved, and cannot be used for JS plugins",
"Plugin name '{plugin_name}' is reserved, and cannot be used for JS plugins.\n\
\n\
The '{plugin_name}' plugin is already implemented natively in Rust within oxlint.\n\
Using both the native and JS versions would create ambiguity about which rules to use.\n\
\n\
To use an external '{plugin_name}' plugin instead, provide a custom alias:\n\
\n\
\"jsPlugins\": [{{ \"name\": \"{plugin_name}-js\", \"specifier\": \"eslint-plugin-{plugin_name}\" }}]\n\
\n\
Then reference rules using your alias:\n\
\n\
\"rules\": {{\n\
\"{plugin_name}-js/rule-name\": \"error\"\n\
}}\n\
\n\
See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html",
)?;
Ok(())
}
Expand Down
Loading
Loading