Skip to content

compress: Class property closures capture wrong instance values #11368

@DorianListens

Description

@DorianListens

Describe the bug

When SWC's compress option is enabled, a class that has a class property initialized by calling a function that returns a closure will have that closure incorrectly capture values from the last instantiated instance instead of its own instance.

Input code

/**
 * SWC Compress Bug - Playground Reproduction
 * ==========================================
 *
 *
 * Steps:
 * 1. Run the output → logs "BUG: expected A, got B"
 * 2. Disable compress and run again → logs "OK: got A"
 *
 * Bug: When SWC's compress is enabled, a class property that calls
 * a function which returns a closure will have that closure capture
 * values from the LAST instance instead of its own.
 */

const wrap = (cb) => () => cb();

class Base {
  constructor(props) {
    this.props = props;
  }
}

class C extends Base {
  fn = wrap(this.props.cb);
}

// Test
const a = new C({cb: () => 'A'});
const b = new C({cb: () => 'B'});

const result = a.fn();

console.log(result === 'A' ? 'OK: got A' : 'BUG: expected A, got ' + result);

Config

{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": true,
    },
    "minify": {
      "compress": {
        "arguments": false,
        "arrows": true,
        "booleans": true,
        "booleans_as_integers": false,
        "collapse_vars": true,
        "comparisons": true,
        "computed_props": true,
        "conditionals": true,
        "dead_code": true,
        "directives": true,
        "drop_console": false,
        "drop_debugger": true,
        "evaluate": true,
        "expression": false,
        "hoist_funs": false,
        "hoist_props": true,
        "hoist_vars": false,
        "if_return": true,
        "join_vars": true,
        "keep_classnames": false,
        "keep_fargs": true,
        "keep_fnames": false,
        "keep_infinity": false,
        "loops": true,
        "negate_iife": true,
        "properties": true,
        "reduce_funcs": false,
        "reduce_vars": false,
        "side_effects": true,
        "switches": true,
        "typeofs": true,
        "unsafe": false,
        "unsafe_arrows": false,
        "unsafe_comps": false,
        "unsafe_Function": false,
        "unsafe_math": false,
        "unsafe_symbols": false,
        "unsafe_methods": false,
        "unsafe_proto": false,
        "unsafe_regexp": false,
        "unsafe_undefined": false,
        "unused": true,
        "const_to_let": true,
        "pristine_globals": true
      },
      "mangle": false,
    },
  },
  "module": {
    "type": "es6"
  },
  "minify": false,
  "env": {
    "targets": "> 0.5%, last 3 major versions, not dead, Firefox ESR"
  },
  "isModule": true,
}

Link to the code that reproduces this issue

