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

Private Static Fields Features: Stage 3 #8205

Merged
merged 4 commits into from Sep 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/babel-helpers/src/helpers.js
Expand Up @@ -1041,3 +1041,35 @@ helpers.classPrivateFieldSet = helper("7.0.0-beta.0")`
return value;
}
`;

helpers.classStaticPrivateFieldLooseBase = helper("7.0.0-beta.0")`
export default function _classStaticPrivateFieldLooseBase(receiver, classConstructor) {
if (receiver !== classConstructor) {
throw new TypeError("Private static access of wrong provenance");
}
return classConstructor;
}
`;

helpers.classStaticPrivateFieldSpecGet = helper("7.0.0-beta.0")`
export default function _classStaticPrivateFieldSpecGet(
receiver, classConstructor, privateClass, privateId
) {
if (receiver !== classConstructor) {
throw new TypeError("Private static access of wrong provenance");
}
return privateClass[privateId];
}
`;

helpers.classStaticPrivateFieldSpecSet = helper("7.0.0-beta.0")`
export default function _classStaticPrivateFieldSpecSet(
receiver, classConstructor, privateClass, privateId, value
) {
if (receiver !== classConstructor) {
throw new TypeError("Private static access of wrong provenance");
}
privateClass[privateId] = value;
return value;
}
`;
160 changes: 151 additions & 9 deletions packages/babel-plugin-proposal-class-properties/src/index.js
Expand Up @@ -160,6 +160,60 @@ export default declare((api, options) => {
},
};

const staticPrivatePropertyHandlerSpec = {
...privateNameHandlerSpec,

get(member) {
const { file, name, privateClassId, classRef } = this;

return t.callExpression(
file.addHelper("classStaticPrivateFieldSpecGet"),
[
this.receiver(member),
classRef,
privateClassId,
t.stringLiteral(name),
],
);
},

set(member, value) {
const { file, name, privateClassId, classRef } = this;

return t.callExpression(
file.addHelper("classStaticPrivateFieldSpecSet"),
[
this.receiver(member),
classRef,
privateClassId,
t.stringLiteral(name),
value,
],
);
},

call(member, args) {
// The first access (the get) should do the memo assignment.
this.memoise(member, 1);

return optimiseCall(this.get(member), this.receiver(member), args);
},
};

const staticPrivatePropertyHandlerLoose = {
handle(member) {
const { file, privateId, classRef } = this;
member.replaceWith(
template.expression`BASE(RECEIVER, CLASS).PRIVATE_ID`({
BASE: file.addHelper("classStaticPrivateFieldLooseBase"),
RECEIVER: member.node.object,
CLASS: classRef,
PRIVATE_ID: privateId,
}),
);
},
};

function buildClassPropertySpec(ref, path, state) {
const { scope } = path;
const { key, value, computed } = path.node;
Expand Down Expand Up @@ -255,6 +309,80 @@ export default declare((api, options) => {
});
}

function buildClassStaticPrivatePropertySpec(
ref,
path,
state,
privateClassId,
) {
const { scope, parentPath } = path;
const { key, value } = path.node;
const { name } = key.id;
const staticNodesToAdd = [];

if (!privateClassId) {
// 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 = Object.create(null);`({
PRIVATE_CLASS_ID: privateClassId,
}),
);
}

memberExpressionToFunctions(parentPath, privateNameVisitor, {
name,
privateClassId,
classRef: ref,
file: state,
...staticPrivatePropertyHandlerSpec,
});

staticNodesToAdd.push(
t.expressionStatement(
t.callExpression(state.addHelper("defineProperty"), [
privateClassId,
t.stringLiteral(name),
value || scope.buildUndefinedNode(),
]),
),
);

return [staticNodesToAdd, privateClassId];
}

function buildClassStaticPrivatePropertyLoose(ref, path, state) {
const { scope, parentPath } = path;
const { key, value } = path.node;
const { name } = key.id;
const privateId = scope.generateUidIdentifier(name);

parentPath.traverse(privateNameVisitor, {
name,
privateId,
classRef: ref,
file: state,
...staticPrivatePropertyHandlerLoose,
});

const staticNodesToAdd = [
template.statement`
Object.defineProperty(OBJ, KEY, {
value: VALUE,
enumerable: false,
configurable: false,
writable: true
});
`({
OBJ: ref,
KEY: t.stringLiteral(privateId.name),
VALUE: value || scope.buildUndefinedNode(),
}),
];

return [staticNodesToAdd];
}

