Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new decorators transform #14004

Merged

Conversation

pzuraq
Copy link
Contributor

@pzuraq pzuraq commented Nov 28, 2021

Q                       A
Fixed Issues? Fixes #12654
Patch: Bug Fix? No
Major: Breaking Change? No
Minor: New Feature? Yes
Tests Added + Pass? Yes
Documentation PR Link No
Any Dependency Changes? No
License MIT

Adds the 2020-09 decorators transform, along with associated tests. See https://github.com/tc39/proposal-decorators for details on the proposal itself.

Resolves #12654.


Update by @JLHwung

Try this feature on REPL

@pzuraq pzuraq changed the base branch from main to feat-7.16.0/decorators November 28, 2021 01:05
@babel-bot
Copy link
Collaborator

babel-bot commented Nov 28, 2021

Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/50906/

@nicolo-ribaudo
Copy link
Member

@pzuraq I had to rebase #13827; could you rebase this one on top of that (removing the now duplicate commits

@nicolo-ribaudo nicolo-ribaudo added PR: New Feature 🚀 A type of pull request used for our changelog categories Spec: Decorators labels Dec 13, 2021
@pzuraq pzuraq force-pushed the add-new-decorators-transform branch 2 times, most recently from a438971 to 8f2bed6 Compare December 25, 2021 14:44
@pzuraq pzuraq marked this pull request as ready for review December 25, 2021 14:46
@pzuraq pzuraq force-pushed the add-new-decorators-transform branch 2 times, most recently from a0f9d4d to f922636 Compare December 25, 2021 15:26
@nicolo-ribaudo nicolo-ribaudo added this to the v7.17.0 milestone Dec 27, 2021
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only reviewed the explainer and part of the helper so far.

@@ -0,0 +1,741 @@
/* @minVersion 7.16.6 */

