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
Add support for the new decorators proposal #7976
Conversation
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" |
There was a problem hiding this comment.
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.
f6239a6
to
2026871
Compare
@@ -37,7 +37,7 @@ export default declare((api, opts = {}) => { | |||
return { | |||
presets: [[presetStage3, { loose, useBuiltIns }]], | |||
plugins: [ | |||
[transformDecorators, { legacy: decoratorsLegacy }], | |||
[transformDecorators, { legacy: decoratorsLegacy && false }], |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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]
😛
There was a problem hiding this comment.
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.
ba46a74
to
8b45686
Compare
@@ -28,6 +21,8 @@ export default declare((api, options) => { | |||
if (typeof decoratorsBeforeExport !== "boolean") { | |||
throw new Error("'decoratorsBeforeExport' must be a boolean."); | |||
} | |||
} else if (!legacy) { |
There was a problem hiding this comment.
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.
9e25568
to
6dd4a0f
Compare
I added some commits to make the helpers match 1:1 the spec text |
f30514b
to
ce8a181
Compare
There was a problem hiding this 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", |
There was a problem hiding this comment.
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 :) |
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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++) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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."); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adding other tests to check the ordering of finishers
Thanks for the review Daniel, I will update the PR in the next few days. |
); | ||
} | ||
|
||
const { decoratorsBeforeExport } = options; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
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?
There was a problem hiding this 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.
b66722a
to
ba7d327
Compare
There was a problem hiding this 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.
What is blocking this PR from landing? |
export placement? stage advancement? |
Actually nothing, but since this PR is quite big I'm waiting for more reviews. |
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👏 les do it!
ba7d327
to
b304993
Compare
There was a problem hiding this 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.", |
There was a problem hiding this comment.
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 }) { |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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[] */, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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")}( |
There was a problem hiding this comment.
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")} |
There was a problem hiding this comment.
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.
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.' |
I think the docs are wrong. You have to choose a value. The proposed spec hasn’t settled the placement yet. |
Repl: https://babeljs.io/repl/build/8579/ (need to turn on the stage-2 preset)
Repo: https://github.com/tc39/proposal-decorators
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 BEFORE MERGING]
commits