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

Decorators 2 Transform [WIP] #6107

Closed
wants to merge 23 commits into from
Closed

Conversation

@peey
Copy link
Contributor

@peey peey commented Aug 15, 2017

Q A
Fixed Issues Progress on babel/proposals#11
Patch: Bug Fix? No
Major: Breaking Change? No
Minor: New Feature? Yes
Tests Added/Pass? Yes
Spec Compliancy? Yes
License MIT
Doc PR No
Any Dependency Changes? Yes, on decorators2 syntax plugin

Note: I'll use the terms evaluation to mean evaluation of @dec(args) and decoration to mean actual execution of the decorators

Also, note that the decorator proposal introduces the concept of an elementDescriptor, which is distinct from the propertyDescriptor we all know about.

To understand the code, you need to know that an elementDescriptor is an object with the shape:

{
  kind: "property", // I don't know what other values kind can take
  key: <the name of the method>,
  isStatic: true|false,
  descriptor: <the property descriptor of the method>
}

I've implemented the methods decorate, decorateElement, decorateClass, makeElementDescriptor, and mergeDuplicateElements in helpers.js. Out of these, decorateElement and decorateClass correspond to the spec operations DecorateElement, and DecorateClass respectively. mergeDuplicateElements corrseponds to the operation used in step 6 of both DecorateClass and DecorateElement.

The method decorate runs the whole thing. It takes care of the evaluation order (see below) and corrsponds to Runtime Semantics: ClassDefinitionEvaluation step 21 onwards.

The changes in babel-core are from #6058

Assumptions Made: (Please feel free to correct)

  1. evaluation order is as the following: (relevant spec operation)
    a. evaluate all method decorators in order [step 21.g. of above spec link] (left to right / outer to inner)
    b. (decoration) execute all method decorators in reverse order step 24.a.(right to left / inner to outer)
    c. evaluate all class decorators in order step 29. (assuming that DecoratorListEvaluation happens just before this, since it's not explicitly mentioned)
    d. (decoration) execute all class decorators in reverse order step 29
  2. the signature of decorators is as follows:
// for member decorator @foo(a,b,c)
function foo(a, b, c) {
   return (descriptor) => {descriptor, extras, finishers};
} 

// for member decorator @foo
function foo(descriptor) {return {descriptor, extras, finishers}}

// for class decorator @foo(a,b,c)
function foo(a,b,c) {
  // elements is an array of  descriptors of all members after running the decorators
  return (ctor, parent, memberDescriptors) => {constructor, elements, finishers}
}

// for class decorator @foo
function (ctor, parent, memberDescriptors) {return {constructor, elements, finishers}}
  1. The spec uses two forms - a finisher attached to the elementDescriptor and explicitly returning finishers array. I've largely ignored the former because it doesn't seem to make any sense.
  2. The spec is clear that elementDescriptor is passed to method decorators and not propertyDescriptor, and that's how I had implemented it in the past. However, I changed it to use propertyDescriptor because using elementDescriptor would imply that the decorator can change the key of the method being decorated, and if it is static or not. I'm not sure if this behaviour is intended or unintended. I need more feedback on this from the spec designers. Changing back to elementDescriptor shouldn't be too much trouble.

TODO / WIP:

  • tests for heritage
  • configurability check and error if a decorator tries to configure a non-configurable descriptor
  • handling undecorated accessors descriptors (getters and setters)
  • more tests for transformation, optimizing for special cases.
@peey peey closed this Aug 15, 2017
@peey peey changed the title Decorators 2 Transform [WIP Decorators 2 Transform [WIP] Aug 15, 2017
@peey
Copy link
Contributor Author

@peey peey commented Aug 15, 2017

I accidentally opened this PR with incomplete info. Now that I've edited it to include all details, I'm reopening

@peey peey reopened this Aug 15, 2017
@hzoo hzoo requested review from littledan and diervo Aug 15, 2017
@hzoo hzoo added the es-proposal label Aug 15, 2017
@littledan
Copy link

@littledan littledan commented Aug 15, 2017

I'm proposing some changes to the decorator proposal at https://github.com/littledan/proposal-unified-class-features . You can see a summary of the changes in this presentation. The committee and decorators champions seemed to view it positively. I'd be interested to hear what you think.

I don't think this follow-on proposal should affect most of the implementation. I'll review this patch as an implementation of this decorators proposal.

@jkrems
Copy link
Contributor

@jkrems jkrems commented Aug 15, 2017

@littledan Does removing finishers from element decorators mean they can no longer safely attach meta data? Especially if finishers can now replace the class completely? I thought this was done explicitly to allow meta data "annotation"-style decorators that are hard or impossible to implement with stage 0?

@babel-bot babel-bot mentioned this pull request Aug 15, 2017
1 of 2 tasks complete
@hzoo hzoo mentioned this pull request Aug 15, 2017
@littledan
Copy link

@littledan littledan commented Aug 15, 2017

@jkrems You can attach metadata by putting it on the methods in an additional property, or making a WeakMap keyed off of the method. Would that work for you?

@jkrems
Copy link
Contributor

@jkrems jkrems commented Aug 15, 2017

@littledan The nice thing about finalizers was that it allowed to the 2nd thing safely - no matter what other decorators would do.

@injectDataSource
class MyResource {
  @deprecated // not 100% sure about evaluation order
  @GET('/users/{id}') // wants to attach meta data to `getUser`
  async getUser(params) { /* ... */ }
}

We're assuming here that:

  1. deprecated wraps the actual function value that GET saw in another function that logs a deprecation warning.
  2. injectDataSource could be something like a redux connect helper. The point being: It wants to wrap the constructor itself.

In the previous stage 2 iteration, @GET could use a finalizer to myWeakMap.set(finalCtor, metaData). If it tries to do the same during its own run, it would get the MyResource class before being wrapped. And the meta data would get lost. If it tries to attach the meta data to the function/property value itself, it will be swallowed by deprecated.

Copy link
Member

@Jessidhia Jessidhia left a comment

I just looked at the tests for now; will look at the code later

function log(message) {
return function (descriptor) {
let oldFunc = descriptor.value;
descriptor.value = function(...args) {

This comment has been minimized.

@Jessidhia

Jessidhia Aug 16, 2017
Member

Somewhat unfortunately, there is no way of making this function's name derive from the original function name (instead of "value") other than by calling defineProperty on it afterwards 🤔

This comment has been minimized.

@peey

peey Aug 16, 2017
Author Contributor

Actually, we'll probably be passing the element descriptor to the decorator and not the property descriptor. I just had a doubt on how that'd work when you change the key of the descriptor you've returned and that's why I made the call to implement it like this. Once I get a little clarification on assumption #4 from @littledan or someone, I'll make the desired changes

}

class Foo {
@overrider(() => 3) method() {

This comment has been minimized.

@Jessidhia

Jessidhia Aug 16, 2017
Member

What is this here? Is it the constructor, the outer this, or the newly created instance?

Could be worth having a test for it.

const clone = {
kind: "property",
isStatic: !!isStatic,
key: name,

This comment has been minimized.

@Jessidhia

Jessidhia Aug 16, 2017
Member

What should happen if the name conflicts? @spare("foo") foo() {}

This comment has been minimized.

@peey

peey Aug 16, 2017
Author Contributor

Currently what I've interpreted from the spec is that it should "merge". By that I mean it will just take on the value of the last property added with the key "foo". See decorateElement step 6. Also see mergeDuplicateElements helper


undecorated() {}

}, [["undecorated"]], [["method", [methDec]]], void 0)([classDec]);

This comment has been minimized.

@Jessidhia

Jessidhia Aug 16, 2017
Member

(Having not looked at the decorate implementation yet) Too many arrays? Some of these look like they could be flattened one level, but I'm only guessing at the meaning of each parameter.

This comment has been minimized.

@peey

peey Aug 16, 2017
Author Contributor

currently I'm using const [key, isStatic] = [..] so that's why the inner items of those are arrays (in case of a non static member, I just omit the second array element). We could certainly simplify it further if needed, e.g. if it's static then it'll look like ["method", true] otherwise it'll look like just "method" and not ["method"]


[calculated + and + undecorated]() {}

}, [["andAnUndecoratedMethod"], [calculated + and + undecorated]], [["m1", [dec]], ["m2", [bar, foo.bar(baz)]], [(_key = 3 + 7), [dec], true]], void 0)([decorator]);

This comment has been minimized.

@Jessidhia

Jessidhia Aug 16, 2017
Member

calculated + and + undecorated should be put in a ref as well (could be an impure expression)

This comment has been minimized.

@peey

peey Aug 16, 2017
Author Contributor

I'm using babel's built-in isPure which is why that wasn't put in a ref

This comment has been minimized.

@peey

peey Aug 20, 2017
Author Contributor

You're right, this should have been put in a ref. Nicolo pointed out the error in code and I've fixed it, will push soon

}
// decorate and store in elementDescriptors
for (const [key, decorators, isStatic] of memberDecorators) {

This comment has been minimized.

@xtuc

xtuc Aug 16, 2017
Member

Can we ES2015 here?

And a bit later, Array.from will require a polyfill.
It could cause issues like this #5876, right?

This comment has been minimized.

@peey

peey Aug 16, 2017
Author Contributor

I wish we could. But if we can't, I'll refactor it to use ES5, it shouldn't be complex. I'll leave this be till the end though, since the code is functionally correct so for the review we could focus on just that

@@ -21,6 +21,6 @@ function t() {

function t() {
for (var i = 0; i < arguments.length; i++) {
return arguments.length <= i ? undefined : arguments[i];

This comment has been minimized.

@hzoo

hzoo Aug 16, 2017
Member

this should be fixed

This comment has been minimized.

@peey

peey Aug 17, 2017
Author Contributor

I'm not sure if this is related to this PR. I'll investigate more

for (const [key, decorators, isStatic] of memberDecorators) {
const target = isStatic ? constructor : prototype;
const propertyDescriptor =
elementDescriptors.has([key, isStatic]) && elementDescriptors.get([key, isStatic]).descriptor

This comment has been minimized.

@littledan

littledan Aug 30, 2017

This has call will always return false, since ES6 maps are by identity. Since [1, 2] !== [1, 2], calling Map.prototype.has on a fresh array will never do what you're hoping for.

Instead, you could maintain two Sets of keys, or two objects with keys and values, for the static and non-static halves.

finishers = finishers.concat(result.finishers);
//TODO: heritage hacks so result.constructor has the correct prototype and instanceof results
//TODO: step 38 and 39, what do they mean "initialize"?

This comment has been minimized.

@littledan

littledan Aug 30, 2017

"Initialize" was about having methods and the constructor start in some state where they would be unusable, and make them usable later. In my follow-on decorators proposal, I'm suggesting to get rid of this initialized/uninitialized state. I think it's OK to go without it to start, even if it's possible that TC39 will ask to bring it back.

);
finishers = finishers.concat(result.finishers);
//TODO: heritage hacks so result.constructor has the correct prototype and instanceof results

This comment has been minimized.

@littledan

littledan Aug 30, 2017

Heritage is just given as an argument to class decorators; it's the responsibility of the decorators to do the appropriate thing with that, I believe.

@danez danez closed this Aug 31, 2017
@danez danez reopened this Aug 31, 2017
@danez danez changed the base branch from 7.0 to master Aug 31, 2017
@hzoo hzoo mentioned this pull request Sep 14, 2017
10 of 10 tasks complete
peey added 2 commits Sep 16, 2017
@peey
Copy link
Contributor Author

@peey peey commented Sep 16, 2017

Apologies for responding late to the review. Uni workload has started again.

Updates: Now we're merging getters and setters & other things pointed out by littledan in their review

Todo:

  • Refactor merging so we can merge when adding extras instead of overriding
  • Look into if other props things like enumerable, configurable, writable & value (in case of data descriptor) are also meant to be merged. @littledan thoughts on this?
  • Fix the failing test for x instanceof Foo if Foo has been processed by the transform
  • Incorporate nicolo's #6254
I had missed extends in the test case but the asserts were expecting it.
I thought that tests were failing for more sinister reasons, but
thankfully it isn't so
Copy link

@littledan littledan left a comment

I've confirmed with @bterlson and @wycats that http://tc39.github.io/proposal-unified-class-features should be considered to subsume http://tc39.github.io/proposal-decorators for the planned spec text.

@hzoo hzoo mentioned this pull request Sep 27, 2017
2 of 9 tasks complete
@hzoo hzoo added the Priority: High label Nov 29, 2017
@hzoo hzoo added this to the Babel 7.next milestone Dec 27, 2017
buschtoens added a commit to buschtoens/acme-v2 that referenced this pull request Feb 28, 2018
@nicolo-ribaudo nicolo-ribaudo mentioned this pull request Mar 10, 2018
1 of 2 tasks complete
@peey
Copy link
Contributor Author

@peey peey commented Mar 12, 2018

Update: @nicolo-ribaudo asked me if they could take this up, and I told them they could.

The outstanding work on this PR would have been the todo in my previous comment and then any changes between the spec at the time this PR was made and the spec now (I'm not sure about the exact status of this, seems like http://tc39.github.io/proposal-unified-class-features that @littledan's comment specified is dead and http://tc39.github.io/proposal-decorators has been updated)

Since Babel's codebase has changed much (and the proposal may have too), I guess Nicolo's taking the route of reworking this feature and salvaging code from this PR as needed in #7542 . Kudos and good luck!

Here's what might be useful from this PR:

@nicolo-ribaudo nicolo-ribaudo mentioned this pull request May 18, 2018
1 of 4 tasks complete
@nicolo-ribaudo
Copy link
Member

@nicolo-ribaudo nicolo-ribaudo commented Jun 3, 2018

Closing in favor #7976. Thank you @peey for your work!

@lock lock bot added the outdated label Oct 4, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Oct 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

9 participants
You can’t perform that action at this time.