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 support for the new decorators proposal #7976

Merged
merged 2 commits into from Sep 7, 2018

Conversation

nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented May 18, 2018

Repl: https://babeljs.io/repl/build/8579/ (need to turn on the stage-2 preset)

Repo: https://github.com/tc39/proposal-decorators

Q                       A
Fixed Issues? Closes #7542, closes #6107, fixes #7773
Patch: Bug Fix?
Major: Breaking Change?
Minor: New Feature? 🎉
Tests Added + Pass? Yes
Documentation PR
Any Dependency Changes?
License MIT

Support for private elements is still missing, but I will work on it after that this PR and #7842 are merged.

This PR should be merged after #7938 and #7948, since I will add some tests for them in this PR.

cc @littledan


Hi possible reviewers 🙂
Since this PR is quite big, it would be very helpful if you could review even just a part of it:


TODO:

  • Revert [REVERT BEFORE MERGING] commits

@nicolo-ribaudo nicolo-ribaudo added PR: New Feature 🚀 A type of pull request used for our changelog categories Spec: Decorators labels May 18, 2018
@babel-bot
Copy link
Collaborator

babel-bot commented May 18, 2018

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

"@babel/helper-plugin-test-runner": "7.0.0-beta.47"
"@babel/helper-plugin-test-runner": "7.0.0-beta.47",
"@babel/helper-replace-supers": "7.0.0-beta.47",
"@babel/helper-split-export-declaration": "7.0.0-beta.47"
Copy link
Member Author

Choose a reason for hiding this comment

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

Reminder to myself - this should be a dependency.

