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
17 changes: 14 additions & 3 deletions crates/oxc_linter/src/config/config_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,11 +553,22 @@ impl ConfigStoreBuilder {

match result {
PluginLoadResult::Success { name, offset, rule_names } => {
if LintPlugins::try_from(name.as_str()).is_err() {
external_plugin_store.register_plugin(plugin_path, name, offset, rule_names);
// Normalize plugin name (e.g., "eslint-plugin-foo" -> "foo", "@foo/eslint-plugin" -> "@foo")
use crate::config::plugins::normalize_plugin_name;
let normalized_name = normalize_plugin_name(&name).into_owned();

if LintPlugins::try_from(normalized_name.as_str()).is_err() {
external_plugin_store.register_plugin(
plugin_path,
normalized_name,
offset,
rule_names,
);
Ok(())
} else {
Err(ConfigBuilderError::ReservedExternalPluginName { plugin_name: name })
Err(ConfigBuilderError::ReservedExternalPluginName {
plugin_name: normalized_name,
})
}
}
PluginLoadResult::Failure(e) => Err(ConfigBuilderError::PluginLoadFailed {
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_linter/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ mod globals;
mod ignore_matcher;
mod overrides;
mod oxlintrc;
mod plugins;
pub mod plugins;
mod rules;
mod settings;
pub use config_builder::{ConfigBuilderError, ConfigStoreBuilder};
Expand Down
88 changes: 88 additions & 0 deletions crates/oxc_linter/src/config/plugins.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,51 @@
use std::borrow::Cow;

use bitflags::bitflags;
use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::Schema};
use serde::{Deserialize, Serialize, de::Deserializer, ser::Serializer};

/// Normalizes plugin names by stripping common ESLint plugin prefixes and suffixes.
///
/// This handles the various naming conventions used in the ESLint ecosystem:
/// - `eslint-plugin-foo` → `foo`
/// - `@scope/eslint-plugin` → `@scope`
/// - `@scope/eslint-plugin-foo` → `@scope/foo`
///
/// # Examples
///
/// ```
/// use oxc_linter::normalize_plugin_name;
///
/// assert_eq!(normalize_plugin_name("eslint-plugin-react"), "react");
/// assert_eq!(normalize_plugin_name("@typescript-eslint/eslint-plugin"), "@typescript-eslint");
/// assert_eq!(normalize_plugin_name("@foo/eslint-plugin-bar"), "@foo/bar");
/// ```
pub fn normalize_plugin_name(plugin_name: &str) -> Cow<'_, str> {
// Handle scoped packages (@scope/...)
if let Some(scope_end) = plugin_name.find('/') {
let scope = &plugin_name[..scope_end]; // e.g., "@foo"
let rest = &plugin_name[scope_end + 1..]; // e.g., "eslint-plugin" or "eslint-plugin-bar"

// Check if it's @scope/eslint-plugin or @scope/eslint-plugin-something
if rest == "eslint-plugin" {
// @foo/eslint-plugin -> @foo
return Cow::Borrowed(scope);
} else if let Some(suffix) = rest.strip_prefix("eslint-plugin-") {
// @foo/eslint-plugin-bar -> @foo/bar
return Cow::Owned(format!("{scope}/{suffix}"));
}
}

// Handle non-scoped packages
if let Some(suffix) = plugin_name.strip_prefix("eslint-plugin-") {
// eslint-plugin-foo -> foo
return Cow::Borrowed(suffix);
}

// No normalization needed
Cow::Borrowed(plugin_name)
}

bitflags! {
// NOTE: may be increased to a u32 if needed
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -79,6 +123,10 @@ impl TryFrom<&str> for LintPlugins {
type Error = ();

fn try_from(value: &str) -> Result<Self, Self::Error> {
// Normalize plugin name first to handle eslint-plugin-* naming
let normalized = normalize_plugin_name(value);
let value = normalized.as_ref();

match value {
"react" | "react-hooks" | "react_hooks" => Ok(LintPlugins::REACT),
"unicorn" => Ok(LintPlugins::UNICORN),
Expand Down Expand Up @@ -290,4 +338,44 @@ mod tests {
let error = result.unwrap_err().to_string();
assert_eq!(error, "Unknown plugin: 'not-a-real-plugin'.");
}

#[test]
fn test_plugin_normalization() {
// Test eslint-plugin- prefix normalization
assert_eq!(LintPlugins::try_from("eslint-plugin-react"), Ok(LintPlugins::REACT));
assert_eq!(LintPlugins::try_from("eslint-plugin-unicorn"), Ok(LintPlugins::UNICORN));
assert_eq!(LintPlugins::try_from("eslint-plugin-import"), Ok(LintPlugins::IMPORT));
assert_eq!(LintPlugins::try_from("eslint-plugin-jest"), Ok(LintPlugins::JEST));

// Test @scope/eslint-plugin normalization
assert_eq!(
LintPlugins::try_from("@typescript-eslint/eslint-plugin"),
Ok(LintPlugins::TYPESCRIPT)
);

// Verify existing plugin names still work
assert_eq!(LintPlugins::try_from("react"), Ok(LintPlugins::REACT));
assert_eq!(LintPlugins::try_from("unicorn"), Ok(LintPlugins::UNICORN));
assert_eq!(LintPlugins::try_from("@typescript-eslint"), Ok(LintPlugins::TYPESCRIPT));
}

#[test]
fn test_normalize_plugin_name() {
use super::normalize_plugin_name;

// Test eslint-plugin- prefix stripping
assert_eq!(normalize_plugin_name("eslint-plugin-foo"), "foo");
assert_eq!(normalize_plugin_name("eslint-plugin-react"), "react");

// Test @scope/eslint-plugin suffix stripping
assert_eq!(normalize_plugin_name("@foo/eslint-plugin"), "@foo");
assert_eq!(normalize_plugin_name("@bar/eslint-plugin"), "@bar");

// Test @scope/eslint-plugin-name normalization
assert_eq!(normalize_plugin_name("@foo/eslint-plugin-bar"), "@foo/bar");

// Test no change for already normalized names
assert_eq!(normalize_plugin_name("react"), "react");
assert_eq!(normalize_plugin_name("@typescript-eslint"), "@typescript-eslint");
}
}
50 changes: 50 additions & 0 deletions crates/oxc_linter/src/config/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ fn parse_rule_key(name: &str) -> (String, String) {
}

pub(super) fn unalias_plugin_name(plugin_name: &str, rule_name: &str) -> (String, String) {
// First normalize the plugin name by stripping eslint-plugin- prefix/suffix
let normalized = super::plugins::normalize_plugin_name(plugin_name);
let plugin_name = normalized.as_ref();

let (oxlint_plugin_name, rule_name) = match plugin_name {
"@typescript-eslint" => ("typescript", rule_name),
// import-x has the same rules but better performance
Expand Down Expand Up @@ -467,4 +471,50 @@ mod test {
assert_eq!(severity, &AllowWarnDeny::Deny, "{config:?}");
}
}

#[test]
fn test_normalize_plugin_name_in_rules() {
use super::super::plugins::normalize_plugin_name;

// Test eslint-plugin- prefix stripping
assert_eq!(normalize_plugin_name("eslint-plugin-foo"), "foo");
assert_eq!(normalize_plugin_name("eslint-plugin-react"), "react");
assert_eq!(normalize_plugin_name("eslint-plugin-import"), "import");

// Test @scope/eslint-plugin suffix stripping
assert_eq!(normalize_plugin_name("@foo/eslint-plugin"), "@foo");
assert_eq!(normalize_plugin_name("@bar/eslint-plugin"), "@bar");

// Test @scope/eslint-plugin-name normalization
assert_eq!(normalize_plugin_name("@foo/eslint-plugin-bar"), "@foo/bar");
assert_eq!(normalize_plugin_name("@typescript-eslint/eslint-plugin"), "@typescript-eslint");

// Test no change for already normalized names
assert_eq!(normalize_plugin_name("react"), "react");
assert_eq!(normalize_plugin_name("unicorn"), "unicorn");
assert_eq!(normalize_plugin_name("@typescript-eslint"), "@typescript-eslint");
assert_eq!(normalize_plugin_name("jsx-a11y"), "jsx-a11y");
}

#[test]
fn test_parse_rules_with_eslint_plugin_prefix() {
// Test that eslint-plugin- prefix is properly normalized in various formats
let rules = OxlintRules::deserialize(&json!({
"eslint-plugin-react/jsx-uses-vars": "error",
"eslint-plugin-unicorn/no-null": "warn",
}))
.unwrap();

let mut rules_iter = rules.rules.iter();

let r1 = rules_iter.next().unwrap();
assert_eq!(r1.rule_name, "jsx-uses-vars");
assert_eq!(r1.plugin_name, "react");
assert!(r1.severity.is_warn_deny());

let r2 = rules_iter.next().unwrap();
assert_eq!(r2.rule_name, "no-null");
assert_eq!(r2.plugin_name, "unicorn");
assert!(r2.severity.is_warn_deny());
}
}
1 change: 1 addition & 0 deletions crates/oxc_linter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ mod tester;

mod lint_runner;

pub use crate::config::plugins::normalize_plugin_name;
pub use crate::disable_directives::{
DisableDirectives, DisableRuleComment, RuleCommentRule, RuleCommentType,
create_unused_directives_diagnostics,
Expand Down
Loading