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

Fix parsing of class properties #351

Merged
merged 1 commit into from
Mar 10, 2017
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 97 additions & 80 deletions src/parser/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,11 +625,18 @@ pp.parseClass = function (node, isStatement, optionalId) {
};

pp.isClassProperty = function () {
return this.match(tt.eq) || this.isLineTerminator();
return this.match(tt.eq) || this.match(tt.semi) || this.match(tt.braceR);
};

pp.isClassMutatorStarter = function () {
return false;
pp.isClassMethod = function () {
return this.match(tt.parenL);
};

pp.isNonstaticConstructor = function (method) {
return !method.computed && !method.static && (
(method.key.name === "constructor") || // Identifier
(method.key.value === "constructor") // Literal
);
};

pp.parseClassBody = function (node) {
Expand Down Expand Up @@ -664,92 +671,102 @@ pp.parseClassBody = function (node) {
decorators = [];
}

let isConstructorCall = false;
const isMaybeStatic = this.match(tt.name) && this.state.value === "static";
let isGenerator = this.eat(tt.star);
let isGetSet = false;
let isAsync = false;

this.parsePropertyName(method);

method.static = isMaybeStatic && !this.match(tt.parenL);
if (method.static) {
isGenerator = this.eat(tt.star);
this.parsePropertyName(method);
}

if (!isGenerator) {
if (this.isClassProperty()) {
method.static = false;
if (this.match(tt.name) && this.state.value === "static") {
const key = this.parseIdentifier(true); // eats 'static'
if (this.isClassMethod()) {
// a method named 'static'
method.kind = "method";
method.computed = false;
method.key = key;
this.parseClassMethod(classBody, method, false, false);
continue;
} else if (this.isClassProperty()) {
// a property named 'static'
method.computed = false;
method.key = key;
classBody.body.push(this.parseClassProperty(method));
continue;
}

if (method.key.type === "Identifier" && !method.computed && this.hasPlugin("classConstructorCall") && method.key.name === "call" && this.match(tt.name) && this.state.value === "constructor") {
isConstructorCall = true;
this.parsePropertyName(method);
}
// otherwise something static
method.static = true;
}

const isAsyncMethod = !this.match(tt.parenL) && !method.computed && method.key.type === "Identifier" && method.key.name === "async";
if (isAsyncMethod) {
if (this.hasPlugin("asyncGenerators") && this.eat(tt.star)) isGenerator = true;
isAsync = true;
if (this.eat(tt.star)) {
// a generator
method.kind = "method";
this.parsePropertyName(method);
}

method.kind = "method";

if (!method.computed) {
let { key } = method;

// handle get/set methods
// eg. class Foo { get bar() {} set bar() {} }
if (!isAsync && !isGenerator && !this.isClassMutatorStarter() && key.type === "Identifier" && !this.match(tt.parenL) && (key.name === "get" || key.name === "set")) {
isGetSet = true;
method.kind = key.name;
key = this.parsePropertyName(method);
if (this.isNonstaticConstructor(method)) {
this.raise(method.key.start, "Constructor can't be a generator");
}

// disallow invalid constructors
const isConstructor = !isConstructorCall && !method.static && (
(key.name === "constructor") || // Identifier
(key.value === "constructor") // Literal
);
if (isConstructor) {
if (hadConstructor) this.raise(key.start, "Duplicate constructor in the same class");
if (isGetSet) this.raise(key.start, "Constructor can't have get/set modifier");
if (isGenerator) this.raise(key.start, "Constructor can't be a generator");
if (isAsync) this.raise(key.start, "Constructor can't be an async function");
method.kind = "constructor";
hadConstructor = true;
if (!method.computed && method.static && (method.key.name === "prototype" || method.key.value === "prototype")) {
this.raise(method.key.start, "Classes may not have static property named prototype");
}

// disallow static prototype method
const isStaticPrototype = method.static && (
(key.name === "prototype") || // Identifier
(key.value === "prototype") // Literal
);
if (isStaticPrototype) {
this.raise(key.start, "Classes may not have static property named prototype");
this.parseClassMethod(classBody, method, true, false);
} else {
const isSimple = this.match(tt.name);
const key = this.parsePropertyName(method);
if (!method.computed && method.static && (method.key.name === "prototype" || method.key.value === "prototype")) {
this.raise(method.key.start, "Classes may not have static property named prototype");
}
if (this.isClassMethod()) {
// a normal method
if (this.isNonstaticConstructor(method)) {
if (hadConstructor) {
this.raise(key.start, "Duplicate constructor in the same class");
} else if (method.decorators) {
this.raise(method.start, "You can't attach decorators to a class constructor");
}
hadConstructor = true;
method.kind = "constructor";
} else {
method.kind = "method";
}
this.parseClassMethod(classBody, method, false, false);
} else if (this.isClassProperty()) {
// a normal property
if (this.isNonstaticConstructor(method)) {
this.raise(method.key.start, "Classes may not have a non-static field named 'constructor'");
}
classBody.body.push(this.parseClassProperty(method));
} else if (isSimple && key.name === "async" && !this.isLineTerminator()) {
// an async method
const isGenerator = this.hasPlugin("asyncGenerators") && this.eat(tt.star);
method.kind = "method";
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);
} else if (isSimple && (key.name === "get" || key.name === "set") && !(this.isLineTerminator() && this.match(tt.star))) { // `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");
}
this.parseClassMethod(classBody, method, false, false);
this.checkGetterSetterParamCount(method);
} else if (this.hasPlugin("classConstructorCall") && isSimple && key.name === "call" && this.match(tt.name) && this.state.value === "constructor") {
// a (deprecated) call constructor
if (hadConstructorCall) {
this.raise(method.start, "Duplicate constructor call in the same class");
} else if (method.decorators) {
this.raise(method.start, "You can't attach decorators to a class constructor");
}
hadConstructorCall = true;
method.kind = "constructorCall";
this.parsePropertyName(method); // consume "constructor" and make it the method's name
this.parseClassMethod(classBody, method, false, false);
} else if (this.isLineTerminator()) {
// an uninitialized class property (due to ASI, since we don't otherwise recognize the next token)
if (this.isNonstaticConstructor(method)) {
this.raise(method.key.start, "Classes may not have a non-static field named 'constructor'");
}
classBody.body.push(this.parseClassProperty(method));
} else {
this.unexpected();
}
}

// convert constructor to a constructor call
if (isConstructorCall) {
if (hadConstructorCall) this.raise(method.start, "Duplicate constructor call in the same class");
method.kind = "constructorCall";
hadConstructorCall = true;
}

// disallow decorators on class constructors
if ((method.kind === "constructor" || method.kind === "constructorCall") && method.decorators) {
this.raise(method.start, "You can't attach decorators to a class constructor");
}

this.parseClassMethod(classBody, method, isGenerator, isAsync);

if (isGetSet) {
this.checkGetterSetterParamCount(method);
}
}

Expand Down
17 changes: 7 additions & 10 deletions src/plugins/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,13 @@ export default function (instance) {
};
});

// determine whether or not we're currently in the position where a class method would appear
instance.extend("isClassMethod", function (inner) {
return function () {
return this.isRelational("<") || inner.call(this);
};
});

// determine whether or not we're currently in the position where a class property would appear
instance.extend("isClassProperty", function (inner) {
return function () {
Expand Down Expand Up @@ -1450,14 +1457,4 @@ export default function (instance) {
return this.match(tt.colon) || inner.call(this);
};
});

instance.extend("isClassMutatorStarter", function (inner) {
return function () {
if (this.isRelational("<")) {
return true;
} else {
return inner.call(this);
}
};
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class A {
static *prototype() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"throws": "Classes may not have static property named prototype (2:10)"
}
42 changes: 42 additions & 0 deletions test/fixtures/es2015/class-methods/linebreaks/actual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
class A {
get
a
() {}

set
a
(a) {}

constructor
() {}

a
() {}

*
a
() {}

static
get
a
() {}

static
set
a
(a) {}

static
constructor
() {}

static
a
() {}

static
*
a
() {}
}
Loading