https://play.swc.rs/?version=1.15.3&code=H4sIAAAAAAAAA42STW7bMBCF9zrFgzeS3VRGu5ShFpYLdNECLeIUWdPU6AdgSIE%2FUYPA2x6gR%2BxJOmTkJossKggSMXrv4xuOtptNhg2OtwcczN1kyTk0ocdbfFfiobcm6BbXNFnTBulHo6O6%2Fu%2BL1U83jp4mV8XVuxLXQcMPBBP8FDz%2B%2FPoNZXqHVfPjcwX6OZH01GJ%2Fhd54NKtoe1%2Fi0%2BjESRHkJangcJZZohejfoH59qVKzv1q2Z1bqnA7kI6d5u6ZMDqQjtD2CgJSCa5xsxNZ%2F8ARhYcUSrnIEOiCTmeAeRjlAEs%2BWO2Sz7hgCfOoFAZxT4t1KUsxsZIi5F6oQA6dNXfpBL7ujzcYtfNCS0oLEi1Mh9E7mFmXbNpmmTT8BbMVE2oU8rRG%2FQFFespTsd6xIkVvhCM8ZkAyWB6ZsUXsx61TGbzp6MpUYVJ677h%2Bzs4XxIHP35NuX8A6zdq4efHsLjnELrq2W9yQ80tEwUpNMw7FozxVS8R8n59Z%2FKQ4vapokmKR8GSC8qwTZaeLS90oKnm8xeVrXUcwPiL%2FN%2B4cFaNe%2B4dyvFmwjPsL8m7U8vUCAAA%3D&config=H4sIAAAAAAAAA32UyY7bMAxA7%2F0KwcDcgqJA0Tn00Fvn1kv7AYJiUY5SWzREKpNgkH8vZWdrQufihY%2BLxO3jkzHNltrmu%2FmQT%2FkZXSbIl3%2BR0CGx24uk4cMI1OY4crM6U6aKOBeYRcf51QwxxXC49dPiMGYgupGJ1OWuDJC4ioPrCVa3LOM7%2Fed%2Bkq8Re3DpCbGObEwMHWTNcYt970YCu3NZ8VJP6nIk1EJUWBi8HTOOKk8%2BcsQkMR%2BpB%2Bdtix4UFDO0HHegmUksMUsk11PuM2EP69J1U%2B3urGHn%2BuJYiQn7qSRyWsXrBiOxDSVpKZzhQg5meEruvWUMNgOXnB7tthjTQk3%2BAkgGekeU3ACa30kjSD8tWYenljEFaVk%2BKLxH9ZYJOkmqjTEoma2ZgcxRq2YGX1qomW2145zwQvooerAQgvSK4preI7cbLWgdXgwKkPq6oHXVDOxlChd4HYgn%2BE1uyXqDnTQGx5tlSodhjf2TAAPwBv0TBSkF4zLOsiX24zIvyYO0BnhVpdAEHpeADACj7YG13pDxEI%2B263F9XRMnheNltw4udffzPtHp0Qzoy4RPq7tWuK5poNfmqnTewxcfDaTdjZEMDEyd1PwwXz5%2Fe1kZGTI2X83gtpjNTjaoVI9WJiGbur5W5k02VcC9%2Bfnn9zVQpF%2Fn88zXPf4DBX4mo10GAAA%3D

SWC Info output

Operating System:
    Platform: darwin
    Arch: arm64
    Machine Type: arm64
    Version: Darwin Kernel Version 24.6.0: Wed Oct 15 21:12:06 PDT 2025; root:xnu-11417.140.69.703.14~1/RELEASE_ARM64_T6000
    CPU: (10 cores)
        Models: Apple M1 Max

Binaries:
    Node: 20.19.0
    npm: 10.8.2
    Yarn: N/A
    pnpm: N/A

Relevant Packages:
    @swc/core: 1.15.3
    @swc/helpers: 0.5.11
    @swc/types: 0.1.25
    typescript: 5.9.3
    next: 14.2.32

SWC Config:
    output: N/A
    .swcrc path: N/A

Next.js info:
    output: N/A

Expected behavior

When calling a.fn(), the closure should invoke the callback from instance a, returning 'A'.

Output should be: OK: got A

Actual behavior

With compress enabled, the closure incorrectly captures the callback from the last instantiated instance (b), returning 'B' instead.

Output is: BUG: expected A, got B

Version

1.15.3

Additional context

Conditions Required to Trigger

  1. Class extends a base class that sets a property (e.g., this.props) in its constructor
  2. Subclass has a class property initialized by calling an external function
  3. That function returns a closure over its parameter

Root Cause

When compress inlines the wrap function, it incorrectly hoists the cb parameter to module scope:

Input:

const wrap = (cb) => () => cb();

class C extends Base {
  fn = wrap(this.props.cb);
}

Compressed output:

let cb;  // <-- Hoisted to module scope!

class C extends Base {
  fn = (cb = this.props.cb, ()=>cb());
}

The inlined code uses the comma operator to assign this.props.cb to the shared cb variable, then returns a closure over it. Since cb is at module scope, all instances share the same variable, and each instantiation overwrites it.

Correct behavior would be to keep cb scoped per-invocation, something like:

class C extends Base {
  fn = ((cb) => () => cb())(this.props.cb);
}

Notes

  • The bug does not occur when the closure is created inline in the class property
  • The bug does not occur without compress enabled

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions