Skip to content

[Bug]: Tagged template with strict private field/method tag has incorrect receiver #13366

@jridgewell

Description

@jridgewell

💻

  • Would you like to work on a fix?

How are you using Babel?

Programmatic API (babel.transform, babel.parse)

Input code

While scanning the recent esbuild commits, I noticed evanw/esbuild@f66b586. Essentially, Tagged Template Literals are a special form of function invocation, and we need to preserve the this receiver when invoking:

REPL

class Foo {
  #tag() {
    return this;
  }

  constructor() {
    const receiver = this.#tag`tagged template`;
    console.assert(receiver === this);
  }
}
new Foo();

Configuration file name

babel.config.json

Configuration

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "shippedProposals": true,
        "targets": {
          "chrome": "75"
        }
      }
    ]
  ]
}

Current and expected behavior

Currently, the this.#tag is transformed into _classPrivateMethodGet(this, _tag, _tag2). The return value from _classPrivateMethodGet (which is _tag2, the transformed #tag method) is then invoked via Tagged Template:

const receiver = _classPrivateMethodGet(this, _tag, _tag2)`tagged template`;

In this case, the receiver is undefined, which fails the assertion.

console.assert(receiver === this); // Failure, receiver is undefined

Environment

Babel v7.14.1

Possible solution

When we're transforming a private access in the tag of a Tagged Template Literal, we need to bind the value to the correct receiver:

- const receiver = _classPrivateMethodGet(this, _tag, _tag2)`tagged template`;
+ const receiver = _classPrivateMethodGet(this, _tag, _tag2).bind(this)`tagged template`;

Where this is done is a bit confusing. Transforming is done via transformPrivateNamesUsage, which calls an abstract memberExpressionToFunctions transformer. memberExpressionToFunctions will call several "handler" functions, which are defined in privateNameHandlerSpec.

Currently, the this.#tag`template literal` is invoking the get handler. This fails to preserve the this receiver once transformed. Luckily, there's a similar handler called boundGet which will preserve the receiver.

We actually just need to fix the memberExpressionToFunctions implementation to detect when the get is happening inside a Tagged Template's tag. In this case, we need to call boundGet instead of get. Specifically this line:

// MEMBER -> _get(MEMBER)
member.replaceWith(this.get(member));

We'll also need

Additional context

This also needs to be applied when transforming a private field, not just a private method:

class Foo {
  #tag() {
    return this;
  }
  
  #tag2 = this.#tag;

  constructor() {
    const receiver = this.#tag`tagged template`;
    console.assert(receiver === this);

    const receiver2 = this.#tag2`tagged template`;
    console.assert(receiver2 === this);
  }
}
new Foo();

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions