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
Private Static Fields Features: Stage 3 #8205
Conversation
2824e86
to
1c43798
Compare
Build successful! You can test your changes in the REPL here: https://babeljs.io/repl/build/8935/ |
1c43798
to
b00fa81
Compare
We only handled private static fields for now, @tim-mc is going to help bring private static methods now. From there we'll be able to think about accessors... |
181e814
to
ff1203e
Compare
ff1203e
to
69e5e2e
Compare
It feels like this PR is ready for review. I also would like some help for making that Travis build pass... |
|
||
expect("bar" in Foo).toBe(false) | ||
expect(Foo.test()).toBe(undefined) | ||
expect(Foo.test()).toBe(undefined) |
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.
Some of these duplicated tests are weird to me expect(Foo.test()).toBe(undefined)
- I know they were just copied over but is it supposed to be testing the static and instance methods or just remove it?
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 it should be expect(() => Foo.test()).not.toThrow()
?
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.
@hzoo, you're right, I'll go ahead and do a second review pass on those tests (was only checking for the output I was seeing). I'm also trying to run test-262 right now against it (having npm link
issues) and I might add some relevant tests then
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 a general thing we should do moving forward is run against test262, I know @leobalter/@rwaldron have done some work there. (@xtuc had https://github.com/xtuc/babel-test262 but we haven't used)
|
||
helpers.classStaticPrivateFieldBase = () => template.program.ast` | ||
export default function _classStaticPrivateFieldBase(receiver, classConstructor, privateClass) { | ||
if (receiver !== classConstructor && receiver.constructor !== classConstructor) { |
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.
The receiver.constructor !== classConstructor
shouldn't be necessary.
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.
Unfortunately, this seems necessary to be spec compliant. We should discuss that on slack.
throw path.buildCodeFrameError( | ||
"Static class fields are not spec'ed yet.", | ||
); | ||
if (privateStaticNames.has(name)) { |
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.
Private names are shared between instance and static, ie, the following is an error:
class Example {
static #x;
#x;
}
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.
Wasn't sure about this one, I'll make the change.
var Foo = | ||
/*#__PURE__*/ | ||
function () { | ||
"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 is my mistake. This test case was meant to output class
expressions, not compile classes to functions.
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 makes more sense indeed, I'll redo the whole test dir with that then!
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.
@jridgewell what should I do to convert tests to output class expressions?
@@ -83,6 +83,18 @@ export default declare((api, options) => { | |||
}, | |||
}; | |||
|
|||
// Traverses the class scope, handling private static name references. | |||
const staticPrivatePropertyVisitor = { |
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 should be able to reuse privateNameVisitor
. This is necessary because we have to use its inner traverser, too.
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.
Ok, I kept them separate to make sure I don't mess with your logic but indeed, it's better to change just a few things in your logic than to branch out an entirely new one!
@@ -1011,3 +1011,12 @@ helpers.classPrivateFieldSet = () => template.program.ast` | |||
return value; | |||
} | |||
`; | |||
|
|||
helpers.classStaticPrivateFieldBase = () => template.program.ast` | |||
export default function _classStaticPrivateFieldBase(receiver, classConstructor, privateClass) { |
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.
So there are a few issues here, and we can choose either of two ways to handle it:
- We can remove this
privateClass
holder object forloose
mode. - We can handle
get
,set
, andcall
for bothloose
andspec
mode.
The first option would allow us to reuse the same code we already have for instance private fields.
If we continue to use a holder object, we have to handle get
, set
, and call
operations on the holder, and use the receiver
as the calling context for all those cases. This complicates the runtime for loose
mode. But, we're gonna need to do that anyways for spec mode.
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 is super helpful, I had a hard time understanding the line between loose and spec.
@@ -157,6 +169,23 @@ export default declare((api, options) => { | |||
}, | |||
}; | |||
|
|||
const staticPrivatePropertyHandler = { | |||
handle(member) { |
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.
So this doesn't handle get
, set
, and call
operations. Given that all this is stored on a holder object, anything like ClassName.#x()
will really have to be transformed to base(ClassName, ClassName, holder)['x'].call(ClassName)
. This currently just outputs base(ClassName, ClassName, holder)['x']()
.
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 I just missed something about how the handlers are working and that made it way clearer. Thanks!
Thanks @jridgewell, I'm sorry I have a lot of work on the side of this but I'll try to get back to it this week. |
e9c8e56
to
83fbdbc
Compare
Todo:
|
2923f1b
to
1c448ee
Compare
Hi @jridgewell, I could use some help changing the tests to not transform the classes. Otherwise, I think I'm almost done. |
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.
Sorry for being flakey. This is looking good!
|
||
helpers.classStaticPrivateFieldLooseBase = () => template.program.ast` | ||
export default function _classStaticPrivateFieldLooseBase(receiver, classConstructor) { | ||
if (receiver !== classConstructor && receiver.constructor !== classConstructor) { |
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.
What's the receiver.constructor !== classConstructor
for?
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 for when the static field gets called from this
. In that case I'll probably have an instance of the class instead of the constructor itself. I do think that's what we expect from the spec but it's been a while since the last time I dived in the proposal.
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 has to throw a type error if it's called on an instance of the class. This check should just be the receiver !== classConstructor
.
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 not sure, I think that if the instance is of the class type, access should work through this
. I can check that again. However, in the meantime, we can restrict the access so it does not work on the instance while we figure that out.
// Create a private static "host" object if it does not exist | ||
privateClassId = path.scope.generateUidIdentifier(ref.name + "Statics"); | ||
staticNodesToAdd.push( | ||
template.statement`const PRIVATE_CLASS_ID = {};`({ |
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: Should probably use a prototype-less object here.
if (privateNames.has(name)) { | ||
throw path.buildCodeFrameError("Duplicate private field"); | ||
throw path.buildCodeFrameError("Duplicate static private field"); |
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.
Let's leave this as the old message.
state, | ||
privateClassId, | ||
); | ||
staticNodesToAdd.forEach(node => staticNodes.push(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.
Can just staticNodes.push(...staticNodesToAdd)
@jridgewell don't worry, we're in no rush. I'll just need some help setting up the tests correctly. |
e061a59
to
93e205a
Compare
I was trying to prevent the class transform itself but I managed to get that right in the plane |
It should be good for a second review |
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.
Looking good!
Need tests for loose
, and some that do a call operation on the private static.
const { scope, parentPath } = path; | ||
const { key, value } = path.node; | ||
const { name } = key.id; | ||
const privateId = scope.generateUidIdentifier(name); |
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: So we actually don't need to generate a unique id here, we can just use name
.
Loose tests have been removed while I changed my test methodology, they're back now. I also removed the privateId for spec mode but not loose mode (where the unique id prevents test breakage in loose mode) |
eb66be9
to
6dc2a28
Compare
Rebased the whole thing. Still need to test |
@jridgewell I think this covers all of the requested changes. Thanks! |
|
||
} | ||
|
||
Foo._foo = "foo"; |
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.
My last nit: This should be using Object.defineProperty
with { enumerable: false, configurable: false }
. We can do this in a follow PR if you'd like.
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 is a good point, I'll try to put that here before we merge
@jridgewell I made the change, would you like to have it in a separate helper though? |
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 is all good. Need one more approval before we can merge.
Eh, let's just go for it. |
I'm sorry, I forgot to review this PR 😅 |
classStaticPrivateFieldBase