Skip to content
This repository has been archived by the owner on May 19, 2018. It is now read-only.

Commit

Permalink
Add support for class private methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Qantas94Heavy committed Aug 29, 2017
1 parent 3f1dbec commit c367d80
Show file tree
Hide file tree
Showing 27 changed files with 2,490 additions and 56 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ require("babylon").parse("code", {
| `decorators` (Stage 1) and `decorators2` (Stage 2 [proposal](https://github.com/tc39/proposal-decorators)) | `@a class A {}` |
| `classProperties` ([proposal](https://github.com/tc39/proposal-class-public-fields)) | `class A { b = 1; }` |
| `classPrivateProperties` ([proposal](https://github.com/tc39/proposal-private-fields)) | `class A { #b = 1; }` |
| `classPrivateMethods` ([proposal](https://github.com/littledan/proposal-private-methods)) | `class A { #c() {} }` |
| `exportExtensions` ([proposal 1](https://github.com/leebyron/ecmascript-export-default-from)), ([proposal 2](https://github.com/leebyron/ecmascript-export-ns-from)) | Proposal 1: `export v from "mod"` Proposal 2: `export * as ns from "mod"` |
| `asyncGenerators` ([proposal](https://github.com/tc39/proposal-async-iteration)) | `async function*() {}`, `for await (let a of b) {}` |
| `functionBind` ([proposal](https://github.com/zenparsing/es-function-bind)) | `a::b`, `::console.log` |
Expand Down
16 changes: 15 additions & 1 deletion ast/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ These are the core Babylon AST node types.
- [Classes](#classes)
- [ClassBody](#classbody)
- [ClassMethod](#classmethod)
- [ClassPrivateMethod](#classprivatemethod)
- [ClassProperty](#classproperty)
- [ClassPrivateProperty](#classprivateproperty)
- [ClassDeclaration](#classdeclaration)
Expand Down Expand Up @@ -1032,7 +1033,7 @@ interface Class <: Node {
```js
interface ClassBody <: Node {
type: "ClassBody";
body: [ ClassMethod | ClassProperty | ClassPrivateProperty ];
body: [ ClassMethod | ClassPrivateMethod | ClassProperty | ClassPrivateProperty ];
}
```

Expand All @@ -1049,6 +1050,19 @@ interface ClassMethod <: Function {
}
```

## ClassMethod

```js
interface ClassPrivateMethod <: Function {
type: "ClassPrivateMethod";
key: Identifier;
kind: "method" | "get" | "set";
computed: boolean;
static: boolean;
decorators: [ Decorator ];
}
```

## ClassProperty

```js
Expand Down
13 changes: 13 additions & 0 deletions src/parser/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,19 @@ export default class ExpressionParser extends LValParser {
return prop.key;
}

// FIXME: does this make sense separate?
parseClassPrivateName(
prop: N.ClassPrivateProperty | N.ClassPrivateMethod,
): N.Expression {
prop.computed = false;
const oldInPropertyName = this.state.inPropertyName;
this.state.inPropertyName = true;
prop.key = this.parseIdentifier(true);
this.state.inPropertyName = oldInPropertyName;

return prop.key;
}

// Initialize empty function node.

initFunction(node: N.BodilessFunctionOrMethodBase, isAsync: ?boolean): void {
Expand Down
200 changes: 150 additions & 50 deletions src/parser/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -955,17 +955,6 @@ export default class StatementParser extends ExpressionParser {
isStatic = true;
}

if (this.match(tt.hash)) {
// Private property
this.expectPlugin("classPrivateProperties");
this.next();
const privateProp: N.ClassPrivateProperty = memberAny;
privateProp.key = this.parseIdentifier(true);
privateProp.static = isStatic;
classBody.body.push(this.parsePrivateClassProperty(privateProp));
return;
}

this.parseClassMemberWithIsStatic(classBody, member, state, isStatic);
}

Expand All @@ -976,16 +965,37 @@ export default class StatementParser extends ExpressionParser {
isStatic: boolean,
) {
const memberAny: any = member;
const methodOrProp: N.ClassMethod | N.ClassProperty = memberAny;
const method: N.ClassMethod = memberAny;
const prop: N.ClassProperty = memberAny;
const methodOrProp:
| N.ClassMethod
| N.ClassPrivateMethod
| N.ClassProperty
| N.ClassPrivateProperty = memberAny;
const method: N.ClassMethod | N.ClassPrivateMethod = memberAny;
const prop: N.ClassProperty | N.ClassPrivateProperty = memberAny;

methodOrProp.static = isStatic;

if (this.eat(tt.star)) {
// a generator
method.kind = "method";

if (this.match(tt.hash)) {
this.expectPlugin("classPrivateMethods");
this.next();
// Private generator method
this.parseClassPrivateName(method);
this.parseClassPrivateMethod(
classBody,
method,
true,
false,
/* isConstructor */ false,
);
return;
}

this.parsePropertyName(method);

if (this.isNonstaticConstructor(method)) {
this.raise(method.key.start, "Constructor can't be a generator");
}
Expand All @@ -1009,12 +1019,36 @@ export default class StatementParser extends ExpressionParser {
return;
}

const isPrivate = this.eat(tt.hash);

if (isPrivate) {
this.expectOnePlugin(["classPrivateProperties", "classPrivateMethods"]);
}

const isSimple = this.match(tt.name);
const key = this.parseClassPropertyName(methodOrProp);
const key = isPrivate
? this.parseClassPrivateName(methodOrProp)
: this.parseClassPropertyName(methodOrProp);

this.parsePostMemberNameModifiers(methodOrProp);

if (this.isClassMethod()) {
if (isPrivate) {
// TODO: seems a bit inconsistent error throwing: in some cases we've already
// eaten the #, but in other cases we haven't. How to solve?
this.expectPlugin("classPrivateMethods");
// private "normal" method
method.kind = "method";
this.parseClassPrivateMethod(
classBody,
method,
false,
false,
isConstructor,
);
return;
}

// a normal method
const isConstructor = this.isNonstaticConstructor(method);
if (isConstructor) {
Expand All @@ -1040,7 +1074,13 @@ export default class StatementParser extends ExpressionParser {

this.parseClassMethod(classBody, method, false, false, isConstructor);
} else if (this.isClassProperty()) {
this.pushClassProperty(classBody, prop);
if (isPrivate) {
this.expectPlugin("classPrivateProperties");
// Private property
classBody.body.push(this.parseClassPrivateProperty(prop));
} else {
this.pushClassProperty(classBody, prop);
}
} else if (isSimple && key.name === "async" && !this.isLineTerminator()) {
// an async method
let isGenerator = false;
Expand All @@ -1050,17 +1090,37 @@ export default class StatementParser extends ExpressionParser {
isGenerator = true;
}
method.kind = "method";
this.parsePropertyName(method);
if (this.isNonstaticConstructor(method)) {
this.raise(method.key.start, "Constructor can't be an async function");

if (this.match(tt.hash)) {
// private async method
this.expectPlugin("classPrivateMethods");
// The private # wouldn't have been eaten as the "async" was in front of it.
this.next();
// The so-called parsed name would have been "async": get the real name.
this.parseClassPrivateName(method);
this.parseClassPrivateMethod(
classBody,
method,
isGenerator,
true,
/* isConstructor */ false,
);
} else {
this.parsePropertyName(method);
if (this.isNonstaticConstructor(method)) {
this.raise(
method.key.start,
"Constructor can't be an async function",
);
}
this.parseClassMethod(
classBody,
method,
isGenerator,
true,
/* isConstructor */ false,
);
}
this.parseClassMethod(
classBody,
method,
isGenerator,
true,
/* isConstructor */ false,
);
} else if (
isSimple &&
(key.name === "get" || key.name === "set") &&
Expand All @@ -1069,27 +1129,52 @@ export default class StatementParser extends ExpressionParser {
// `get\n*` is an uninitialized property named 'get' followed by a generator.
// a getter or setter
method.kind = key.name;
this.parsePropertyName(method);
if (this.isNonstaticConstructor(method)) {
this.raise(method.key.start, "Constructor can't have get/set modifier");
if (this.match(tt.hash)) {
// private getter/setter
this.expectPlugin("classPrivateMethods");
// The private # wouldn't have been eaten as the "async" was in front of it.
this.next();
// The so-called parsed name would have been "async": get the real name.
this.parseClassPrivateName(method);
this.parseClassPrivateMethod(
classBody,
method,
false,
false,
/* isConstructor */ false,
);
} else {
this.parsePropertyName(method);
if (this.isNonstaticConstructor(method)) {
this.raise(
method.key.start,
"Constructor can't have get/set modifier",
);
}
this.parseClassMethod(
classBody,
method,
false,
false,
/* isConstructor */ false,
);
this.checkGetterSetterParamCount(method);
}
this.parseClassMethod(
classBody,
method,
false,
false,
/* isConstructor */ false,
);
this.checkGetterSetterParamCount(method);
} else if (this.isLineTerminator()) {
// an uninitialized class property (due to ASI, since we don't otherwise recognize the next token)
if (this.isNonstaticConstructor(prop)) {
this.raise(
prop.key.start,
"Classes may not have a non-static field named 'constructor'",
);
if (isPrivate) {
this.expectPlugin("classPrivateProperties");
// Private property
classBody.body.push(this.parseClassPrivateProperty(prop));
} else {
if (this.isNonstaticConstructor(prop)) {
this.raise(
prop.key.start,
"Classes may not have a non-static field named 'constructor'",
);
}
classBody.body.push(this.parseClassProperty(prop));
}
classBody.body.push(this.parseClassProperty(prop));
} else {
this.unexpected();
}
Expand Down Expand Up @@ -1134,17 +1219,14 @@ export default class StatementParser extends ExpressionParser {
return undefined;
}

parsePrivateClassProperty(
parseClassPrivateProperty(
node: N.ClassPrivateProperty,
): N.ClassPrivateProperty {
this.state.inClassProperty = true;

if (this.match(tt.eq)) {
this.next();
node.value = this.parseMaybeAssign();
} else {
node.value = null;
}
if (node.computed !== undefined) node.computed = undefined;
node.value = this.eat(tt.eq) ? this.parseMaybeAssign() : null;

this.semicolon();
this.state.inClassProperty = false;
return this.finishNode(node, "ClassPrivateProperty");
Expand Down Expand Up @@ -1188,6 +1270,24 @@ export default class StatementParser extends ExpressionParser {
);
}

parseClassPrivateMethod(
classBody: N.ClassBody,
method: N.ClassMethod,
isGenerator: boolean,
isAsync: boolean,
isConstructor: boolean,
): void {
classBody.body.push(
this.parseMethod(
method,
isGenerator,
isAsync,
isConstructor,
"ClassPrivateMethod",
),
);
}

parseClassId(
node: N.Class,
isStatement: boolean,
Expand Down
3 changes: 2 additions & 1 deletion src/tokenizer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,8 @@ export default class Tokenizer extends LocationParser {
switch (code) {
case 35: // '#'
if (
this.hasPlugin("classPrivateProperties") &&
(this.hasPlugin("classPrivateProperties") ||
this.hasPlugin("classPrivateMethods")) &&
this.state.classLevel > 0
) {
++this.state.pos;
Expand Down
9 changes: 9 additions & 0 deletions src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ export type ClassMemberBase = NodeBase &

export type ClassMember =
| ClassMethod
| ClassPrivateMethod
| ClassProperty
| ClassPrivateProperty
| TsIndexSignature;
Expand All @@ -643,6 +644,7 @@ export type MethodLike =
| ObjectMethod
| FunctionExpression
| ClassMethod
| ClassPrivateMethod
| TSDeclareMethod;

export type MethodBase = FunctionBase & {
Expand All @@ -664,6 +666,13 @@ export type ClassMethod = MethodBase &
variance?: ?FlowVariance, // TODO: Not in spec
};

export type ClassPrivateMethod = NodeBase &
ClassMethodOrDeclareMethodCommon & {
type: "ClassPrivateMethod",
key: Identifier,
static: false,
};

export type ClassProperty = ClassMemberBase & {
type: "ClassProperty",
key: Expression,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Foo {
async* #readLines(path) {
let file = await fileOpen(path);

try {
while (!file.EOF) {
yield await file.readLine();
}
} finally {
await file.close();
}
}
}
Loading

0 comments on commit c367d80

Please sign in to comment.