DEV Community

Christo
Christo

Posted on

eslint-plugin-jsx-a11y says it doesn't support ESLint 10. It does.

eslint-plugin-jsx-a11y@6.10.2 declares this:

"peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" }
Enter fullscreen mode Exit fullscreen mode

ESLint's current release is 10.9.0, so npm refuses to put them in the same tree:

npm error Could not resolve dependency:
npm error peer eslint@"^3 || ... || ^9" from eslint-plugin-jsx-a11y@6.10.2
Enter fullscreen mode Exit fullscreen mode

I forced it with --legacy-peer-deps, turned on all 39 rules the plugin exports, and pointed it at a pile of ordinary JSX. Zero crashes. It works perfectly on ESLint 10. The range is just stale.

Then I did exactly the same thing to eslint-plugin-react@7.37.5, which declares an almost identical range, and 38 of its 101 rules threw.

Same declared constraint, opposite reality. Which is the whole problem with reading manifests: a peer range tells you when the author last checked, not whether the thing runs.

Most of react's 38 are one bug

I expected 38 separate messes. It's mostly one:

Error while loading rule 'react/display-name':
contextOrFilename.getFilename is not a function
Enter fullscreen mode Exit fullscreen mode

32 of the 38 are that. Trace it and you land in lib/util/version.js:

function resolveBasedir(contextOrFilename) {
  if (contextOrFilename) {
    const filename = typeof contextOrFilename === 'string'
      ? contextOrFilename
      : contextOrFilename.getFilename();
Enter fullscreen mode Exit fullscreen mode

context.getFilename() was removed in ESLint 10. Any rule that asks which React version you're on ends up here, which is why the casualty list looks so arbitrary: prop-types, no-multi-comp, sort-comp, hook-use-state. Nothing to do with what those rules check, everything to do with them wanting a version number.

The useful consequence is that it depends on your config. With settings: { react: { version: 'detect' } } I get 38 crashing rules. With no settings.react.version at all, same fixtures, same everything else, I get 6. If you and a colleague are comparing notes on this and your numbers disagree, that's probably why.

The 6 that break regardless are jsx-curly-spacing, jsx-equals-spacing, jsx-tag-spacing and jsx-one-expression-per-line on isSpaceBetweenTokens, forward-ref-uses-ref on getSourceCode, and jsx-filename-extension on getFilename.

One crashing rule hides all the others

First version of this told me react had one broken rule. It has 38.

When a rule throws, ESLint aborts the entire run at that point. You get the first casualty and nothing else, because there is no run left to report on. So "enable everything and see what happens" gives you a number that is always 1, no matter how bad things are.

The fix is dumb and works: lint with every rule on, and if that blows up, go back and lint again with one rule at a time. It is 101 passes for one plugin instead of 1, which is fine, because Linter.verify on a handful of files is milliseconds.

The number that was wrong

The first run said @typescript-eslint/eslint-plugin was blocked, with 64 crashing rules. Worst result in the whole set, on the most-installed plugin in the ecosystem.

It was wrong. Those 64 were rules like await-thenable refusing to start:

You have used a rule which requires type information,
but don't have parserOptions set to generate type information for this file.
Enter fullscreen mode Exit fullscreen mode

That is a rule telling you your tsconfig is not wired up. It is not an ESLint 10 incompatibility, and the tell was that it reproduced identically on ESLint 9.

That turned into a rule I now apply everywhere: in a two-version compatibility check, a failure that reproduces on both versions is your harness, not the subject. It caught three separate bugs in mine. typescript@latest is now TS 7.0 and typescript-eslint hard-refuses it, which broke four plugins in a way that looked nothing like a version problem. --legacy-peer-deps silently skips peers, so @angular-eslint was missing @typescript-eslint/utils and looked broken when it was fine.

It belongs in the output too, not just in debugging. "Blocked" should mean breaks on 10 and not on 9. Anything else is blaming the upgrade for something that was already there.

The failure doesn't have to be yours

eslint-plugin-vitest and eslint-plugin-deprecation don't crash on ESLint 10. They don't load at all:

Class extends value undefined is not a constructor or null
Enter fullscreen mode Exit fullscreen mode

Neither plugin's own code is involved. @typescript-eslint/utils does class LegacyESLint extends eslint.LegacyESLint, and ESLint 10 removed LegacyESLint when it removed eslintrc. Two plugins die through a dependency, and nothing in either manifest hints at it.

Where this ended up

54 plugins, installed into clean temp directories against 9.39.5 and 10.9.0, every rule enabled, nightly: booyaka101.github.io/eslint10-matrix

For a specific repo, npx eslint10-matrix check reads your flat config and sorts your plugins into blocked, safe to force (declares an old range, verified clean, here's the npm overrides block), and already fine.

Worth being honest about the limits. It tests two ESLint versions, not all nine 10.x minors. It tests each plugin at its latest version, so if you're pinned to something older the row won't describe you. Flat config only. And it's a fixture corpus, so a rule that only breaks on syntax my fixtures never use will read as clean. That last one is the weakest part and PRs to the fixtures would genuinely help.

The count on eslint-plugin-react will hopefully be obsolete soon. PR #3979 has been grinding toward a fix for months and looks close.

Top comments (0)