swc-project / swc

Rust-based platform for the Web

Home Page:https://swc.rs

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

typescript: literal constructor references of a decorated class reference un-decorated class's constructor, when used inside decorated class

benasher44 opened this issue · comments

Describe the bug

Literal references to the constructor inside the class's implementation reference the un-decorated class's constructor. This results in functions, like a copy function, unexpectedly returning instances of the class without decoration.

Input code

function markedClass<T extends Constructor<any>>(
  constructor: T,
  markerName: string
): T & Constructor<any> {
  const result = {
    [constructor.name]: class extends constructor {
      constructor(...args: any[]) {
        super(...args);
        Object.assign(this, { __markerName: markerName });
      }
    },
  };
  return result[constructor.name];
}

function MarkClass(markerName: string) {
  return <T extends Constructor<object>>(target: T): T => {
    return markedClass(target, markerName);
  };
}

@MarkClass("example")
export class Example {
  public copy() {
    return new Example();
  }
}

Config

{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true,
      "useDefineForClassFields": false
    },
    "target": "es2015",
    "minify": {
      "mangle": false,
      "compress": false
    },
    "loose": false
  },
  "isModule": true,
  "module": {
    "type": "commonjs"
  },
  "minify": false
}

Playground link (or link to the minimal reproduction)

https://play.swc.rs/?version=1.5.2&code=H4sIAAAAAAAAA3VRzWqDQBC%2BC77DkENZQXwAm0gh9Nj2klsIYbOZGFtdZXelhuC7d7Kurq30tsx%2B8%2F3NpZXCFLWEiqsvPG9LrvV6B9gZlGcN21pqo1pharXm8pZlLAwAhJ%2BmsIsfI7uu3nmFKdBfIfMwiOgTnhYccJ84QKFuSwObYQawn1EnktgOKYiHp8nRDDAu%2FTLEkiThKtcpkNb%2BEHkQgG4bnADRs%2F%2F4OH2iMAnpFLlk5lroGO5wPM5T%2BTf0frcfHr0tobdjhaZV0kVbBiIMLYXBZWz%2BjYht72xZorPvKP85TG3d020M5UJDrdvmN9mY3a3PTuyg8SzVkKkf7b14WyvseNWUuIrCALumVsbd5HWYDzJNeyoLQadobiz6oyzxewQzp2NlfgCGAagKfgIAAA%3D%3D&config=H4sIAAAAAAAAA21QQY6DMAy87yuQzz1sV%2Bql16Le%2BggrGBSUxMg2UlHVvzdQgtjt3uKZzHjGj6%2Bqgl4dnKtHfuZhQFGSbc6ITsnwnhGwaSB14geDQ2FNZ6rFoLRhDTkWNBbNlMlIC%2FF882CCSVuWuF8SqEM31UW46j4db2TYoOHfD6NSTa1PdGW5BFS9egqNlmi%2FA6B0ZHMh0p%2Fv42ktA9En3077VBFTF%2Bijn%2BM4COn%2F7oFZaccsOHi9cTMuXiU3xIKsp5%2FPO6fK9pFTr7Cpt2Rv0%2BcLSEYzDLcBAAA%3D

SWC Info output

Operating System:
    Platform: darwin
    Arch: arm64
    Machine Type: arm64
    Version: Darwin Kernel Version 23.4.0: Fri Mar 15 00:10:42 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T6000
    CPU: (10 cores)
        Models: Apple M1 Max

Binaries:
    Node: 20.11.1
    npm: 10.2.4
    Yarn: 4.0.1
    pnpm: N/A

Relevant Packages:
    @swc/core: 1.5.0
    @swc/helpers: N/A
    @swc/types: 0.1.6
    typescript: 5.4.5

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

Next.js info:
    output: N/A

Expected behavior

With the example setup, calling (new Example()).copy() should return a new instance of Example that is also decorated.

Actual behavior

Instead you can an un-decorated Example instance.

Version

1.5.0

Additional context

The workaround is to ensure that you do new this.constructor(), when attempting to create a new version of a class from inside that same class. Upon inspecting tsc emit, this is exactly how typescript handles it: transforming such new Example() (where Example is the constructor of the class owning the function where the constructor is being called) code to be new this.constructor() instead.

One interesting note: still debugging this one, but there is a similar flavor of bug where static functions on the class don't get transformed this way. For example:

@MarkClass("example")
class Example {
  static create() {
    new Example()
  }
}

tsc creates an intermediary var so the emit looks like

let Example_1 = class Example {
  static create() {
    new Example_1()
  }
}
// apply decorate code here

Somehow this doesn't exhibit the same bug in tsc, but the viable workaround for swc is similar. We've updated our code to instead use new this(), in the static function. I guess somehow creating that Example_1 var allows makings this work by tsc — maybe something specific to tslib's decorate function? not sure though

I suppose swc could apply a similar transformation and also transform new Example() to new this(). Apologies; I haven't fully debugged this part yet, but I saw this issue was assigned and wanted to call out this part too, in case the assignee has a better handle on tslib's decorating than myself :)

Ah okay so what tsc does is apply the decoration to Example and update Example_1 to point to the decorated class, to then calling Example.create() works as expected.