Add support for the new decorators proposal#7976
Add support for the new decorators proposal#7976nicolo-ribaudo merged 2 commits intobabel:masterfrom
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.
Reminder to myself - this should be a dependency.
f6239a6 to
2026871
Compare
| presets: [[presetStage3, { loose, useBuiltIns }]], | ||
| plugins: [ | ||
| [transformDecorators, { legacy: decoratorsLegacy }], | ||
| [transformDecorators, { legacy: decoratorsLegacy && false }], |
There was a problem hiding this comment.
this will prevent decoratorsLegacy from ever affecting the value. It will always be false?
There was a problem hiding this comment.
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.
oh, Ha... I didn't see the commit message :) sorry.
ba46a74 to
8b45686
Compare
| if (typeof decoratorsBeforeExport !== "boolean") { | ||
| throw new Error("'decoratorsBeforeExport' must be a boolean."); | ||
| } | ||
| } else if (!legacy) { |
There was a problem hiding this comment.
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
littledan
left a comment
There was a problem hiding this comment.
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.
This has changed--it's now "Descriptor". tc39/proposal-decorators#96 (ditto for Method Descriptor, etc)
| } | ||
| `; | ||
|
|
||
| // Don't review me, review babel-helpers/src/helpers/decorators.js :) |
|
|
||
| // This is exposed to the user code | ||
| type ElementObjectInput = ElementDescriptor & { | ||
| [@@toStringTag]?: "Method Descriptor" | "Field Descriptor" |
There was a problem hiding this comment.
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 |
| 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.
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.
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.
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.
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.
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.
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.
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.
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); |
|
|
||
| function push(x) { log.push(x); return x; } | ||
|
|
||
| function logFinisher(a, b) { |
There was a problem hiding this comment.
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.
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.
nit: do we want to just move this destructuring to line 9?
There was a problem hiding this comment.
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.
I think it's ok to not use env in these tests? What do you think?
hzoo
left a comment
There was a problem hiding this comment.
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
littledan
left a comment
There was a problem hiding this comment.
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. |
ba7d327 to
b304993
Compare
loganfsmyth
left a comment
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Loving the type annotations by the way, thanks for doing that :D
There was a problem hiding this comment.
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.
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.
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