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

đź’»

  • 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 https://github.com/evanw/esbuild/commit/f66b586923e2e4569155e758b57bae9d473c7d8f. 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: https://github.com/babel/babel/blob/b1f57e5fb58f7e890b3be58965e014f6fd4950e0/packages/babel-helper-member-expression-to-functions/src/index.ts#L465-L466

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();

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 15 (15 by maintainers)

Most upvoted comments

There are a few needed , which will live in the following directories:

Class fields:

  • packages/babel-plugin-proposal-class-properties/test/fixtures/private/tagged-template
  • packages/babel-plugin-proposal-class-properties/test/fixtures/private/tagged-template-static

Private method:

  • packages/babel-plugin-proposal-private-methods/test/fixtures/private-method/tagged-template
  • packages/babel-plugin-proposal-private-methods/test/fixtures/private-static-method/tagged-template

Private accessor:

  • packages/babel-plugin-proposal-private-methods/test/fixtures/accessors/tagged-template
  • packages/babel-plugin-proposal-private-methods/test/fixtures/accessors-static/tagged-template

You can look at the other fixtures in these directories to see an example of the input.js and output.js.

Yup, right