var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using non-hoistable top-level code can cause problems with circular dependencies. For example (disclaimer: I didn't read the transform yet so this output does not match it, but it shows the problem):

// a.js
import "./b.js";

var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;

function _applyDecorators(Class, decs) {
  getOwnPropertyDescriptor(Class, "foo");
}

export function getClass() {
  class _Class {};
  let Class = _applyDecorators(_Class, []);
  return Class;
}
// b.js
import { getClass } "./a.js";

getClass();

When running node a.js, getOwnPropertyDescriptor(Class, "foo"); will be run before getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor so it will throw that getOwnPropertyDescriptor is not a function.


Since Babel assumes that built-in functions behave as in the spec and are not modified, we don't need to extract all these Object.* and Array.* methods. For the remaining variables, we can just inline them (with comments like /* CONSTRUCTOR */ 0 to explain what the inlined value is).

Comment on lines 59 to 72
let ret = applyDecs(
this,
elementDecs,
[dec]
);

initA = ret[0];
callB = ret[1];
initC = ret[2];
getC = ret[3];
setC = ret[4];
initInstance = ret[5];
Class = ret[6];
initClass = ret[7];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably get a slightly smaller output by doing

    ({
      0: initA,
      1: callB,
      2: initC,
      3: getC,
      4: setC,
      5: initInstance,
      6: Class,
      7: initClass,
    } = applyDecs(
      this,
      elementDecs,
      [dec]
    ));

Comment on lines 214 to 218
1. `static #b`: This is the method itself, which being a private method we
cannot overwrite with `defineProperty`. We also can't convert it into a
private field because that would change its semantics (would make it
writable). So, we instead have it proxy to the locally scoped `callB`
variable, which will be populated with the fully decorated method.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this code log true or false?

function x() {}

function replace() {
  return x;
}

class C {
  @replace
  static #m() {}
  
  static getM() {
    return this.#m;
  }
}

console.log(C.getM() === x);

if it should log true, then this proxy does not work correctly. We might decide that it's ok and it's a known limitation, but we could make it work by converting #m to a field and replacing obj.#m = val assignments with obj, val, _readOnlyError("#m") calls (_readOnlyError is an existing helper).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code should return true, but it definitely is not a common use case so it would be alright if we did not support this. I think the larger issue is that it seems private fields cannot be accessed prior to initialization, so for instance:

let getA;

class Foo {
  #a = ((this.#b = (v) => v), this.#b(123));

  #b;

  getA() {
    return this.#a;
  }
}

(new Foo()).getA()

This code does not work. With a true private method, it would work (tested in Chrome at least), and I think this is generally a more common pattern so I think we'll still want to do the proxy method here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhm right, private methods would need to be "hoisted" before all the private fields when converting them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, yeah I think we can do that and that should work actually, will update

@@ -7,6 +7,7 @@ import {
FEATURES,
} from "@babel/helper-create-class-features-plugin";
import legacyVisitor from "./transformer-legacy";
import transformer2020_09 from "./transformer-2020-09";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: We can rename this to 2021-12, since it's when it was last presented (without @init).

Comment on lines 224 to 226
wrapped with decorator code/logic. They then `delete` this temporary property,
which is necessary because no additional elements should be added to a class
definition.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this deleted property somewhere observable? i.e. can decorators access _Class before it's deleted?

We might consider storing it as a private method (which then doesn't need to be deleted), and pass an additional array of "private accessors" to _applyDecorators, like

applyDecs(
    this,
    elementDecs,
    classDecs,
    [o => o.#original_b]
);

If we end up using the "private method to fields" design suggested in the above comment, then we only need to do

static #b = function () {
  console.log('foo');
}

and then pass it like

applyDecs(
    this,
    elementDecs,
    classDecs,
    [this.#b]
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I came up with this before I realized that we could use static blocks (and in fact needed to in order to match the spec), and I just tested it out, we can actually refer to private names in static blocks. So we can fully extract private methods to the static block and avoid the delete in these cases 😄 I'll think about the best way to do this and update the code of applyDecs accordingly, no need to add private class fields.

Comment on lines 280 to 281
which the decorator will get via `Object.getOwnPropertyDescriptor`. They will be
deleted from the class fully, so again the name is not important here, just
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also here, if possible I'd prefer to pass them in a new last param of applyDecs rather than deleting.

Comment on lines 1 to 11
{
"plugins": [
[
"proposal-decorators",
{ "version": "2020-09", "decoratorsBeforeExport": false }
],
"proposal-class-properties",
"proposal-private-methods",
"proposal-class-static-block"
]
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you copy all these tests to also see how the transform behaves when only compiling decorators?

@JLHwung JLHwung self-requested a review December 28, 2021 04:20
@pzuraq pzuraq force-pushed the add-new-decorators-transform branch 2 times, most recently from 6a86175 to 04b7188 Compare December 29, 2021 16:08
}

path.traverse({
AssignmentExpression(path) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge cases: A member expression can also appears in a UpdateExpression or a destructuring assignment target:

this.#x++;
[...this.#x] = [];
for (this.#x of []);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, in those cases I don't believe that the readOnlyError helper will really work, it'll be a syntax error. Should we just throw a compiler error if we find any assignments to the method? This may also be a capability we add in the future (see: tc39/proposal-decorators#438) so we could throw an error for now, and in the future if we end up supporting setting to decorated methods just remove that error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pzuraq pzuraq force-pushed the add-new-decorators-transform branch from 04b7188 to b75a922 Compare December 29, 2021 23:40
}

/**
* Generates a new element name that is unique to the given class. This can be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"unique to the given class" might not be enough. It must be unique in the given class and in all the class enclosing the current one. For example:

class A {
  #A = 1;
  static B = class B extends A {
    accessor a = 2;
    
    getA() {
      return this.#A;
    }
}

new A.B().getA();

we don't want to transform it to

class A {
  #A = 1;
  static B = class B extends A {
    #A = 2;
    get a() { return this.#A }
    set a(v) { this.#A = v }
    
    getA() {
      return this.#A;
    }
}

new A.B().getA();

otherwise it returns 2 rather than 1.


Now that this PR is mostly finished, could you add new commits without squashing 🙏 It makes it easier to see what changed since the last review.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, updated the logic to remove the public UID part because we no longer use that, and now it takes in all referenced PrivateNames within the class body, so it should be sufficiently unique. If a duplicate key does exist within a parent, I don't think it should matter in the child unless the child references it, correct me if I'm wrong there.

Now that this PR is mostly finished, could you add new commits without squashing 🙏 It makes it easier to see what changed since the last review.

No prob!

@pzuraq pzuraq force-pushed the add-new-decorators-transform branch 2 times, most recently from 2e8d2d6 to 9960464 Compare December 31, 2021 15:06
@nicolo-ribaudo
Copy link
Member

Feel free to ignore the CI / Publish to local Verdaccio registry; it will be fixed by rebasing the feat-7.16.0/decorators branch.

import { types as t } from "@babel/core";
import syntaxDecorators from "@babel/plugin-syntax-decorators";
import ReplaceSupers from "@babel/helper-replace-supers";
import * as charCodes from "charcodes";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you edit

babel/babel.config.js

Lines 197 to 199 in ddd93b9

test: ["packages/babel-generator"].map(normalize),
plugins: ["babel-plugin-transform-charcodes"],
},
to also inline charCodes in this file?

Comment on lines 40 to 45
if (legacy) {
if (version !== undefined) {
throw new Error("'version' can't be used with legacy decorators");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also allow version: "legacy", but throw if both version and legacy are set. Then in Babel 8 we can remove legacy: true, to reduce the number of options.

* keeping the length of those names as small as possible. This is important for
* minification purposes, since public names cannot be safely renamed/minified.
*
* Names are split into two namespaces, public and private. Static and non-static
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't true anymore, since we only use it for private fields?

Comment on lines 66 to 73
let reifiedId = String.fromCharCode(...currentPrivateId);

while (privateNames.has(reifiedId)) {
incrementId(currentPrivateId);
reifiedId = String.fromCharCode(...currentPrivateId);
}

incrementId(currentPrivateId);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, but we could compact this code a bit by doing

let reifiedId;
do {
  reifiedId = String.fromCharCode(...currentPrivateId);
  incrementId(currentPrivateId);
} while (privateNames.has(reifiedId));

and currentPrivateId starts from [] rather than [charCodes.uppercaseA].

[_Foo, _initClass] = babelHelpers.applyDecs(Foo, [], [dec]);
})();

babelHelpers.defineProperty(Foo, "field", 123);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct that this uses Foo instead of _Foo?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So technically I don't think so, it should be using _Foo. However, I think that it's not really possible to do this, particularly with static private fields. We would have to extract all of the static fields and assign them to _Foo instead, but static private fields need to be defined within a class body to be valid.

I think there's a few options here:

  1. Keep it as is, and note this as a limitation of the transform. The biggest meaningful difference would be that static public fields would not be own properties of the class, which I don't think is very common behavior to rely on.
  2. Extract all static public fields, leave static private fields, and disregard ordering. I think that classes tend to rely on field initialization order more often than own-ness, so I don't think this is the best option.
  3. Extract all static public fields, but assign them within static blocks in between private field assignments. Probably the most accurate transform, but also the most work.

t.classMethod(
"constructor",
t.identifier("constructor"),
[t.restElement(t.identifier("args"))],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need ...args if the class does not have superClass , right?


const elementDecs = [
[dec, 0, 'a'],
[dec, 7, 'x', '#b']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to update the example here because the method implementation is now moved to the static block so x is no longer required.

@pzuraq
Copy link
Contributor Author

pzuraq commented Jan 16, 2022

There was one minor change we discussed at the decorators meeting this week, which was to have the metadata objects inherit from Object.prototype instead of being Object.create(null). Other than that, I think this should be good to go!

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already approved this PR, but there have been a bunch of new changes.

@nicolo-ribaudo nicolo-ribaudo merged commit ec325e2 into babel:feat-7.16.0/decorators Jan 24, 2022
nicolo-ribaudo added a commit that referenced this pull request Jan 24, 2022
Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>
nicolo-ribaudo added a commit that referenced this pull request Jan 24, 2022
Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>
@nicolo-ribaudo
Copy link
Member

Merged to #13827

nicolo-ribaudo added a commit that referenced this pull request Jan 27, 2022
Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>
nicolo-ribaudo added a commit that referenced this pull request Feb 1, 2022
Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label May 6, 2022
@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 6, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue PR: New Feature 🚀 A type of pull request used for our changelog categories Spec: Decorators
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants