|
| 1 | +use std::borrow::Cow; |
| 2 | + |
1 | 3 | use bitflags::bitflags; |
2 | 4 | use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::Schema}; |
3 | 5 | use serde::{Deserialize, Serialize, de::Deserializer, ser::Serializer}; |
4 | 6 |
|
| 7 | +/// Normalizes plugin names by stripping common ESLint plugin prefixes and suffixes. |
| 8 | +/// |
| 9 | +/// This handles the various naming conventions used in the ESLint ecosystem: |
| 10 | +/// - `eslint-plugin-foo` → `foo` |
| 11 | +/// - `@scope/eslint-plugin` → `@scope` |
| 12 | +/// - `@scope/eslint-plugin-foo` → `@scope/foo` |
| 13 | +/// |
| 14 | +/// # Examples |
| 15 | +/// |
| 16 | +/// ``` |
| 17 | +/// use oxc_linter::normalize_plugin_name; |
| 18 | +/// |
| 19 | +/// assert_eq!(normalize_plugin_name("eslint-plugin-react"), "react"); |
| 20 | +/// assert_eq!(normalize_plugin_name("@typescript-eslint/eslint-plugin"), "@typescript-eslint"); |
| 21 | +/// assert_eq!(normalize_plugin_name("@foo/eslint-plugin-bar"), "@foo/bar"); |
| 22 | +/// ``` |
| 23 | +pub fn normalize_plugin_name(plugin_name: &str) -> Cow<'_, str> { |
| 24 | + // Handle scoped packages (@scope/...) |
| 25 | + if let Some(scope_end) = plugin_name.find('/') { |
| 26 | + let scope = &plugin_name[..scope_end]; // e.g., "@foo" |
| 27 | + let rest = &plugin_name[scope_end + 1..]; // e.g., "eslint-plugin" or "eslint-plugin-bar" |
| 28 | + |
| 29 | + // Check if it's @scope/eslint-plugin or @scope/eslint-plugin-something |
| 30 | + if rest == "eslint-plugin" { |
| 31 | + // @foo/eslint-plugin -> @foo |
| 32 | + return Cow::Borrowed(scope); |
| 33 | + } else if let Some(suffix) = rest.strip_prefix("eslint-plugin-") { |
| 34 | + // @foo/eslint-plugin-bar -> @foo/bar |
| 35 | + return Cow::Owned(format!("{scope}/{suffix}")); |
| 36 | + } |
| 37 | + } |
| 38 | + |
| 39 | + // Handle non-scoped packages |
| 40 | + if let Some(suffix) = plugin_name.strip_prefix("eslint-plugin-") { |
| 41 | + // eslint-plugin-foo -> foo |
| 42 | + return Cow::Borrowed(suffix); |
| 43 | + } |
| 44 | + |
| 45 | + // No normalization needed |
| 46 | + Cow::Borrowed(plugin_name) |
| 47 | +} |
| 48 | + |
5 | 49 | bitflags! { |
6 | 50 | // NOTE: may be increased to a u32 if needed |
7 | 51 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
@@ -79,6 +123,10 @@ impl TryFrom<&str> for LintPlugins { |
79 | 123 | type Error = (); |
80 | 124 |
|
81 | 125 | fn try_from(value: &str) -> Result<Self, Self::Error> { |
| 126 | + // Normalize plugin name first to handle eslint-plugin-* naming |
| 127 | + let normalized = normalize_plugin_name(value); |
| 128 | + let value = normalized.as_ref(); |
| 129 | + |
82 | 130 | match value { |
83 | 131 | "react" | "react-hooks" | "react_hooks" => Ok(LintPlugins::REACT), |
84 | 132 | "unicorn" => Ok(LintPlugins::UNICORN), |
@@ -290,4 +338,44 @@ mod tests { |
290 | 338 | let error = result.unwrap_err().to_string(); |
291 | 339 | assert_eq!(error, "Unknown plugin: 'not-a-real-plugin'."); |
292 | 340 | } |
| 341 | + |
| 342 | + #[test] |
| 343 | + fn test_plugin_normalization() { |
| 344 | + // Test eslint-plugin- prefix normalization |
| 345 | + assert_eq!(LintPlugins::try_from("eslint-plugin-react"), Ok(LintPlugins::REACT)); |
| 346 | + assert_eq!(LintPlugins::try_from("eslint-plugin-unicorn"), Ok(LintPlugins::UNICORN)); |
| 347 | + assert_eq!(LintPlugins::try_from("eslint-plugin-import"), Ok(LintPlugins::IMPORT)); |
| 348 | + assert_eq!(LintPlugins::try_from("eslint-plugin-jest"), Ok(LintPlugins::JEST)); |
| 349 | + |
| 350 | + // Test @scope/eslint-plugin normalization |
| 351 | + assert_eq!( |
| 352 | + LintPlugins::try_from("@typescript-eslint/eslint-plugin"), |
| 353 | + Ok(LintPlugins::TYPESCRIPT) |
| 354 | + ); |
| 355 | + |
| 356 | + // Verify existing plugin names still work |
| 357 | + assert_eq!(LintPlugins::try_from("react"), Ok(LintPlugins::REACT)); |
| 358 | + assert_eq!(LintPlugins::try_from("unicorn"), Ok(LintPlugins::UNICORN)); |
| 359 | + assert_eq!(LintPlugins::try_from("@typescript-eslint"), Ok(LintPlugins::TYPESCRIPT)); |
| 360 | + } |
| 361 | + |
| 362 | + #[test] |
| 363 | + fn test_normalize_plugin_name() { |
| 364 | + use super::normalize_plugin_name; |
| 365 | + |
| 366 | + // Test eslint-plugin- prefix stripping |
| 367 | + assert_eq!(normalize_plugin_name("eslint-plugin-foo"), "foo"); |
| 368 | + assert_eq!(normalize_plugin_name("eslint-plugin-react"), "react"); |
| 369 | + |
| 370 | + // Test @scope/eslint-plugin suffix stripping |
| 371 | + assert_eq!(normalize_plugin_name("@foo/eslint-plugin"), "@foo"); |
| 372 | + assert_eq!(normalize_plugin_name("@bar/eslint-plugin"), "@bar"); |
| 373 | + |
| 374 | + // Test @scope/eslint-plugin-name normalization |
| 375 | + assert_eq!(normalize_plugin_name("@foo/eslint-plugin-bar"), "@foo/bar"); |
| 376 | + |
| 377 | + // Test no change for already normalized names |
| 378 | + assert_eq!(normalize_plugin_name("react"), "react"); |
| 379 | + assert_eq!(normalize_plugin_name("@typescript-eslint"), "@typescript-eslint"); |
| 380 | + } |
293 | 381 | } |
0 commit comments