Skip to content

Commit a0f74a0

Browse files
camc314autofix-ci[bot]overlookmotel
authored
feat(linter/config): allow aliasing plugin names to allow names the same as builtin plugins (#15569)
fixes #15413 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
1 parent 6524f72 commit a0f74a0

File tree

17 files changed

+832
-116
lines changed

17 files changed

+832
-116
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"plugins": ["jsdoc"],
3+
"jsPlugins": [{ "name": "jsPluginJsDoc", "specifier": "./plugin.ts" }],
4+
"categories": { "correctness": "off" },
5+
"rules": {
6+
"jsPluginJsDoc/no-debugger": "error",
7+
"jsdoc/rule-name": "error"
8+
}
9+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
debugger;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x jsPluginJsDoc(no-debugger): Unexpected Debugger Statement
7+
,-[files/index.js:1:1]
8+
1 | debugger;
9+
: ^^^^^^^^^
10+
`----
11+
12+
Found 0 warnings and 1 error.
13+
Finished in Xms on 1 file using X threads.
14+
```
15+
16+
# stderr
17+
```
18+
WARNING: JS plugins are experimental and not subject to semver.
19+
Breaking changes are possible while JS plugins support is under development.
20+
```
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Plugin } from "../../../dist/index.js";
2+
3+
const plugin: Plugin = {
4+
meta: {
5+
name: "jsdoc",
6+
},
7+
rules: {
8+
"no-debugger": {
9+
create(context) {
10+
return {
11+
DebuggerStatement(debuggerStatement) {
12+
context.report({
13+
message: "Unexpected Debugger Statement",
14+
node: debuggerStatement,
15+
});
16+
},
17+
};
18+
},
19+
},
20+
},
21+
};
22+
23+
export default plugin;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"plugins": ["jsdoc"],
3+
"jsPlugins": [{ "name": "jsdoc", "specifier": "./plugin.ts" }],
4+
"rules": {}
5+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
debugger;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
Failed to parse configuration file.
7+
8+
x Plugin name 'jsdoc' is reserved, and cannot be used for JS plugins.
9+
|
10+
| The 'jsdoc' plugin is already implemented natively in Rust within oxlint.
11+
| Using both the native and JS versions would create ambiguity about which rules to use.
12+
|
13+
| To use an external 'jsdoc' plugin instead, provide a custom alias:
14+
|
15+
| "jsPlugins": [{ "name": "jsdoc-js", "specifier": "eslint-plugin-jsdoc" }]
16+
|
17+
| Then reference rules using your alias:
18+
|
19+
| "rules": {
20+
| "jsdoc-js/rule-name": "error"
21+
| }
22+
|
23+
| See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html
24+
```
25+
26+
# stderr
27+
```
28+
WARNING: JS plugins are experimental and not subject to semver.
29+
Breaking changes are possible while JS plugins support is under development.
30+
```
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Plugin } from "../../../dist/index.js";
2+
3+
const plugin: Plugin = {
4+
meta: {
5+
name: "jsdoc",
6+
},
7+
rules: {
8+
"no-debugger": {
9+
create(context) {
10+
return {
11+
DebuggerStatement(debuggerStatement) {
12+
context.report({
13+
message: "Unexpected Debugger Statement",
14+
node: debuggerStatement,
15+
});
16+
},
17+
};
18+
},
19+
},
20+
},
21+
};
22+
23+
export default plugin;

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

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@
55
```
66
Failed to parse configuration file.
77
8-
x Plugin name 'import' is reserved, and cannot be used for JS plugins
8+
x Plugin name 'import' is reserved, and cannot be used for JS plugins.
9+
|
10+
| The 'import' plugin is already implemented natively in Rust within oxlint.
11+
| Using both the native and JS versions would create ambiguity about which rules to use.
12+
|
13+
| To use an external 'import' plugin instead, provide a custom alias:
14+
|
15+
| "jsPlugins": [{ "name": "import-js", "specifier": "eslint-plugin-import" }]
16+
|
17+
| Then reference rules using your alias:
18+
|
19+
| "rules": {
20+
| "import-js/rule-name": "error"
21+
| }
22+
|
23+
| See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html
924
```
1025

1126
# stderr

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::{
1515
RuleCategory, RuleEnum,
1616
config::{
1717
ESLintRule, OxlintOverrides, OxlintRules,
18+
external_plugins::ExternalPluginEntry,
1819
overrides::OxlintOverride,
1920
plugins::{LintPlugins, normalize_plugin_name},
2021
},
@@ -150,16 +151,15 @@ impl ConfigStoreBuilder {
150151
let (oxlintrc, extended_paths) = resolve_oxlintrc_config(oxlintrc)?;
151152

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

155156
if let Some(base_external_plugins) = &oxlintrc.external_plugins {
156-
external_plugins.extend(base_external_plugins.iter().map(|(k, v)| (k, v.as_str())));
157+
external_plugins.extend(base_external_plugins.iter());
157158
}
158159

159160
for r#override in &oxlintrc.overrides {
160161
if let Some(override_external_plugins) = &r#override.external_plugins {
161-
external_plugins
162-
.extend(override_external_plugins.iter().map(|(k, v)| (k, v.as_str())));
162+
external_plugins.extend(override_external_plugins.iter());
163163
}
164164
}
165165

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

@@ -180,10 +180,11 @@ impl ConfigStoreBuilder {
180180
..Default::default()
181181
});
182182

183-
for (config_path, specifier) in &external_plugins {
183+
for entry in &external_plugins {
184184
Self::load_external_plugin(
185-
config_path,
186-
specifier,
185+
&entry.config_dir,
186+
&entry.specifier,
187+
entry.name.as_deref(),
187188
external_linter,
188189
&resolver,
189190
external_plugin_store,
@@ -523,6 +524,7 @@ impl ConfigStoreBuilder {
523524
fn load_external_plugin(
524525
resolve_dir: &Path,
525526
plugin_specifier: &str,
527+
alias: Option<&str>,
526528
external_linter: &ExternalLinter,
527529
resolver: &Resolver,
528530
external_plugin_store: &mut ExternalPluginStore,
@@ -562,8 +564,13 @@ impl ConfigStoreBuilder {
562564
}
563565
})?;
564566

565-
// Normalize plugin name (e.g., "eslint-plugin-foo" -> "foo", "@foo/eslint-plugin" -> "@foo")
566-
let plugin_name = normalize_plugin_name(&result.name).into_owned();
567+
// Use alias if provided, otherwise normalize plugin name
568+
let plugin_name = if let Some(alias_name) = alias {
569+
alias_name.to_string()
570+
} else {
571+
// Normalize plugin name (e.g., "eslint-plugin-foo" -> "foo", "@foo/eslint-plugin" -> "@foo")
572+
normalize_plugin_name(&result.name).into_owned()
573+
};
567574

568575
if LintPlugins::try_from(plugin_name.as_str()).is_err() {
569576
external_plugin_store.register_plugin(
@@ -652,7 +659,22 @@ impl Display for ConfigBuilderError {
652659
ConfigBuilderError::ReservedExternalPluginName { plugin_name } => {
653660
write!(
654661
f,
655-
"Plugin name '{plugin_name}' is reserved, and cannot be used for JS plugins",
662+
"Plugin name '{plugin_name}' is reserved, and cannot be used for JS plugins.\n\
663+
\n\
664+
The '{plugin_name}' plugin is already implemented natively in Rust within oxlint.\n\
665+
Using both the native and JS versions would create ambiguity about which rules to use.\n\
666+
\n\
667+
To use an external '{plugin_name}' plugin instead, provide a custom alias:\n\
668+
\n\
669+
\"jsPlugins\": [{{ \"name\": \"{plugin_name}-js\", \"specifier\": \"eslint-plugin-{plugin_name}\" }}]\n\
670+
\n\
671+
Then reference rules using your alias:\n\
672+
\n\
673+
\"rules\": {{\n\
674+
\"{plugin_name}-js/rule-name\": \"error\"\n\
675+
}}\n\
676+
\n\
677+
See: https://oxc.rs/docs/guide/usage/linter/js-plugins.html",
656678
)?;
657679
Ok(())
658680
}

0 commit comments

Comments
 (0)