Skip to content

fix: Fix plugin scope determination logic#402

Merged
Sysix merged 8 commits intooxc-project:mainfrom
connorshea:claude/add-oxlint-migrate-snapshot-test-7b5QK
Mar 1, 2026
Merged

fix: Fix plugin scope determination logic#402
Sysix merged 8 commits intooxc-project:mainfrom
connorshea:claude/add-oxlint-migrate-snapshot-test-7b5QK

Conversation

@connorshea
Copy link
Member

@connorshea connorshea commented Feb 23, 2026

This change ensures that the plugin rules are migrated with the correct name (e18e/rule-name, not @e18e/rule-name). It also ensures that the plugin name that is used is correct.

It should migrate as "jsPlugins": ["@e18e/eslint-plugin"], but prior to this PR did not.

From what I understand, the reason it is e18e/rule-name despite the scoped plugin name is that it specifies the meta.name field here.

The Tanstack integration test is unchanged as it was already correct.

This also fixes using the usage of @eslint-react plugin. Fixes #377.

Generated with Claude Code, tested by me and confirmed as fixing the problem fully.

@connorshea connorshea changed the title test: Add e18e ESLint plugin integration test fix: Fix plugin scope determination logic, and add e18e ESLint plugin integration test Feb 23, 2026
Comment on lines +23 to +37
/**
* Resolves the npm package name for an ESLint plugin given its scope name.
*
* For scoped plugin names (starting with `@`), the mapping is unambiguous:
* - `@scope` -> `@scope/eslint-plugin`
* - `@scope/sub` -> `@scope/eslint-plugin-sub`
*
* For non-scoped names, the npm package could follow either convention:
* - `eslint-plugin-{name}` (e.g. `eslint-plugin-mocha`)
* - `@{name}/eslint-plugin` (e.g. `@e18e/eslint-plugin`)
*
* We try to resolve both candidates against the installed packages and
* use the one that is actually present, falling back to the standard
* `eslint-plugin-{name}` convention when neither can be resolved.
*/
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is entirely Claude's assumption, I'm pretty sure you could (though if you did, you're insane) have a non-scoped plugin like eslint-plugin-foo but set meta.name to @foo. But I have no idea if we care about supporting that edge-case for now.

const guessEslintPluginName = (pluginName: string): string => {
const tryResolvePackage = (packageName: string): boolean => {
try {
import.meta.resolve(packageName);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only concern is whether it can actually resolve this, e.g. if you use npx @oxlint/migrate but your project is using yarn or pnpm, will npx be able to resolve the packages?

@connorshea connorshea requested a review from Sysix February 23, 2026 02:24
@connorshea

This comment was marked as outdated.

@connorshea
Copy link
Member Author

Alright, I think this correctly handles all three cases:

  • @e18e/eslint-plugin but with e18e meta.name means that the rules should be e18e/rule-name.
  • @tanstack/eslint-plugin-query can be used with rule names like @tanstack/query/rule-name.
  • @eslint-react/eslint-plugin with sub-plugins is handled correctly, migrates such that we rename the rules to use the canonical scopes (react-dom/no-flush-sync for the rule, eslint-plugin-react-dom for the plugin), as Oxlint doesn't handle the aliasing right now without using the specifier system in the Oxlint config itself.

I guess we could consider setting this up to explicitly use the aliasing system for the last edge case. Then the rules can have names like @eslint-react/dom/rule-name? But I'd rather do that in a separate PR if we want to do it, as I have confirmed that this current PR works correctly as-is, and it fixes a user's active problem.

// with enabled rules that go through enableJsPluginRule.
const resolvedRule = resolveJsPluginRuleName(
rule,
eslintConfig.plugins
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a rule be activated from a plugin declaration from another ESLint config?
Something like:

export default [
  {"plugins": await import("xyz") },
  {"rules": { "xyz/rule-1": "error" } }
]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I'm not sure if you can do it async like that, but it can be activated from another config

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The async part was only for code demo :).
Then we should probably collect first all plugins across every ESLint config?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, after thinking on it more: I think the plugins value should be fully resolved at this point in our evaluation of the config. So I'm pretty sure this wouldn't be a problem?

Copy link
Member

@Sysix Sysix Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure, somebody needs to test it :')
The first call will have only {"plugins": ...} and the second call will only have the rule {"rules": {"xyz-rule1": ...}}.

resolveJsPluginRuleName does not access the global resolved plugin cache. Instead, it relies on eslintConfig.plugins

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a snapshot test and confirmed the behavior of ESLint is as you described, now I just need to fix it...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed :) e044a9d

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah you mentioned "across all plugins". I didn't realize this case would work as well, let me make sure this case is fixed 🙃

import e18e from '@e18e/eslint-plugin';

export default [
  {
    rules: {
      'e18e/prefer-array-at': 'error',
      'e18e/prefer-includes': 'warn'
    },
  },
  {
    plugins: { e18e }
  },
];

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^ FWIW I did test that just now (with @eslint-react/dom/ as well) and it worked correctly with no code changes, so we're good!

connorshea added a commit that referenced this pull request Feb 27, 2026
This is just taking some of the tests added in #402 and porting them to main so I can avoid merge conflicts later. I assume these tests are safe to merge since they are just tests and are simple enough.
@connorshea connorshea force-pushed the claude/add-oxlint-migrate-snapshot-test-7b5QK branch from 982ccdc to c9db8fc Compare February 27, 2026 02:00
connorshea added a commit that referenced this pull request Feb 27, 2026
This is just taking some of the tests added in #402 and porting them to main so I can avoid merge conflicts later. I assume these tests are safe to merge since they are just tests and are simple enough.
claude and others added 5 commits February 26, 2026 19:01
Instead of guessing the npm package name from the plugin scope
(e.g. "e18e" → "eslint-plugin-e18e"), try to resolve the actual
installed package. This correctly handles plugins like
@e18e/eslint-plugin that register with a non-scoped name ("e18e")
but whose npm package uses the @scope/eslint-plugin convention.

For non-scoped plugin names, we now try both candidate conventions:
  1. eslint-plugin-{name} (standard, e.g. eslint-plugin-mocha)
  2. @{name}/eslint-plugin (scoped, e.g. @e18e/eslint-plugin)

Results are cached to avoid repeated module resolution.

https://claude.ai/code/session_01GBWbJpvKSgVenUQNzc27dg
When an ESLint plugin provides meta.name, use it as the authoritative
source for the npm package name rather than guessing from the rule ID.
If meta.name contains "eslint-plugin" it is used directly; otherwise
it is fed through the existing heuristic resolution.

https://claude.ai/code/session_01GBWbJpvKSgVenUQNzc27dg
When a plugin is registered under an alias (e.g. `@eslint-react/dom`)
but its `meta.name` reveals a different canonical package name (e.g.
`eslint-plugin-react-dom`), rewrite rule prefixes so oxlint can match
them to the loaded plugin.

For example:
  @eslint-react/dom/no-find-dom-node -> react-dom/no-find-dom-node
  @eslint-react/jsx-key-before-spread -> react-x/jsx-key-before-spread

https://claude.ai/code/session_01GBWbJpvKSgVenUQNzc27dg
@connorshea connorshea force-pushed the claude/add-oxlint-migrate-snapshot-test-7b5QK branch from c9db8fc to 6c49337 Compare February 27, 2026 02:03
@connorshea connorshea changed the title fix: Fix plugin scope determination logic, and add e18e ESLint plugin integration test fix: Fix plugin scope determination logic Feb 27, 2026
@connorshea
Copy link
Member Author

I have merged the integration tests for the @tanstack and e18e plugins into main via a separate PR to make this one easier to understand and review :)

@connorshea connorshea requested a review from Sysix February 27, 2026 02:05
// with enabled rules that go through enableJsPluginRule.
const resolvedRule = resolveJsPluginRuleName(
rule,
eslintConfig.plugins
Copy link
Member

@Sysix Sysix Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not 100% sure, somebody needs to test it :')
The first call will have only {"plugins": ...} and the second call will only have the rule {"rules": {"xyz-rule1": ...}}.

resolveJsPluginRuleName does not access the global resolved plugin cache. Instead, it relies on eslintConfig.plugins

return rule;
}

const metaName = plugins?.[pluginName]?.meta?.name;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here

This adds a snapshot test to ensure that the, I confirmed in a test repo that this is valid and works in ESLint. We need to fix the logic of oxlint-migrate to handle it.
It does not result in the correct config right now.
@connorshea connorshea requested a review from Sysix March 1, 2026 18:35
@Sysix
Copy link
Member

Sysix commented Mar 1, 2026

thank you for looking into this and confirming the global plugins problem :)

@Sysix Sysix merged commit 69a3821 into oxc-project:main Mar 1, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect migration of js plugin @eslint-react/eslint-plugin

3 participants