const buildClassProperty = loose
? buildClassPropertyLoose
: buildClassPropertySpec;
Expand All @@ -263,6 +391,10 @@ export default declare((api, options) => {
? buildClassPrivatePropertyLoose
: buildClassPrivatePropertySpec;

const buildClassStaticPrivateProperty = loose
? buildClassStaticPrivatePropertyLoose
: buildClassStaticPrivatePropertySpec;

return {
inherits: syntaxClassProperties,

Expand All @@ -288,17 +420,11 @@ export default declare((api, options) => {

if (path.isClassPrivateProperty()) {
const {
static: isStatic,
key: {
id: { name },
},
} = path.node;

if (isStatic) {
throw path.buildCodeFrameError(
"Static class fields are not spec'ed yet.",
);
}
if (privateNames.has(name)) {
throw path.buildCodeFrameError("Duplicate private field");
}
Expand Down Expand Up @@ -354,7 +480,7 @@ export default declare((api, options) => {
const privateMaps = [];
const privateMapInits = [];
for (const prop of props) {
if (prop.isPrivate()) {
if (prop.isPrivate() && !prop.node.static) {
const inits = [];
privateMapInits.push(inits);

Expand All @@ -363,11 +489,27 @@ export default declare((api, options) => {
);
}
}

let p = 0;
let privateClassId;
for (const prop of props) {
if (prop.node.static) {
staticNodes.push(buildClassProperty(t.cloneNode(ref), prop, state));
if (prop.isPrivate()) {
let staticNodesToAdd;
[
staticNodesToAdd,
privateClassId,
] = buildClassStaticPrivateProperty(
t.cloneNode(ref),
prop,
state,
privateClassId,
);
staticNodes.push(...staticNodesToAdd);
} else {
staticNodes.push(
buildClassProperty(t.cloneNode(ref), prop, state),
);
}
} else if (prop.isPrivate()) {
instanceBody.push(privateMaps[p]());
staticNodes.push(...privateMapInits[p]);
Expand Down
@@ -1,3 +1,3 @@
{
"throws": "Static class fields are not spec'ed yet."
"plugins": ["external-helpers", ["proposal-class-properties", { "loose": true }], "transform-block-scoping", "syntax-class-properties"]
}
@@ -0,0 +1,32 @@
class Foo {
constructor() {
Object.defineProperty(this, _bar, {
writable: true,
value: "bar"
});
}

static test() {
return babelHelpers.classStaticPrivateFieldLooseBase(Foo, Foo)._foo;
}

test() {
return babelHelpers.classPrivateFieldLooseBase(this, _bar)[_bar];
}

}

Object.defineProperty(Foo, "_foo", {
value: "foo",
enumerable: false,
configurable: false,
writable: true
});

var _bar = babelHelpers.classPrivateFieldLooseKey("bar");

var f = new Foo();
expect("foo" in Foo).toBe(false);
expect("bar" in f).toBe(false);
expect(Foo.test()).toBe("foo");
expect(f.test()).toBe("bar");
@@ -1,3 +1,3 @@
{
"throws": "Static class fields are not spec'ed yet."
"plugins": ["external-helpers", ["proposal-class-properties", { "loose": true }], "transform-block-scoping", "syntax-class-properties"]
}
@@ -0,0 +1,18 @@
export default (param => {
var _class, _temp;

return _temp = _class = class App {
getParam() {
return param;
}

}, Object.defineProperty(_class, "_props", {
value: {
prop1: 'prop1',
prop2: 'prop2'
},
enumerable: false,
configurable: false,
writable: true
}), _temp;
});
@@ -1,3 +1,3 @@
{
"plugins": ["external-helpers",["proposal-class-properties", { "loose": true }], "transform-classes", "transform-block-scoping", "syntax-class-properties"]
"plugins": ["external-helpers", ["proposal-class-properties", { "loose": true }], "transform-classes", "transform-block-scoping", "syntax-class-properties"]
}
@@ -1,3 +1,3 @@
{
"throws": "Static class fields are not spec'ed yet."
"plugins": ["external-helpers", ["proposal-class-properties", { "loose": true }], "transform-block-scoping", "syntax-class-properties"]
}
@@ -0,0 +1,51 @@
function classFactory() {
var _class, _temp, _foo;

return _temp = _class = class Foo {
constructor() {
Object.defineProperty(this, _foo, {
writable: true,
value: "foo"
});
}

instance() {
return babelHelpers.classPrivateFieldLooseBase(this, _foo)[_foo];
}

static() {
return babelHelpers.classStaticPrivateFieldLooseBase(Foo, _class)._bar;
}

static instance(inst) {
return babelHelpers.classPrivateFieldLooseBase(inst, _foo)[_foo];
}

static static() {
return babelHelpers.classStaticPrivateFieldLooseBase(Foo, _class)._bar;
}

}, _foo = babelHelpers.classPrivateFieldLooseKey("foo"), Object.defineProperty(_class, "_bar", {
value: "bar",
enumerable: false,
configurable: false,
writable: true
}), _temp;
}

var Foo1 = classFactory();
var Foo2 = classFactory();
var f1 = new Foo1();
var f2 = new Foo2();
expect(f1.instance()).toBe("foo");
expect(f1.static()).toBe("bar");
expect(f2.instance()).toBe("foo");
expect(f2.static()).toBe("bar");
expect(Foo1.instance(f1)).toBe("foo");
expect(Foo1.static()).toBe("bar");
expect(Foo2.instance(f2)).toBe("foo");
expect(Foo2.static()).toBe("bar");
assert.throws(() => f1.instance.call(f2));
assert.throws(() => f2.instance.call(f1));
assert.throws(() => Foo1.instance(f2));
assert.throws(() => Foo2.instance(f1));
@@ -0,0 +1,13 @@
class Foo {
static #foo = function(x) {
return x;
}

test(x) {
return Foo.#foo(x);
}
}

const f = new Foo;
const test = f.test();
expect(f.test("bar")).toBe("bar");
@@ -0,0 +1,10 @@
class Foo {
static #foo = function(x) {
return x;
}

test(x) {
return Foo.#foo(x);
}
}