Skip to content

Conversation

dragomirtitian
Copy link
Contributor

@dragomirtitian dragomirtitian commented Feb 26, 2021

Static Private Class Elements

This PR implements the TC39 Stage 3 Static Class features which introduce private identifier static class elements (fields/methods/accessors).

Fixes: #42985

Dependencies

This PR depends on the existing PR for Private Methods & Accessors #42458

Open issues

  • Wait for the dependent PR to get to master.
  • PR the finalized helpers to tslib
  • Validate limitations below

Downlevel

class A { static #field = 10 static #method() : void {} // no error static get #roProp() { return 0; } static set #woProp(value: number) { } static get #prop() { return 0 } static set #prop(value: number) { } static test() { console.log(A.#field); A.#field = 10; A.#method(); console.log(A.#roProp); A.#roProp = 10 console.log(A.#woProp); A.#woProp = 10 console.log(A.#prop); A.#prop = 10 } }

JavaScript - ES2020 emit

Helper functions
var __classStaticPrivateFieldGet = (this && this.__classStaticPrivateFieldGet) || function (receiver, classConstructor, propertyDescriptor) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } if (propertyDescriptor === undefined) { throw new TypeError("Private static field was accessed before its declaration."); } return propertyDescriptor.value; }; var __classStaticPrivateFieldSet = (this && this.__classStaticPrivateFieldSet) || function (receiver, classConstructor, propertyDescriptor, value) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } if (propertyDescriptor === undefined) { throw new TypeError("Private static field was accessed before its declaration."); } propertyDescriptor.value = value; return value; }; var __classStaticPrivateMethodGet = (this && this.__classStaticPrivateMethodGet) || function (receiver, classConstructor, fn) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } return fn; }; var __classStaticPrivateAccessorGet = (this && this.__classStaticPrivateAccessorGet) || function (receiver, classConstructor, fn) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } return fn.call(receiver); }; var __classStaticPrivateReadonly = (this && this.__classStaticPrivateReadonly) || function () { throw new TypeError("Private static element is not writable"); }; var __classStaticPrivateWriteonly = (this && this.__classStaticPrivateWriteonly) || function () { throw new TypeError("Private static element is not readable"); }; var __classStaticPrivateAccessorSet = (this && this.__classStaticPrivateAccessorSet) || function (receiver, classConstructor, fn, value) { if (receiver !== classConstructor) { throw new TypeError("Private static access of wrong provenance"); } fn.call(receiver, value); return value; };
var _A_field, _A_method, _A_roProp_get, _A_woProp_set, _A_prop_get, _A_prop_set; class A { static test() { console.log(__classStaticPrivateFieldGet(A, A, _A_field)); __classStaticPrivateFieldSet(A, A, _A_field, 10); __classStaticPrivateMethodGet(A, A, _A_method).call(A); console.log(__classStaticPrivateAccessorGet(A, A, _A_roProp_get)); __classStaticPrivateReadonly(A, 10); console.log(__classStaticPrivateWriteonly(A)); __classStaticPrivateAccessorSet(A, A, _A_woProp_set, 10); console.log(__classStaticPrivateAccessorGet(A, A, _A_prop_get)); __classStaticPrivateAccessorSet(A, A, _A_prop_set, 10); } } _A_method = function _A_method() { }, _A_roProp_get = function _A_roProp_get() { return 0; }, _A_woProp_set = function _A_woProp_set(value) { }, _A_prop_get = function _A_prop_get() { return 0; }, _A_prop_set = function _A_prop_set(value) { }; _A_field = { value: 10 };

References

Wider Contributions

The following Babel issues were discovered and reported during this work:

Credits

This PR includes contributions from the following Bloomberg engineers:

Design Limitations

  1. This implementation does not work with the experimental Decorators in TypeScript. There is no spec for the interaction between these two features and implementing something non-standard that is likely to break in the future does not seem useful. Therefore we issue an error if a Decorators is used on a class containing a static private field/method/accessor.
