react / react/metro

constant-folding-plugin emits 0/0 (NaN) for arithmetic on destructured bindings

Open Beginner friendly
#1,927 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
5.6k
Forks
696
Avg merge
8m
Merged PRs (30d)
7

Description

Description

metro-transform-plugins' constant-folding-plugin replaces arithmetic on destructured bindings with a literal 0 / 0 (NaN) in production bundles.

const SHAPE = { lo: 80, hi: 85, scale: 2.8 };

export function f() {
  const { lo, hi, scale } = SHAPE;
  const span = (hi - lo) * scale;   // 14
  ...
}

After the pass:

const span = 0 / 0;

With + instead, a value becomes a string:

const { e } = O; return e + 1;   // → return "[object Object]1";

Dev builds are unaffected (the optimisation pass does not run), so this only appears in release, on both platforms, silently.

Root cause

Not Metro's own logic. The plugin asks Babel:

const evaluated = path.evaluate();
return { confident: evaluated.confident, value: evaluated.value };

and Babel returns { confident: true, value: NaN }, because path.evaluate() resolves a destructured binding to its declarator's whole init object. Filed upstream: https://github.com/babel/babel/issues/18238

The plugin then does path.replaceWith(t.valueToNode(result.value)), and valueToNode(NaN) emits 0 / 0.

Why it is easy to miss

Babel only hands back the bad value when each destructured binding is referenced exactly once (a binding.references > 1 guard masks it otherwise), and only when the object is a local literal — destructuring from params, props or imports deopts earlier. So the trigger is narrow, but when it fires the result is a silent wrong value in a shipped bundle. This has caused a rendering failure in a released React Native app: a coordinate became NaN and the renderer drew nothing, with no error. Nothing fails loudly, and dev builds look fine, so it is very hard to trace back to the bundler.

Suggested defensive fix

The plugin already refuses to fold subtrees containing calls or assignments, so it is defensive by design — it just has no protection against the evaluator being confidently wrong. Declining to substitute a non-finite result would have prevented this entirely, and legitimate folds to NaN/Infinity are vanishingly rare and harmless to skip:

const Expression = {
  exit(path) {
    const result = evaluate(path);
    if (result.confident && Number.isFinite(result.value)) {   // or: typeof !== "number" || Number.isFinite
      path.replaceWith(t.valueToNode(result.value));
      path.skip();
    }
  },
};

Environment

  • metro 0.84.4, metro-transform-plugins 0.84.4
  • @babel/core / @babel/traverse 7.29.7
  • React Native 0.86 / Expo SDK 57, Node v26.8.2, macOS

Reproduction

const babel = require("@babel/core");
const fold = require("metro-transform-plugins/src/constant-folding-plugin.js");

console.log(babel.transformSync(
  `const O = { e: 80 };
   function f() { const { e } = O; return e + 1; }`,
  { babelrc: false, configFile: false, plugins: [fold] },
).code);
// → function f() { const { e } = O; return "[object Object]1"; }

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with metro-transform-plugins/src/constant-folding-plugin.js and run the Babel reproduction from the issue to observe the incorrect folded output. Check the evaluator result before substitution, then verify that arithmetic on destructured bindings is no longer emitted as 0 / 0 or a stringified object while valid finite folds still work.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
build-system
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.