Reaction

ESLint 10.14 fixed two broken autofixes. One of them passed lint for a release

ESLint 10.14.0 fixes a yoda autofix that changed what code does. On 10.13.0 the fixed file passed lint and threw at run time. Treat --fix as a code change.

Published Updated 4 min read

ESLint 10.14.0, released on 30 October, carries two autofix bug fixes worth reading closely. The yoda fix could rewrite a working line into one that parses differently, and the object-shorthand fix could produce code that does not parse at all. Our view: upgrade, and then stop treating eslint --fix as formatting. A fix is a code change written by a program. The linter checks it the only way it can, against your lint config, and a rewrite that is valid JavaScript with different behaviour passes that check. The thing that would catch it is the same thing that catches any other code change: running the code.

What the two bugs were

The yoda rule with "never" flips 0 === x.indexOf(y) into x.indexOf(y) === 0. The bug report, filed on 19 October, was titled "autofix breaks code when the flipped comparison starts a statement". In code without semicolons, a line that now starts with [ joins the line above it. The fix adds a leading ; when the previous line would otherwise continue.

The object-shorthand rule with "never" turns a method into name: function () {}. The PR explains why that fails for a method that calls super: the result "fails to parse because super is no longer inside a method." 10.14.0 now skips that conversion.

The run

We installed eslint@10.13.0 and eslint@10.14.0 side by side on Node 22.23.4 and wrote two small modules in a no-semicolon style. The config turns on only the two rules:

export default [{ files: ["**/*.mjs"], rules: {
  yoda: ["error", "never"], "object-shorthand": ["error", "never"] } }];

The first module tracks a step, then checks whether a name is one of the first two steps:

  track(name)
  0 === [steps[0], steps[1]].indexOf(name) && (first = true)

Before any fix, isFirstStep(["cart","pay"], "cart") returned true. Then we ran eslint --fix with each version.

ESLint 10.13.0ESLint 10.14.0
yoda rewrite[steps[0], steps[1]].indexOf(name) === 0 && ...;[steps[0], steps[1]].indexOf(name) === 0 && ...
Lint result--fix reported nothing for this file; a re-lint with the same config exited 0--fix reported nothing
isFirstStep(["cart","pay"], "cart")TypeError: Cannot read properties of undefined (reading 'indexOf')true
object-shorthand rewritelabel: function() { return super.label() ... }unchanged method
Reported by --fixParsing error: 'super' keyword outside a methodnothing
Importing the moduleSyntaxError: 'super' keyword unexpected hereorder receipt

Without the semicolon, track(name) and the next line become one expression: index into whatever track returned with steps[1], then call .indexOf on the result. That is valid JavaScript. On 10.13.0 the fixed file linted clean under the config that produced it, and the function threw on every call.

The super bug was the loud one. ESLint printed a parsing error and exited 1, and it had already written the broken method to disk. The CLI reference says as much about --fix: "The fixes are made to the actual files themselves and only the remaining unfixed issues are output."

The cheap guard was already in the box

We added one rule to the 10.13.0 run: no-unexpected-multiline. It caught the rewritten line straight away, "Unexpected newline between object and [ of property access", and exit 1. The rule is in ESLint's recommended set (docs). A config that extends recommended would have flagged the bad fix in the same run. A hand-picked rule list, like ours, would not.

That is the general shape of the problem. A fixer is only checked by the rules you have turned on. Turn on a rule that rewrites code, leave off the rule that guards its blind spot, and the linter cannot tell a style change from a behaviour change.

What to do

  • Upgrade to 10.14.0. Both fixes are real, and both come with regression tests in the PRs.
  • Run the tests after --fix, not before. In CI and in pre-commit hooks, order matters. A hook that fixes and commits without running anything ships the rewrite unexamined.
  • Review fixes as a diff. --fix-dry-run shows what would change without writing it. For a large or first-time fix across a codebase, read that diff like a pull request.
  • Keep the guard rules on. If you turn on fixable style rules one by one, also turn on no-unexpected-multiline and the other parse-hazard rules in recommended.
  • Do not let an agent's "lint is clean" stand in for "it still works". An agent that runs eslint --fix to get a green check has made a code change and checked it with the linter alone.

None of this is a knock on ESLint. The yoda bug went from report to release in 11 days, and both fixes ship with regression tests. The lesson is about what a green lint run proves after an autofix: the code satisfies your rules. Whether it still does what it did is a different question, and only running it answers that.

Sources

  1. 01ESLint: v10.14.0 release notesgithub.com
  2. 02ESLint issue #21421: yoda autofix breaks code when the flipped comparison starts a statementgithub.com
  3. 03ESLint PR #21424: fix yoda invalid autofix at statement startgithub.com
  4. 04ESLint PR #21323: avoid invalid object-shorthand fixes for super methodsgithub.com
  5. 05ESLint docs: no-unexpected-multilineeslint.org
  6. 06ESLint docs: Command Line Interface referenceeslint.org
Reticleverdict

// The agent clicked "Pay". Did the right things actually happen?

{verified: "no"failureReason: "POST /api/order returned 500, expected 200"source: "src/checkout/PayButton.tsx:42"}

Example from the README

Your agent says it works. Reticle checks the network, state and console inside your running app, and names the line to fix.

85 of 88 seeded bugs caught in the public benchmark