diff --git a/apps/oxlint/test/fixtures/plugin_name/.oxlintrc.json b/apps/oxlint/test/fixtures/plugin_name/.oxlintrc.json index 388a463631435..61505606d7359 100644 --- a/apps/oxlint/test/fixtures/plugin_name/.oxlintrc.json +++ b/apps/oxlint/test/fixtures/plugin_name/.oxlintrc.json @@ -11,6 +11,8 @@ // `/eslint-plugin` suffix stripped "@scope/eslint-plugin", "@scope2/eslint-plugin", + // `eslint-plugin-` prefix stripped from scoped package name (split at last `/`) + "@scope3/eslint-plugin-subplugin", // Plugin name is `jsdoc`, but that's overridden by alias { "name": "js-jsdoc", "specifier": "./plugins/jsdoc.ts" }, { "name": "js-jsdoc2", "specifier": "jsdoc" }, @@ -32,6 +34,8 @@ "@scope/rule": "error", // `@scope2/eslint-plugin` "@scope2/rule": "error", + // `@scope3/eslint-plugin-subplugin` (split at last `/`) + "@scope3/subplugin/rule": "error", // Aliased "js-jsdoc/rule": "error", "js-jsdoc2/rule": "error", diff --git a/apps/oxlint/test/fixtures/plugin_name/node_modules/@scope3/eslint-plugin-subplugin/index.js b/apps/oxlint/test/fixtures/plugin_name/node_modules/@scope3/eslint-plugin-subplugin/index.js new file mode 100644 index 0000000000000..fafa7f400b3cf --- /dev/null +++ b/apps/oxlint/test/fixtures/plugin_name/node_modules/@scope3/eslint-plugin-subplugin/index.js @@ -0,0 +1,17 @@ +export default { + // No name defined + rules: { + rule: { + create(context) { + return { + FunctionDeclaration(node) { + context.report({ + message: `id: ${context.id}`, + node, + }); + }, + }; + }, + }, + }, +}; diff --git a/apps/oxlint/test/fixtures/plugin_name/node_modules/@scope3/eslint-plugin-subplugin/package.json b/apps/oxlint/test/fixtures/plugin_name/node_modules/@scope3/eslint-plugin-subplugin/package.json new file mode 100644 index 0000000000000..6f9c34add3bde --- /dev/null +++ b/apps/oxlint/test/fixtures/plugin_name/node_modules/@scope3/eslint-plugin-subplugin/package.json @@ -0,0 +1,5 @@ +{ + "name": "@scope3/eslint-plugin-subplugin", + "type": "module", + "main": "index.js" +} diff --git a/apps/oxlint/test/fixtures/plugin_name/output.snap.md b/apps/oxlint/test/fixtures/plugin_name/output.snap.md index 3d37037e529e4..a4e213b9f9360 100644 --- a/apps/oxlint/test/fixtures/plugin_name/output.snap.md +++ b/apps/oxlint/test/fixtures/plugin_name/output.snap.md @@ -13,6 +13,13 @@ x @scope2(rule): id: @scope2/rule ,-[files/index.js:4:1] 3 | */ + 4 | function f(foo, bar) {} + : ^^^^^^^^^^^^^^^^^^^^^^^ + `---- + + x @scope3/subplugin(rule): id: @scope3/subplugin/rule + ,-[files/index.js:4:1] + 3 | */ 4 | function f(foo, bar) {} : ^^^^^^^^^^^^^^^^^^^^^^^ `---- @@ -81,7 +88,7 @@ `---- help: Add `@param` tag with name. -Found 0 warnings and 11 errors. +Found 0 warnings and 12 errors. Finished in Xms on 1 file using X threads. ``` diff --git a/crates/oxc_linter/src/config/rules.rs b/crates/oxc_linter/src/config/rules.rs index 3190ff87a3cc4..75248b84d6763 100644 --- a/crates/oxc_linter/src/config/rules.rs +++ b/crates/oxc_linter/src/config/rules.rs @@ -249,7 +249,12 @@ impl<'de> Deserialize<'de> for OxlintRules { } fn parse_rule_key(name: &str) -> (String, String) { - let Some((plugin_name, rule_name)) = name.split_once('/') else { + // For scoped packages (starting with `@`), split at the last `/` to handle + // packages like `@eslint-react/naming-convention` with rule `rule-name`. + // For non-scoped packages, split at the first `/`. + let parts = if name.starts_with('@') { name.rsplit_once('/') } else { name.split_once('/') }; + + let Some((plugin_name, rule_name)) = parts else { return ( RULES .iter() @@ -275,8 +280,8 @@ pub(super) fn unalias_plugin_name(plugin_name: &str, rule_name: &str) -> (String "import-x" => ("import", rule_name), "jsx-a11y" => ("jsx_a11y", rule_name), "react-perf" => ("react_perf", rule_name), - // e.g. "@next/next/google-font-display" - "@next" => ("nextjs", rule_name.trim_start_matches("next/")), + // e.g. "@next/google-font-display", "@next/next/google-font-display" + "@next" | "@next/next" => ("nextjs", rule_name), // For backwards compatibility, react hook rules reside in the react plugin. "react-hooks" => ("react", rule_name), // For backwards compatibility, deepscan rules reside in the oxc plugin. @@ -374,6 +379,9 @@ mod test { "foo/no-unused-vars": [1], "dummy": ["error", "arg1", "args2"], "@next/next/noop": 2, + "@next/something": "error", + "@tanstack/query/exhaustive-deps": "warn", + "@scope/whatever": "warn", })) .unwrap(); let mut rules = rules.rules.iter(); @@ -396,11 +404,32 @@ mod test { assert!(r3.severity.is_warn_deny()); assert_eq!(r3.config.as_slice(), &[serde_json::json!("arg1"), serde_json::json!("args2")]); + // `@next/next` is aliased to `nextjs` let r4 = rules.next().unwrap(); assert_eq!(r4.rule_name, "noop"); assert_eq!(r4.plugin_name, "nextjs"); assert!(r4.severity.is_warn_deny()); assert!(r4.config.is_empty()); + + // `@next` is also aliased to `nextjs` + let r5 = rules.next().unwrap(); + assert_eq!(r5.rule_name, "something"); + assert_eq!(r5.plugin_name, "nextjs"); + assert!(r5.severity.is_warn_deny()); + assert!(r5.config.is_empty()); + + // Scoped package with nested name - split at last `/` + let r6 = rules.next().unwrap(); + assert_eq!(r6.rule_name, "exhaustive-deps"); + assert_eq!(r6.plugin_name, "@tanstack/query"); + assert!(r6.severity.is_warn_deny()); + assert!(r6.config.is_empty()); + + let r7 = rules.next().unwrap(); + assert_eq!(r7.rule_name, "whatever"); + assert_eq!(r7.plugin_name, "@scope"); + assert!(r7.severity.is_warn_deny()); + assert!(r7.config.is_empty()); } #[test]