feat(linter/config): allow aliasing plugin names to allow names the same as builtin plugins (#15569)

fixes https://github.com/oxc-project/oxc/issues/15413

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: overlookmotel <theoverlookmotel@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-12-17 19:17:36 +00:00
1 parent 6524f72742
commit a0f74a0fb2
17 files changed
+832 -116

No files matched your search

@@ -0,0 +1,9 @@
{
"plugins": ["jsdoc"],
"jsPlugins": [{ "name": "jsPluginJsDoc", "specifier": "./plugin.ts" }],
"categories": { "correctness": "off" },
"rules": {
"jsPluginJsDoc/no-debugger": "error",
"jsdoc/rule-name": "error"
}
}
@@ -0,0 +1 @@
debugger;
@@ -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.
```
@@ -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;
@@ -0,0 +1,5 @@
{
"plugins": ["jsdoc"],
"jsPlugins": [{ "name": "jsdoc", "specifier": "./plugin.ts" }],
"rules": {}
}
@@ -0,0 +1 @@
debugger;
@@ -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.
```
@@ -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;
+16 -1
View File
@@ -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
+34 -12
View File
@@ -15,6 +15,7 @@ use crate::{
RuleCategory, RuleEnum,
config::{
ESLintRule, OxlintOverrides, OxlintRules,
external_plugins::ExternalPluginEntry,
overrides::OxlintOverride,
plugins::{LintPlugins, normalize_plugin_name},
},
@@ -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());
}
}
@@ -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(),
});
};
@@ -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,
@@ -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,
@@ -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(
@@ -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(())
}
@@ -0,0 +1,373 @@
use std::{fmt, path::PathBuf};
use rustc_hash::FxHashSet;
use schemars::{
JsonSchema, SchemaGenerator,
schema::{
ArrayValidation, InstanceType, Metadata, ObjectValidation, Schema, SchemaObject,
SubschemaValidation,
},
};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{Error, SeqAccess, Visitor},
};
/// External plugin entry containing the plugin specifier and optional custom name
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExternalPluginEntry {
/// Directory containing the config file that specified this plugin
pub config_dir: PathBuf,
/// Plugin specifier (path, package name, or URL)
pub specifier: String,
/// Optional custom name/alias for the plugin
pub name: Option<String>,
}
/// Custom deserializer for external plugins
/// Supports:
/// - Array of strings: `["plugin1", "plugin2"]`
/// - Array of objects: `[{ "name": "alias", "specifier": "plugin" }]`
/// - Mixed array: `["plugin1", { "name": "alias", "specifier": "plugin2" }]`
pub fn deserialize_external_plugins<'de, D>(
deserializer: D,
) -> Result<Option<FxHashSet<ExternalPluginEntry>>, D::Error>
where
D: Deserializer<'de>,
{
struct PluginSetVisitor;
impl<'de> Visitor<'de> for PluginSetVisitor {
type Value = Option<FxHashSet<ExternalPluginEntry>>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("null or an array of plugin entries")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: Error,
{
Ok(None)
}
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: Error,
{
Ok(None)
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut plugins = FxHashSet::default();
while let Some(entry) = seq.next_element::<ExternalPluginEntry>()? {
plugins.insert(entry);
}
Ok(Some(plugins))
}
}
deserializer.deserialize_any(PluginSetVisitor)
}
impl<'de> Deserialize<'de> for ExternalPluginEntry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct PluginObject {
name: String,
specifier: String,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum PluginEntry {
String(String),
Object(PluginObject),
}
let entry = PluginEntry::deserialize(deserializer)?;
Ok(match entry {
PluginEntry::String(specifier) => {
ExternalPluginEntry { config_dir: PathBuf::default(), specifier, name: None }
}
PluginEntry::Object(obj) => ExternalPluginEntry {
config_dir: PathBuf::default(),
specifier: obj.specifier,
name: Some(obj.name),
},
})
}
}
impl Serialize for ExternalPluginEntry {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
#[serde(untagged)]
enum PluginEntry<'a> {
String(&'a str),
Object { name: &'a str, specifier: &'a str },
}
if let Some(alias_name) = &self.name {
PluginEntry::Object { name: alias_name.as_str(), specifier: self.specifier.as_str() }
.serialize(serializer)
} else {
PluginEntry::String(&self.specifier).serialize(serializer)
}
}
}
/// Custom JSON schema generator for external plugins that includes uniqueItems constraint
pub fn external_plugins_schema(generator: &mut SchemaGenerator) -> Schema {
let entry_schema = generator.subschema_for::<ExternalPluginEntry>();
let array_schema = SchemaObject {
instance_type: Some(InstanceType::Array.into()),
array: Some(Box::new(ArrayValidation {
items: Some(entry_schema.into()),
unique_items: Some(true),
..Default::default()
})),
..Default::default()
};
SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![
SchemaObject {
instance_type: Some(InstanceType::Null.into()),
..Default::default()
}
.into(),
array_schema.into(),
]),
..Default::default()
})),
..Default::default()
}
.into()
}
impl JsonSchema for ExternalPluginEntry {
fn schema_name() -> String {
"ExternalPluginEntry".to_string()
}
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
// Schema represents: string | { name: string, specifier: string }
let string_schema = SchemaObject {
instance_type: Some(InstanceType::String.into()),
metadata: Some(Box::new(Metadata {
description: Some("Path or package name of the plugin".to_string()),
..Default::default()
})),
..Default::default()
};
let mut object_properties = schemars::Map::new();
object_properties.insert(
"name".to_string(),
SchemaObject {
instance_type: Some(InstanceType::String.into()),
metadata: Some(Box::new(Metadata {
description: Some(
"Custom name/alias for the plugin.\n\n\
Note: The following plugin names are reserved because they are implemented \
natively in Rust within oxlint and cannot be used for JS plugins:\n\
- react (includes react-hooks)\n\
- unicorn\n\
- typescript\n\
- oxc\n\
- import (includes import-x)\n\
- jsdoc\n\
- jest\n\
- vitest\n\
- jsx-a11y\n\
- nextjs\n\
- react-perf\n\
- promise\n\
- node\n\
- regex\n\
- vue\n\
- eslint\n\n\
If you need to use the JavaScript version of any of these plugins, \
provide a custom alias to avoid conflicts."
.to_string()
),
..Default::default()
})),
..Default::default()
}
.into(),
);
object_properties.insert(
"specifier".to_string(),
SchemaObject {
instance_type: Some(InstanceType::String.into()),
metadata: Some(Box::new(Metadata {
description: Some("Path or package name of the plugin".to_string()),
..Default::default()
})),
..Default::default()
}
.into(),
);
let object_schema = SchemaObject {
instance_type: Some(InstanceType::Object.into()),
metadata: Some(Box::new(Metadata {
description: Some("Plugin with custom name/alias".to_string()),
..Default::default()
})),
object: Some(Box::new(ObjectValidation {
properties: object_properties,
required: vec!["name".to_string(), "specifier".to_string()].into_iter().collect(),
additional_properties: Some(Box::new(Schema::Bool(false))),
..Default::default()
})),
..Default::default()
};
SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(vec![string_schema.into(), object_schema.into()]),
..Default::default()
})),
..Default::default()
}
.into()
}
}
#[cfg(test)]
mod test {
use rustc_hash::FxHashSet;
use super::*;
#[test]
fn test_deserialize() {
#[derive(Deserialize)]
struct TestConfig {
#[serde(
rename = "jsPlugins",
default,
deserialize_with = "deserialize_external_plugins"
)]
plugins: Option<FxHashSet<ExternalPluginEntry>>,
}
let json = serde_json::json!({
"jsPlugins": [
"./plugin.ts",
{ "name": "custom", "specifier": "./plugin2.ts" }
]
});
let config: TestConfig = serde_json::from_value(json).unwrap();
let plugins = config.plugins.as_ref().unwrap();
assert_eq!(plugins.len(), 2);
assert_eq!(plugins.iter().filter(|e| e.name.is_some()).count(), 1);
// Null
let json = serde_json::json!({ "jsPlugins": null });
let config: TestConfig = serde_json::from_value(json).unwrap();
assert!(config.plugins.is_none());
// Empty array
let json = serde_json::json!({ "jsPlugins": [] });
let config: TestConfig = serde_json::from_value(json).unwrap();
assert_eq!(config.plugins.unwrap().len(), 0);
}
#[test]
fn test_deserialize_mixed_formats() {
#[derive(Deserialize)]
struct TestConfig {
#[serde(
rename = "jsPlugins",
default,
deserialize_with = "deserialize_external_plugins"
)]
plugins: Option<FxHashSet<ExternalPluginEntry>>,
}
// Mix string and object formats
let json = serde_json::json!({
"jsPlugins": [
"eslint-plugin-import",
{ "name": "custom", "specifier": "./plugin.ts" }
]
});
let config: TestConfig = serde_json::from_value(json).unwrap();
let plugins = config.plugins.as_ref().unwrap();
assert_eq!(plugins.len(), 2);
assert_eq!(plugins.iter().filter(|e| e.name.is_some()).count(), 1);
}
#[test]
fn test_deserialize_rejects_invalid() {
#[derive(Deserialize)]
struct TestConfig {
#[expect(dead_code)]
#[serde(
rename = "jsPlugins",
default,
deserialize_with = "deserialize_external_plugins"
)]
plugins: Option<FxHashSet<ExternalPluginEntry>>,
}
// Extra fields should be rejected
let json = serde_json::json!({
"jsPlugins": [
{ "name": "x", "specifier": "y", "extra": "z" }
]
});
assert!(serde_json::from_value::<TestConfig>(json).is_err());
// Missing required fields should be rejected
let json = serde_json::json!({ "jsPlugins": [{ "name": "x" }] });
assert!(serde_json::from_value::<TestConfig>(json).is_err());
// Object with single arbitrary field should be rejected
let json = serde_json::json!({ "jsPlugins": [{ "alias": "plugin" }] });
assert!(serde_json::from_value::<TestConfig>(json).is_err());
}
#[test]
fn test_serialize() {
let mut plugins = FxHashSet::default();
plugins.insert(ExternalPluginEntry {
config_dir: PathBuf::default(),
specifier: "./plugin.ts".to_string(),
name: None,
});
plugins.insert(ExternalPluginEntry {
config_dir: PathBuf::default(),
specifier: "./plugin2.ts".to_string(),
name: Some("custom".to_string()),
});
let json = serde_json::to_value(Some(plugins)).unwrap();
let arr = json.as_array().unwrap();
assert_eq!(arr.len(), 2);
// Null
let json = serde_json::to_value(&None::<FxHashSet<ExternalPluginEntry>>).unwrap();
assert!(json.is_null());
}
}
+1
View File
@@ -4,6 +4,7 @@ mod categories;
mod config_builder;
mod config_store;
mod env;
mod external_plugins;
mod globals;
mod ignore_matcher;
mod overrides;
+9 -35
View File
@@ -1,15 +1,18 @@
use std::{
borrow::Cow,
ops::{Deref, DerefMut},
path::PathBuf,
};
use rustc_hash::FxHashSet;
use schemars::{JsonSchema, r#gen, schema::Schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserialize, Deserializer, Serialize};
use crate::{LintPlugins, OxlintEnv, OxlintGlobals, config::OxlintRules};
use super::external_plugins::{
ExternalPluginEntry, deserialize_external_plugins, external_plugins_schema,
};
// nominal wrapper required to add JsonSchema impl
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct OxlintOverrides(Vec<OxlintOverride>);
@@ -101,15 +104,12 @@ pub struct OxlintOverride {
/// They are not supported in language server at present.
#[serde(
rename = "jsPlugins",
deserialize_with = "deserialize_external_plugins_override",
serialize_with = "serialize_external_plugins_override",
default,
skip_serializing_if = "Option::is_none"
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_external_plugins"
)]
#[schemars(with = "Option<FxHashSet<String>>")]
pub external_plugins: Option<
FxHashSet<(PathBuf /* config file directory */, String /* plugin specifier */)>,
>,
#[schemars(schema_with = "external_plugins_schema")]
pub external_plugins: Option<FxHashSet<ExternalPluginEntry>>,
#[serde(default)]
pub rules: OxlintRules,
@@ -150,32 +150,6 @@ impl GlobSet {
}
}
fn deserialize_external_plugins_override<'de, D>(
deserializer: D,
) -> Result<Option<FxHashSet<(PathBuf, String)>>, D::Error>
where
D: Deserializer<'de>,
{
let opt_set: Option<FxHashSet<String>> = Option::deserialize(deserializer)?;
Ok(opt_set
.map(|set| set.into_iter().map(|specifier| (PathBuf::default(), specifier)).collect()))
}
#[expect(clippy::ref_option)]
fn serialize_external_plugins_override<S>(
plugins: &Option<FxHashSet<(PathBuf, String)>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Serialize as an array of original specifiers (the values in the map)
match plugins {
Some(set) => serializer.collect_seq(set.iter().map(|(_, specifier)| specifier)),
None => serializer.serialize_none(),
}
}
#[cfg(test)]
mod test {
use crate::config::{globals::GlobalValue, plugins::LintPlugins};
+91 -38
View File
@@ -5,15 +5,22 @@ use std::{
use rustc_hash::{FxHashMap, FxHashSet};
use schemars::{JsonSchema, schema_for};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde::{Deserialize, Serialize};
use oxc_diagnostics::OxcDiagnostic;
use crate::{LintPlugins, utils::read_to_string};
use super::{
categories::OxlintCategories, env::OxlintEnv, globals::OxlintGlobals,
overrides::OxlintOverrides, rules::OxlintRules, settings::OxlintSettings,
categories::OxlintCategories,
env::OxlintEnv,
external_plugins::{
ExternalPluginEntry, deserialize_external_plugins, external_plugins_schema,
},
globals::OxlintGlobals,
overrides::OxlintOverrides,
rules::OxlintRules,
settings::OxlintSettings,
};
/// Oxlint Configuration File
@@ -76,13 +83,12 @@ pub struct Oxlintrc {
/// They are not supported in language server at present.
#[serde(
rename = "jsPlugins",
deserialize_with = "deserialize_external_plugins",
serialize_with = "serialize_external_plugins",
default,
skip_serializing_if = "Option::is_none"
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_external_plugins"
)]
#[schemars(with = "Option<FxHashSet<String>>")]
pub external_plugins: Option<FxHashSet<(PathBuf, String)>>,
#[schemars(schema_with = "external_plugins_schema")]
pub external_plugins: Option<FxHashSet<ExternalPluginEntry>>,
pub categories: OxlintCategories,
/// Example
///
@@ -170,7 +176,10 @@ impl Oxlintrc {
if let Some(external_plugins) = &mut config.external_plugins {
*external_plugins = std::mem::take(external_plugins)
.into_iter()
.map(|(_, specifier)| (config_dir.to_path_buf(), specifier))
.map(|mut entry| {
entry.config_dir = config_dir.to_path_buf();
entry
})
.collect();
}
@@ -178,7 +187,10 @@ impl Oxlintrc {
if let Some(external_plugins) = &mut override_config.external_plugins {
*external_plugins = std::mem::take(external_plugins)
.into_iter()
.map(|(_, specifier)| (config_dir.to_path_buf(), specifier))
.map(|mut entry| {
entry.config_dir = config_dir.to_path_buf();
entry
})
.collect();
}
}
@@ -315,7 +327,7 @@ impl Oxlintrc {
let external_plugins = match (&self.external_plugins, &other.external_plugins) {
(Some(self_external), Some(other_external)) => {
Some(self_external.iter().chain(other_external).cloned().collect())
Some(self_external.iter().chain(other_external.iter()).cloned().collect())
}
(Some(self_external), None) => Some(self_external.clone()),
(None, Some(other_external)) => Some(other_external.clone()),
@@ -342,37 +354,12 @@ fn is_json_ext(ext: &str) -> bool {
ext == "json" || ext == "jsonc"
}
fn deserialize_external_plugins<'de, D>(
deserializer: D,
) -> Result<Option<FxHashSet<(PathBuf, String)>>, D::Error>
where
D: Deserializer<'de>,
{
let opt_set: Option<FxHashSet<String>> = Option::deserialize(deserializer)?;
Ok(opt_set
.map(|set| set.into_iter().map(|specifier| (PathBuf::default(), specifier)).collect()))
}
#[expect(clippy::ref_option)]
fn serialize_external_plugins<S>(
plugins: &Option<FxHashSet<(PathBuf, String)>>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
// Serialize as an array of original specifiers (the values in the map)
match plugins {
Some(set) => serializer.collect_seq(set.iter().map(|(_, specifier)| specifier)),
None => serializer.serialize_none(),
}
}
#[cfg(test)]
mod test {
use rustc_hash::FxHashSet;
use serde_json::json;
use crate::config::plugins::LintPlugins;
use crate::config::{external_plugins::ExternalPluginEntry, plugins::LintPlugins};
use super::*;
@@ -435,4 +422,70 @@ mod test {
let config: Oxlintrc = serde_json::from_str(r#"{"extends": []}"#).unwrap();
assert_eq!(0, config.extends.len());
}
#[test]
fn test_oxlintrc_js_plugins() {
let config: Oxlintrc = serde_json::from_str(
r#"{"jsPlugins": ["./plugin.ts", { "name": "custom", "specifier": "./plugin2.ts" }]}"#,
)
.unwrap();
assert_eq!(config.external_plugins.as_ref().unwrap().len(), 2);
// None
let config: Oxlintrc = serde_json::from_str(r"{}").unwrap();
assert!(config.external_plugins.is_none());
// Empty array
let config: Oxlintrc = serde_json::from_str(r#"{"jsPlugins": []}"#).unwrap();
assert_eq!(config.external_plugins.as_ref().unwrap().len(), 0);
}
#[test]
fn test_oxlintrc_js_plugins_rejects_invalid() {
// Extra fields should be rejected
assert!(
serde_json::from_str::<Oxlintrc>(
r#"{"jsPlugins": [{ "name": "x", "specifier": "y", "extra": "z" }]}"#
)
.is_err()
);
// Missing required fields should be rejected
assert!(serde_json::from_str::<Oxlintrc>(r#"{"jsPlugins": [{ "name": "x" }]}"#).is_err());
// Object with arbitrary field should be rejected
assert!(
serde_json::from_str::<Oxlintrc>(r#"{"jsPlugins": [{ "myAlias": "my-plugin" }]}"#)
.is_err()
);
}
#[test]
fn test_oxlintrc_js_plugins_roundtrip() {
let mut config = Oxlintrc::default();
let mut plugins = FxHashSet::default();
plugins.insert(ExternalPluginEntry {
config_dir: PathBuf::default(),
specifier: "./plugin.ts".to_string(),
name: None,
});
plugins.insert(ExternalPluginEntry {
config_dir: PathBuf::default(),
specifier: "./plugin2.ts".to_string(),
name: Some("custom".to_string()),
});
config.external_plugins = Some(plugins);
let serialized = serde_json::to_string(&config).unwrap();
let deserialized: Oxlintrc = serde_json::from_str(&serialized).unwrap();
assert_eq!(config.external_plugins, deserialized.external_plugins);
}
#[test]
fn test_oxlintrc_js_plugins_merge() {
let config1: Oxlintrc = serde_json::from_str(r#"{"jsPlugins": ["./plugin1.ts"]}"#).unwrap();
let config2: Oxlintrc = serde_json::from_str(r#"{"jsPlugins": ["./plugin2.ts"]}"#).unwrap();
let merged = config1.merge(config2);
assert_eq!(merged.external_plugins.unwrap().len(), 2);
}
}
@@ -57,14 +57,18 @@ expression: json
},
"jsPlugins": {
"description": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
"type": [
"array",
"null"
"anyOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/ExternalPluginEntry"
},
"uniqueItems": true
}
],
"items": {
"type": "string"
},
"uniqueItems": true,
"markdownDescription": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present."
},
"overrides": {
@@ -226,6 +230,37 @@ expression: json
},
"markdownDescription": "See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html)"
},
"ExternalPluginEntry": {
"anyOf": [
{
"description": "Path or package name of the plugin",
"type": "string",
"markdownDescription": "Path or package name of the plugin"
},
{
"description": "Plugin with custom name/alias",
"type": "object",
"required": [
"name",
"specifier"
],
"properties": {
"name": {
"description": "Custom name/alias for the plugin.\n\nNote: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:\n- react (includes react-hooks)\n- unicorn\n- typescript\n- oxc\n- import (includes import-x)\n- jsdoc\n- jest\n- vitest\n- jsx-a11y\n- nextjs\n- react-perf\n- promise\n- node\n- regex\n- vue\n- eslint\n\nIf you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts.",
"type": "string",
"markdownDescription": "Custom name/alias for the plugin.\n\nNote: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:\n- react (includes react-hooks)\n- unicorn\n- typescript\n- oxc\n- import (includes import-x)\n- jsdoc\n- jest\n- vitest\n- jsx-a11y\n- nextjs\n- react-perf\n- promise\n- node\n- regex\n- vue\n- eslint\n\nIf you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts."
},
"specifier": {
"description": "Path or package name of the plugin",
"type": "string",
"markdownDescription": "Path or package name of the plugin"
}
},
"additionalProperties": false,
"markdownDescription": "Plugin with custom name/alias"
}
]
},
"GlobSet": {
"description": "A set of glob patterns.",
"type": "array",
@@ -484,14 +519,18 @@ expression: json
},
"jsPlugins": {
"description": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
"type": [
"array",
"null"
"anyOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/ExternalPluginEntry"
},
"uniqueItems": true
}
],
"items": {
"type": "string"
},
"uniqueItems": true,
"markdownDescription": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present."
},
"plugins": {
+53 -14
View File
@@ -53,14 +53,18 @@
},
"jsPlugins": {
"description": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
"type": [
"array",
"null"
"anyOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/ExternalPluginEntry"
},
"uniqueItems": true
}
],
"items": {
"type": "string"
},
"uniqueItems": true,
"markdownDescription": "JS plugins.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present."
},
"overrides": {
@@ -222,6 +226,37 @@
},
"markdownDescription": "See [Oxlint Rules](https://oxc.rs/docs/guide/usage/linter/rules.html)"
},
"ExternalPluginEntry": {
"anyOf": [
{
"description": "Path or package name of the plugin",
"type": "string",
"markdownDescription": "Path or package name of the plugin"
},
{
"description": "Plugin with custom name/alias",
"type": "object",
"required": [
"name",
"specifier"
],
"properties": {
"name": {
"description": "Custom name/alias for the plugin.\n\nNote: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:\n- react (includes react-hooks)\n- unicorn\n- typescript\n- oxc\n- import (includes import-x)\n- jsdoc\n- jest\n- vitest\n- jsx-a11y\n- nextjs\n- react-perf\n- promise\n- node\n- regex\n- vue\n- eslint\n\nIf you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts.",
"type": "string",
"markdownDescription": "Custom name/alias for the plugin.\n\nNote: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:\n- react (includes react-hooks)\n- unicorn\n- typescript\n- oxc\n- import (includes import-x)\n- jsdoc\n- jest\n- vitest\n- jsx-a11y\n- nextjs\n- react-perf\n- promise\n- node\n- regex\n- vue\n- eslint\n\nIf you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts."
},
"specifier": {
"description": "Path or package name of the plugin",
"type": "string",
"markdownDescription": "Path or package name of the plugin"
}
},
"additionalProperties": false,
"markdownDescription": "Plugin with custom name/alias"
}
]
},
"GlobSet": {
"description": "A set of glob patterns.",
"type": "array",
@@ -480,14 +515,18 @@
},
"jsPlugins": {
"description": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present.",
"type": [
"array",
"null"
"anyOf": [
{
"type": "null"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/ExternalPluginEntry"
},
"uniqueItems": true
}
],
"items": {
"type": "string"
},
"uniqueItems": true,
"markdownDescription": "JS plugins for this override.\n\nNote: JS plugins are experimental and not subject to semver.\nThey are not supported in language server at present."
},
"plugins": {
@@ -198,7 +198,7 @@ Globs to ignore during linting. These are resolved from the configuration file p
## jsPlugins
type: `string[]`
type: `array | null`
JS plugins.
@@ -207,6 +207,50 @@ Note: JS plugins are experimental and not subject to semver.
They are not supported in language server at present.
### jsPlugins[n]
type: `object | string`
#### jsPlugins[n].name
type: `string`
Custom name/alias for the plugin.
Note: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:
- react (includes react-hooks)
- unicorn
- typescript
- oxc
- import (includes import-x)
- jsdoc
- jest
- vitest
- jsx-a11y
- nextjs
- react-perf
- promise
- node
- regex
- vue
- eslint
If you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts.
#### jsPlugins[n].specifier
type: `string`
Path or package name of the plugin
## overrides
type: `array`
@@ -249,7 +293,7 @@ Enabled or disabled specific global variables.
#### overrides[n].jsPlugins
type: `string[]`
type: `array | null`
JS plugins for this override.
@@ -258,6 +302,50 @@ Note: JS plugins are experimental and not subject to semver.
They are not supported in language server at present.
##### overrides[n].jsPlugins[n]
type: `object | string`
###### overrides[n].jsPlugins[n].name
type: `string`
Custom name/alias for the plugin.
Note: The following plugin names are reserved because they are implemented natively in Rust within oxlint and cannot be used for JS plugins:
- react (includes react-hooks)
- unicorn
- typescript
- oxc
- import (includes import-x)
- jsdoc
- jest
- vitest
- jsx-a11y
- nextjs
- react-perf
- promise
- node
- regex
- vue
- eslint
If you need to use the JavaScript version of any of these plugins, provide a custom alias to avoid conflicts.
###### overrides[n].jsPlugins[n].specifier
type: `string`
Path or package name of the plugin
#### overrides[n].plugins
type: `array | null`