Example
function dec() { return function (cls: new (...a: any) => any) { return class extends cls { static someField = 11; constructor(...a: any) { super(...a); } } } } @dec() class Foo { static someField = 10; static #somePrivateField = 10; static m() { // displays 11 because decorator redefined it // and all references to Foo were rewritten to point to  // whatever the decorator returned Foo.someField // Right now this would always fail, since Foo will be rewritten // to point to whatever dec returned, so it will not point to the  // class that actually defined `#somePrivateField` Foo.#somePrivateField } }
  1. Initializers are not allowed if target: esnext AND useDefineForClassFields: false. This is due to the fact that initializing private fields outside of a class is a non-trivial transform (a possible solution is described here - a modified version of it could be applied if there is desire for this combination to allow it) and keeping the init in the class would change runtime ordering of initializers.

  2. this is not allow in static initialization expressions. The restriction is a pre-existing issue and so is considered out-of-scope of this PR.

  3. Unlike instance #private class elements, static #private class elements do not depend on WeakMap or WeakSet. This means that technically we could transpile static #private class elements for es5 or even es3. However having different requirements for instance vs static #private class elements would probably cause more confusion than benefit. Therefore we retain the existing minimum target of es2015 for using static #private class elements and will error otherwise.

dragomirtitian and others added 30 commits February 3, 2021 18:44
…ing except classes. Changes objects literal checking to not bail on first private name found in object literal.
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Private methods inside class expressions should not error. Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
…y assignment Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
Signed-off-by: Kubilay Kahveci <kahvecikubilay@gmail.com>
"category": "Error",
"code": 2804
},
"Static fields with private names can't have initializers when the '--useDefineForClassFields' flag is not specified with a '--target' of 'esnext'. Consider adding the '--useDefineForClassFields' flag.": {
Copy link
Member

Choose a reason for hiding this comment

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

Ugh we need to workshop this one a bit more.

Comment on lines +90 to +103
class A_Field_Field {
#foo = "foo";
#foo = "foo";
~~~~
!!! error TS2300: Duplicate identifier '#foo'.
}

// Error
class A_Field_Method {
#foo = "foo";
~~~~
!!! error TS2300: Duplicate identifier '#foo'.
#foo() { }
~~~~
Copy link
Member

Choose a reason for hiding this comment

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

It's surprising that there's any inconsistency here. Any idea why that's happening?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am unsure of the exact reasons. Some duplication errors come from the binder some from the checker. I think this has something to do with overloads. The effect is that if the duplication is field-method the error comes from the binder (it also has info about the other class element). While duplication for method-field comes from the checker (and does not have related info about the other class element).

This is the reason I massively expanded the duplication tests, as since the errors can come from two different places and order matters, I wanted to make sure I don't miss any corner cases.

I think the duplicate detection code might be due for a revamp. It was quite confusing to have the error to come from different places if the order differs. I tried to just make the changes needed for private static-instance duplication duplication, as such a revamp was not really in scope for this issue.

return !isOptionalChain(node) && !!findAncestor(node, parent => parent === declClass);
}
return checkPropertyAccessibility(node, isSuper, type, prop);
return checkPropertyAccessibility(node, isSuper, type, prop, /* reportError */ false);
Copy link
Member

Choose a reason for hiding this comment

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

So what exactly are we trying to suppress here? It's unclear why this is needed.

Copy link
Contributor Author

@dragomirtitian dragomirtitian Mar 3, 2021

Choose a reason for hiding this comment

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

I added a check in getSuggestedSymbolForNonexistentProperty so that it only suggests accessible members. To do this I use isValidPropertyAccessForCompletions which in turn uses checkPropertyAccessibility.

props = filter(props, prop => isValidPropertyAccessForCompletions(parent, containingType, prop));

The problem is that since checkPropertyAccessibility reports errors, when the compiler tries to get the suggestion, it will report access errors on all tested properties in that filter call. Suppressing the error seems to cause no other issues. isValidPropertyAccessForCompletions isn't named with a check prefix like other methods that report errors, so it is a bit strange it would report errors.

I could remove this improvement and allow invalid suggestions but I think this is a net improvement.

@dragomirtitian
Copy link
Contributor Author

@DanielRosenwasser

This PR is dependent on the PR for private methods and accessors. So some of the commits here are also found in that PR (#42458)

Since the PR for instance methods and accessors is not yet merged this causes some issues when reviewing this PR. Some of the issues in the code review are in the code for private methods and accessors, while others come from static class elements.

Would you prefer that I merge them so that we only have one PR? (retaining the features as separate commits)

Initially we thought it might be easier to keep them separate for ease of review, but we can change the description of this PR and have a single unified PR.

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Mar 2, 2021

@dragomirtitian You might want to mark this PR as a draft until #42458 is merged.

@sandersn sandersn requested review from armanio123 and rbuckton March 9, 2021 01:45
@typescript-bot typescript-bot added For Milestone Bug PRs that fix a bug with a specific milestone and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Mar 9, 2021
@rbuckton rbuckton mentioned this pull request Mar 16, 2021
3 tasks
Copy link
Contributor

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

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

I still have more to review (I've already listed some feedback on #42458), but again I think we could condense these 14 helpers down into the existing two:

var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateState, value, kind, f) { if (typeof privateState === "function" ? receiver !== privateState : !privateState.has(receiver)) { throw new TypeError("Private state access of wrong provenance"); } if (kind === "method") throw new TypeError("Attempted to set private method"); if (kind === "accessor") { if (!f) throw new TypeError("Private element is not writable"); f.call(receiver, value); } else if (typeof privateState === "function") { if (!f) throw new TypeError("Private static field was accessed before its declaration"); f.value = value; } else { privateState.set(receiver, value); } return value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateState, kind, f) { if (typeof privateState === "function" ? receiver !== privateState : !privateState.has(receiver)) { throw new TypeError("Private state access of wrong provenance"); } if (kind === "method") return f; if (kind === "accessor") { if (!f) throw new TypeError("Private element is not readable"); return f.call(receiver); } if (typeof privateState === "function") { if (!f) throw new TypeError("Private static field was accessed before its declaration"); return f.value; } return privateState.get(receiver); };

I'd like to get @DanielRosenwasser's opinion on the helper count vs helper size though.

Comment on lines +37 to +49
createClassPrivateMethodGetHelper(receiver: Expression, instances: Identifier, fn: Identifier): Expression;
createClassPrivateReadonlyHelper(receiver: Expression, value: Expression): Expression;
createClassPrivateWriteonlyHelper(receiver: Expression): Expression;
createClassPrivateAccessorGetHelper(receiver: Expression, instances: Identifier, fn: Identifier): Expression;
createClassPrivateAccessorSetHelper(receiver: Expression, instances: Identifier, fn: Identifier, value: Expression): Expression;
// Class Static Private Helpers
createClassStaticPrivateFieldGetHelper(receiver: Expression, classConstructor: Identifier, privateField: Identifier): Expression;
createClassStaticPrivateFieldSetHelper(receiver: Expression, classConstructor: Identifier, privateField: Identifier, value: Expression): Expression;
createClassStaticPrivateMethodGetHelper(receiver: Expression, classConstructor: Identifier, fn: Identifier): Expression;
createClassStaticPrivateReadonlyHelper(receiver: Expression, value: Expression): Expression;
createClassStaticPrivateWriteonlyHelper(receiver: Expression): Expression;
createClassStaticPrivateAccessorGetHelper(receiver: Expression, classConstructor: Identifier, fn: Identifier): Expression;
createClassStaticPrivateAccessorSetHelper(receiver: Expression, classConstructor: Identifier, fn: Identifier, value: Expression): Expression;
Copy link
Contributor

Choose a reason for hiding this comment

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

This adds a large number of emit helpers that are extremely similar. Perhaps we could condense them down? In #42458 I suggested extending the existing __classPrivateFieldGet and __classPrivateFieldSet helpers in a backwards-compatible fashion. While that suggestion does make those helper functions larger than they are currently, if you were to write a file that exercised every single one of these helpers you could end up with significantly larger emit.

@rbuckton
Copy link
Contributor

rbuckton commented Mar 16, 2021

If we're willing to give up the expressiveness of the runtime assertions in cases where we already report compile-time errors, then the two-helper emit can be even further reduced:

var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (typeof state === "function" ? receiver !== state : !state.has(receiver)) throw new TypeError("Private state access of wrong provenance"); if (kind === "method") throw new TypeError("Attempted to set private method"); if (kind === "accessor") f.call(receiver, value); else if (typeof state === "function") f.value = value; else state.set(receiver, value); return value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (typeof state === "function" ? receiver !== state : !state.has(receiver)) throw new TypeError("Private state access of wrong provenance"); return kind === "method" ? f : kind === "accessor" ? f.call(receiver) : typeof state === "function" ? f.value : state.get(receiver); };

The above still errors at runtime in the same places (since f would be undefined), even though the error would be more cryptic. We can even further reduce the size of the __classPrivateFieldSet if we don't mind emitting somewhat harder to read code:

var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (typeof state === "function" ? receiver !== state : !state.has(receiver)) throw new TypeError("Private state access of wrong provenance"); if (kind === "method") throw new TypeError("Attempted to set private method"); return kind === "accessor" ? (f.call(receiver, value), value) : typeof state === "function" ? f.value = value : (state.set(receiver, value), value); }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (typeof state === "function" ? receiver !== state : !state.has(receiver)) throw new TypeError("Private state access of wrong provenance"); return kind === "method" ? f : kind === "accessor" ? f.call(receiver) : typeof state === "function" ? f.value : state.get(receiver); };

Which is 9 lines of code as opposed to 14 separate helpers.

@rbuckton
Copy link
Contributor

rbuckton commented Mar 16, 2021

I've further reduced the overall size of my proposal for a unified __classPrivateFieldSet and __classPrivateFieldGet:

var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "method") throw new TypeError("Private method is not writable"); if (kind === "accessor" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "accessor" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "accessor" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "method" ? f : kind === "accessor" ? f.call(receiver) : f ? f.value : state.get(receiver); };

I've written up a full breakdown of these helpers here: https://gist.github.com/rbuckton/02e3e9f13e4405184a1e4cfe2da69634

@mkubilayk
Copy link
Contributor

Thank you very much for the review @rbuckton! Since you have more inline comments in #42485, it might be easier to continue on that. We'll move static elements to that PR and close this one if that's OK.

@dragomirtitian
Copy link
Contributor Author

Closing the PR in favor of #42458 (which now contains the static private class members)

@dragomirtitian dragomirtitian deleted the es-private-static-fields-methods-and-accessors branch November 17, 2023 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Milestone Bug PRs that fix a bug with a specific milestone

8 participants