@@ -37,7 +37,7 @@ export default declare((api, opts = {}) => {
return {
presets: [[presetStage3, { loose, useBuiltIns }]],
plugins: [
[transformDecorators, { legacy: decoratorsLegacy }],
[transformDecorators, { legacy: decoratorsLegacy && false }],
Copy link

Choose a reason for hiding this comment

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

this will prevent decoratorsLegacy from ever affecting the value. It will always be false?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope, it's just to test decorators in the REPL before that this PR is meged. That is why I labeled the commit as [REVERT BEFORE MERGING] 😛

Copy link

Choose a reason for hiding this comment

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

oh, Ha... I didn't see the commit message :) sorry.

@nicolo-ribaudo nicolo-ribaudo added the PR: Needs Review A pull request awaiting more approvals label Jun 2, 2018
This was referenced Jun 3, 2018
@@ -28,6 +21,8 @@ export default declare((api, options) => {
if (typeof decoratorsBeforeExport !== "boolean") {
throw new Error("'decoratorsBeforeExport' must be a boolean.");
}
} else if (!legacy) {
Copy link
Contributor

@diervo diervo Jun 3, 2018

Choose a reason for hiding this comment

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

I don't think we need to the option to toggle between before and after exports no more.
I believe there were very strong blocking arguments towards the exports before.

@nicolo-ribaudo
Copy link
Member Author

I added some commits to make the helpers match 1:1 the spec text

Copy link

@littledan littledan left a comment

Choose a reason for hiding this comment

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

This PR looks great. I don't see any non-trivial errors.

I haven't developed any sort of proper test plan, but you may want to add tests for finisher ordering and enumeration ordering of elements/extras.

}

expect(el).toEqual({
[Symbol.toStringTag]: "Field Descriptor",

Choose a reason for hiding this comment

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

This has changed--it's now "Descriptor". tc39/proposal-decorators#96 (ditto for Method Descriptor, etc)

@@ -1011,3 +1011,498 @@ helpers.classPrivateFieldSet = () => template.program.ast`
return value;
}
`;

// Don't review me, review babel-helpers/src/helpers/decorators.js :)

Choose a reason for hiding this comment

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

decorate.js?


// This is exposed to the user code
type ElementObjectInput = ElementDescriptor & {
[@@toStringTag]?: "Method Descriptor" | "Field Descriptor"

Choose a reason for hiding this comment

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

This has changed to just "Descriptor". Same for class descriptors. The change comes up in a few cases below.

*/

/*::
// Various combinations with/without extras and with one or manu finishers

Choose a reason for hiding this comment

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

Nit: s/manu/many/

finishers /*: ClassFinisher[] */,
) /*: Class<*> */ {
for (var i = 0; i < finishers.length; i++) {
var newConstructor /*: ?Class<*> */ = (0, finishers[i])(constructor);

Choose a reason for hiding this comment

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

I don't understand the purpose of 0, here--is this to avoid function name inference? I don't understand why that would occur. Ditto for a few other similar locations.

Copy link
Member

@Jessidhia Jessidhia Jul 6, 2018

Choose a reason for hiding this comment

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

This erases the this by separating the [[Get]] from [[Call]]. If finishers[i](constructor) was directly invoked, the finisher would receive the finishers list as the this.

Choose a reason for hiding this comment

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

Ah, of course. Thanks for explaining.

if (elementsAndFinisher.elements !== undefined) {
elements = elementsAndFinisher.elements;

for (var j = 0; j < elements.length - 1; j++) {

Choose a reason for hiding this comment

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

Maybe a good idea to insert a TODO to check if there's a problem caused by this quadratic algorithm, though it seems fine to start.

Copy link
Member Author

Choose a reason for hiding this comment

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

How else could it be implemented? Creating a Set instead of the array wouldn't work because we need to check some properties on the obejcts, not the objects themselves.

Copy link

@littledan littledan Jul 11, 2018

Choose a reason for hiding this comment

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

There are only three possible placements, so I think you can implement this as three Sets of keys. (This could be organized as, an object which has a property for each placement, which is a Set.)

for (var j = 0; j < newExtras.length; j++) {
_addElementPlacement(newExtras[j], placements);
}
extras = extras.concat(newExtras);

Choose a reason for hiding this comment

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

Consider including a TODO to fix this quadratic algorithm (extras will repeatedly be copied).


function _disallowProperty(obj, name) {
if (obj[name] !== undefined) {
throw new TypeError("Unexpected '" + name + "' property.");

Choose a reason for hiding this comment

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

A detailed error message here would be great (may mean that the function takes some extra arguments).

[push(13)]() {}
}

var numsFrom0to9 = Array.from({ length: 24 }, (_, i) => i);

Choose a reason for hiding this comment

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

Variable name?


function push(x) { log.push(x); return x; }

function logFinisher(a, b) {

Choose a reason for hiding this comment

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

This has to do with the execution of the decorator, not the finisher; consider renaming this test and adding other tests to check the ordering of finishers.

Copy link
Member Author

Choose a reason for hiding this comment

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

@xtuc xtuc closed this Jul 6, 2018
@xtuc xtuc reopened this Jul 6, 2018
@nicolo-ribaudo
Copy link
Member Author

Thanks for the review Daniel, I will update the PR in the next few days.

);
}

const { decoratorsBeforeExport } = options;
Copy link
Member

Choose a reason for hiding this comment

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

nit: do we want to just move this destructuring to line 9?

Copy link
Member Author

Choose a reason for hiding this comment

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

I prefer to keep it there alongside with the decoratorsBeforeExport validation.

@@ -0,0 +1,4 @@
{
"plugins": ["proposal-decorators", "proposal-class-properties", "external-helpers"],
"presets": ["env"]
Copy link
Member

Choose a reason for hiding this comment

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

I think it's ok to not use env in these tests? What do you think?

Copy link
Member

@hzoo hzoo left a comment

Choose a reason for hiding this comment

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

Awesome work! I think we can clean up the test a bit by not running the test of the env preset tho

Curious about sharing the helper functions in src/transformer.js like prop, value since it's probably in @babel/types but really not a big deal, can refactor that stuff later.

Copy link

@littledan littledan left a comment

Choose a reason for hiding this comment

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

Thanks for fixing the initializer issue. I don't have any further concerns.

@littledan
Copy link

What is blocking this PR from landing?

@jsg2021
Copy link

jsg2021 commented Sep 5, 2018

export placement? stage advancement?

@nicolo-ribaudo
Copy link
Member Author

Actually nothing, but since this PR is quite big I'm waiting for more reviews.

@pabloalmunia
Copy link

I'm read the new code and look very well. Please, go a head with this important contribution ASAP.

@nicolo-ribaudo very good work, congratulations.

Copy link
Member

@existentialism existentialism left a comment

Choose a reason for hiding this comment

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

👏 les do it!

Copy link
Member

@loganfsmyth loganfsmyth left a comment

Choose a reason for hiding this comment

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

Awesome work. I'm leaving some comments, but I don't think any of them are realistically blockers, so if you want to land this and address them in a second PR, that's 100% fine.

if (!legacy) {
throw new Error(
"The decorators plugin requires a 'decoratorsBeforeExport' option," +
" whose value must be a boolean.",
Copy link
Member

Choose a reason for hiding this comment

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

Can we mention legacy: true in here too so it is more discoverable?

return false;
}

function extractDecorators({ node }) {
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: From a naming standpoint, takeDecorators might make it clearer that this also clears the decorators.

if (node.computed) {
return node.key;
} else {
return t.stringLiteral(node.key.name || String(node.key.value));
Copy link
Member

Choose a reason for hiding this comment

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

I think explicitness here can be a good thing, e.g.

if (node.computed) {
  return node.key;
} else if (t.isIdentifier(node.key)) {
  return t.stringLiteral(node.key.name);
} else {
  return t.stringLiteral(`${node.key.value}`);
}

}

function getElementsDefinitions(path, fId, file) {
const superRef = path.node.superClass || t.identifier("Function");
Copy link
Member

Choose a reason for hiding this comment

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

Is Function here right? It looks like ReplaceSupers already defaults to Function.prototype when there is no super class. Is that not right?

}

function value(body, params = []) {
return t.objectMethod("method", t.identifier("value"), params, body);
Copy link
Member

Choose a reason for hiding this comment

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

How do you feel about generating an object method vs a property containing a function expression here? I usually prefer to generate ES5 code, but what you have is also fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that ES5 was a good target for ES2015/ES2016 transforms, but now we can output ES6 since users will either only support browsers which support ES6 or compile it down with preset-env.

Copy link
Member

@Jessidhia Jessidhia Sep 7, 2018

Choose a reason for hiding this comment

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

We still have to be careful with helpers, they still have to be as ES3-compatible as possible; but generated output should be safe to use the latest ES20xx syntax that was current before it is made stage-4 I think.

F: A,
d: []
};
}, (await B));
Copy link
Member

Choose a reason for hiding this comment

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

That's a fun edge case. Do we need to worry about

@dec
class Foo {
  [await prop()](){}
}

and

class Foo {
  @(await thing())
  method(){}
}

too?

Might at least be good to explicitly throw an error about that being unsupported. Same for yield.

Copy link
Member

Choose a reason for hiding this comment

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

Those look like perfectly valid syntax to me (inside an async function) 🤔

Or does being inside a class override the +async?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's tracked at #8300. I will add a nice error message.

@Kovensky It's just very hard to support (we don't support it in computer keys either), I will revisit it in another PR.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, a problem with our generated function wrapper...

superClass /*: ?Class<*> */,
) /*: Class<*> */ {
var r = factory(function initialize(O) {
_initializeInstanceElements(O, decorated.elements);
Copy link
Member

Choose a reason for hiding this comment

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

I'm trying to think, is it possible for this to get called before decorated is initialized? Something would have to call new on the class, which may not be possible?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, it isn't possible because the class can't be new-ed before being returned by the factory function.


// ClassDefinitionEvaluation (Steps 26-*)
export default function _decorate(
decorators /*: ClassDecorator[] */,
Copy link
Member

Choose a reason for hiding this comment

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

Loving the type annotations by the way, thanks for doing that :D

Copy link
Member Author

Choose a reason for hiding this comment

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

With #8487 we could probably even type-check them 😛

insertInitializeInstanceElements(path, initializeId);

const expr = template.expression.ast`
${file.addHelper("decorate")}(
Copy link
Member

Choose a reason for hiding this comment

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

It might be nice for users to wrap a try/catch here to give users more feedback, e.g.

function addDecorateHelper(file) {
  try {
    return file.addHelper("decorate");
  } catch (err) {
    if (err.code === "BABEL_HELPER_UNKNOWN") {
      err.message += "\n  @babel/plugin-transform-decorators in non-legacy mode requires @babel/core version ^7.0.1 and you appear to be using an older version";
    }
    throw err;
  }
}

${file.addHelper("decorate")}(
${classDecorators || t.nullLiteral()},
function (${initializeId}, ${superClass ? superId : null}) {
${isStrict ? null : t.stringLiteral("use strict")}
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 quite the right structure unfortunately, because use strict should be a t.directive, and also because this will insert it into body instead of the directives array. I don't think there's an easy way to create an optional directive with babel-templates at the moment unfortunately. I think we'd have to inject it manually after the template AST is created.

@nicolo-ribaudo nicolo-ribaudo merged commit 9aec4ad into babel:master Sep 7, 2018
@nicolo-ribaudo nicolo-ribaudo deleted the decorators-transform branch September 7, 2018 13:58
@pabloalmunia
Copy link

This message is displayed when 'decoratorsBeforeExport' isn't include:

"The decorators plugin requires a 'decoratorsBeforeExport' option, whose value must be a boolean. If you want to use the legacy decorators semantics, you can set the 'legacy: true' option."

But the documentation say 'decoratorsBeforeExport: boolean, defaults to false.'

@jsg2021
Copy link

jsg2021 commented Sep 18, 2018

I think the docs are wrong. You have to choose a value. The proposed spec hasn’t settled the placement yet.

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: Needs Review A pull request awaiting more approvals PR: New Feature 🚀 A type of pull request used for our changelog categories Priority: High Spec: Decorators
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Decorators: implement both optional parens and normal expressions.