diff --git a/acorn-loose/src/statement.js b/acorn-loose/src/statement.js index d55ce0477..cdc535226 100644 --- a/acorn-loose/src/statement.js +++ b/acorn-loose/src/statement.js @@ -281,77 +281,127 @@ lp.parseClass = function(isStatement) { this.eat(tt.braceL) if (this.curIndent + 1 < indent) { indent = this.curIndent; line = this.curLineStart } while (!this.closes(tt.braceR, indent, line)) { - if (this.semicolon()) continue - let element = this.startNode(), isGenerator, isAsync, kind = "method" - if (this.options.ecmaVersion >= 6) { - element.static = false - isGenerator = this.eat(tt.star) - } - this.parseClassElementName(element) - if (isDummy(element.key)) { if (isDummy(this.parseMaybeAssign())) this.next(); this.eat(tt.comma); continue } - if (element.key.type === "Identifier" && !element.computed && element.key.name === "static" && - this.tok.type !== tt.parenL && this.tok.type !== tt.braceL && this.tok.type !== tt.eq && - this.tok.type !== tt.semi) { - element.static = true - isGenerator = this.eat(tt.star) - this.parseClassElementName(element) + const element = this.parseClassElement() + if (element) node.body.body.push(element) + } + this.popCx() + if (!this.eat(tt.braceR)) { + // If there is no closing brace, make the node span to the start + // of the next token (this is useful for Tern) + this.last.end = this.tok.start + if (this.options.locations) this.last.loc.end = this.tok.loc.start + } + this.semicolon() + this.finishNode(node.body, "ClassBody") + return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") +} + +lp.parseClassElement = function() { + if (this.eat(tt.semi)) return null + + const {ecmaVersion, locations} = this.options + const indent = this.curIndent + const line = this.curLineStart + const node = this.startNode() + let keyName = "" + let isGenerator = false + let isAsync = false + let kind = "method" + + // Parse modifiers + node.static = false + if (this.eatContextual("static")) { + if (this.isClassElementNameStart() || this.toks.type === tt.star) { + node.static = true } else { - element.static = false + keyName = "static" } - if (!element.computed && - element.key.type === "Identifier" && element.key.name === "async" && this.tok.type !== tt.parenL && - this.tok.type !== tt.eq && this.tok.type !== tt.semi && !this.canInsertSemicolon()) { + } + if (!keyName && ecmaVersion >= 8 && this.eatContextual("async")) { + if ((this.isClassElementNameStart() || this.toks.type === tt.star) && !this.canInsertSemicolon()) { isAsync = true - isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star) - this.parseClassElementName(element) } else { - isAsync = false + keyName = "async" } - if (this.options.ecmaVersion >= 5 && element.key.type === "Identifier" && - !element.computed && (element.key.name === "get" || element.key.name === "set") && - this.tok.type !== tt.parenL && this.tok.type !== tt.braceL && this.tok.type !== tt.eq && - this.tok.type !== tt.semi) { - kind = element.key.name - this.parseClassElementName(element) - } else if (!element.computed && !element.static && !isGenerator && !isAsync && - (element.key.type === "Identifier" && element.key.name === "constructor" || - element.key.type === "Literal" && element.key.value === "constructor")) { - kind = "constructor" + } + if (!keyName) { + isGenerator = this.eat(tt.star) + const lastValue = this.toks.value + if (this.eatContextual("get") || this.eatContextual("set")) { + if (this.isClassElementNameStart()) { + kind = lastValue + } else { + keyName = lastValue + } } - if (this.options.ecmaVersion < 13 || this.tok.type === tt.parenL || kind !== "method" || isGenerator || isAsync) { - element.kind = kind - element.value = this.parseMethod(isGenerator, isAsync) - this.finishNode(element, "MethodDefinition") - } else { - if (this.eat(tt.eq)) { - const oldInAsync = this.inAsync, oldInGenerator = this.inGenerator + } + + // Parse element name + if (keyName) { + // 'async', 'get', 'set', or 'static' were not a keyword contextually. + // The last token is any of those. Make it the element name. + node.computed = false + node.key = this.startNodeAt(locations ? [this.toks.lastTokStart, this.toks.lastTokStartLoc] : this.toks.lastTokStart) + node.key.name = keyName + this.finishNode(node.key, "Identifier") + } else { + this.parseClassElementName(node) + + // From https://github.com/acornjs/acorn/blob/7deba41118d6384a2c498c61176b3cf434f69590/acorn-loose/src/statement.js#L291 + // Skip broken stuff. + if (isDummy(node.key)) { + if (isDummy(this.parseMaybeAssign())) this.next() + this.eat(tt.comma) + return null + } + } + + // Parse element value + if (ecmaVersion < 13 || this.toks.type === tt.parenL || kind !== "method" || isGenerator || isAsync) { + // Method + const isConstructor = + !node.computed && + !node.static && + !isGenerator && + !isAsync && + kind === "method" && ( + node.key.type === "Identifier" && node.key.name === "constructor" || + node.key.type === "Literal" && node.key.value === "constructor" + ) + node.kind = isConstructor ? "constructor" : kind + node.value = this.parseMethod(isGenerator, isAsync) + this.finishNode(node, "MethodDefinition") + } else { + // Field + if (this.eat(tt.eq)) { + if (this.curLineStart !== line && this.curIndent <= indent && this.tokenStartsLine()) { + // Estimated the next line is the next class element by indentations. + node.value = null + } else { + const oldInAsync = this.inAsync + const oldInGenerator = this.inGenerator this.inAsync = false this.inGenerator = false - element.value = this.parseMaybeAssign() + node.value = this.parseMaybeAssign() this.inAsync = oldInAsync this.inGenerator = oldInGenerator - } else { - element.value = null } - this.semicolon() - this.finishNode(element, "PropertyDefinition") + } else { + node.value = null } - node.body.body.push(element) - } - this.popCx() - if (!this.eat(tt.braceR)) { - // If there is no closing brace, make the node span to the start - // of the next token (this is useful for Tern) - this.last.end = this.tok.start - if (this.options.locations) this.last.loc.end = this.tok.loc.start + this.semicolon() + this.finishNode(node, "PropertyDefinition") } - this.semicolon() - this.finishNode(node.body, "ClassBody") - return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression") + + return node +} + +lp.isClassElementNameStart = function() { + return this.toks.isClassElementNameStart() } lp.parseClassElementName = function(element) { - if (this.tok.type === tt.privateId) { + if (this.toks.type === tt.privateId) { element.computed = false element.key = this.parsePrivateIdent() } else { diff --git a/acorn/src/statement.js b/acorn/src/statement.js index 062056100..9b33a7907 100644 --- a/acorn/src/statement.js +++ b/acorn/src/statement.js @@ -601,46 +601,57 @@ pp.parseClass = function(node, isStatement) { pp.parseClassElement = function(constructorAllowsSuper) { if (this.eat(tt.semi)) return null - let node = this.startNode() - - const tryContextual = (k, noLineBreak = false) => { - const start = this.start, startLoc = this.startLoc - if (node.key || !this.eatContextual(k)) return false - if ( - (this.type === tt.name || - this.type === tt.privateId || - this.type === tt.num || - this.type === tt.string || - this.type === tt.bracketL || - this.type === tt.star) && - !(noLineBreak && this.canInsertSemicolon()) - ) { - return true + const ecmaVersion = this.options.ecmaVersion + const node = this.startNode() + let keyName = "" + let isGenerator = false + let isAsync = false + let kind = "method" + + // Parse modifiers + node.static = false + if (this.eatContextual("static")) { + if (this.isClassElementNameStart() || this.type === tt.star) { + node.static = true + } else { + keyName = "static" } - node.computed = false - node.key = this.startNodeAt(start, startLoc) - node.key.name = k - this.finishNode(node.key, "Identifier") - return false } - - node.static = tryContextual("static") - let kind = "method" - let isGenerator = this.eat(tt.star) - let isAsync = false - if (!isGenerator) { - if (this.options.ecmaVersion >= 8 && tryContextual("async", true)) { + if (!keyName && ecmaVersion >= 8 && this.eatContextual("async")) { + if ((this.isClassElementNameStart() || this.type === tt.star) && !this.canInsertSemicolon()) { isAsync = true - isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star) - } else if (tryContextual("get")) { - kind = "get" - } else if (tryContextual("set")) { - kind = "set" + } else { + keyName = "async" } } - if (!node.key) this.parseClassElementName(node) + if (!keyName && (ecmaVersion >= 9 || !isAsync) && this.eat(tt.star)) { + isGenerator = true + } + if (!keyName && !isAsync && !isGenerator) { + const lastValue = this.value + if (this.eatContextual("get") || this.eatContextual("set")) { + if (this.isClassElementNameStart()) { + kind = lastValue + } else { + keyName = lastValue + } + } + } + + // Parse element name + if (keyName) { + // 'async', 'get', 'set', or 'static' were not a keyword contextually. + // The last token is any of those. Make it the element name. + node.computed = false + node.key = this.startNodeAt(this.lastTokStart, this.lastTokStartLoc) + node.key.name = keyName + this.finishNode(node.key, "Identifier") + } else { + this.parseClassElementName(node) + } - if (this.options.ecmaVersion < 13 || this.type === tt.parenL || kind !== "method" || isGenerator || isAsync) { + // Parse element value + if (ecmaVersion < 13 || this.type === tt.parenL || kind !== "method" || isGenerator || isAsync) { const isConstructor = !node.static && checkKeyName(node, "constructor") const allowsDirectSuper = isConstructor && constructorAllowsSuper // Couldn't move this check into the 'parseClassMethod' method for backward compatibility. @@ -654,6 +665,16 @@ pp.parseClassElement = function(constructorAllowsSuper) { return node } +pp.isClassElementNameStart = function() { + return ( + this.type === tt.name || + this.type === tt.privateId || + this.type === tt.num || + this.type === tt.string || + this.type === tt.bracketL + ) +} + pp.parseClassElementName = function(element) { if (this.type === tt.privateId) { if (this.value === "constructor") { diff --git a/test/tests-class-features-2022.js b/test/tests-class-features-2022.js index b488cb07d..c61614f04 100644 --- a/test/tests-class-features-2022.js +++ b/test/tests-class-features-2022.js @@ -1250,6 +1250,150 @@ test("class C { async\n get(){} }", { "sourceType": "script" }, {ecmaVersion: 13, loose: true}) +// ASI; star after get/set +test("class C { get\n *foo(){} }", { + "type": "Program", + "start": 0, + "end": 25, + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 25, + "id": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "C" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 8, + "end": 25, + "body": [ + { + "type": "PropertyDefinition", + "start": 10, + "end": 13, + "static": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 10, + "end": 13, + "name": "get" + }, + "value": null + }, + { + "type": "MethodDefinition", + "start": 15, + "end": 23, + "static": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 16, + "end": 19, + "name": "foo" + }, + "kind": "method", + "value": { + "type": "FunctionExpression", + "start": 19, + "end": 23, + "id": null, + "expression": false, + "generator": true, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 21, + "end": 23, + "body": [] + } + } + } + ] + } + } + ], + "sourceType": "script" +}, {ecmaVersion: 13}) +test("class C { set\n *foo(){} }", { + "type": "Program", + "start": 0, + "end": 25, + "body": [ + { + "type": "ClassDeclaration", + "start": 0, + "end": 25, + "id": { + "type": "Identifier", + "start": 6, + "end": 7, + "name": "C" + }, + "superClass": null, + "body": { + "type": "ClassBody", + "start": 8, + "end": 25, + "body": [ + { + "type": "PropertyDefinition", + "start": 10, + "end": 13, + "static": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 10, + "end": 13, + "name": "set" + }, + "value": null + }, + { + "type": "MethodDefinition", + "start": 15, + "end": 23, + "static": false, + "computed": false, + "key": { + "type": "Identifier", + "start": 16, + "end": 19, + "name": "foo" + }, + "kind": "method", + "value": { + "type": "FunctionExpression", + "start": 19, + "end": 23, + "id": null, + "expression": false, + "generator": true, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 21, + "end": 23, + "body": [] + } + } + } + ] + } + } + ], + "sourceType": "script" +}, {ecmaVersion: 13}) + // `await` is reference test("async function f() { class C { aaa = await } }", { "type": "Program",