Skip to content

Commit

Permalink
update acorn
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea authored and marijnh committed Apr 24, 2021
1 parent 1d2ac27 commit 4c33507
Show file tree
Hide file tree
Showing 10 changed files with 3,401 additions and 50 deletions.
35 changes: 33 additions & 2 deletions acorn/src/expression.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ pp.parseMaybeUnary = function(refDestructuringErrors, sawUnary) {
else if (this.strict && node.operator === "delete" &&
node.argument.type === "Identifier")
this.raiseRecoverable(node.start, "Deleting local variable in strict mode")
else if (node.operator === "delete" && node.argument.type === "MemberExpression" &&
node.argument.property.type === "PrivateIdentifier")
this.raiseRecoverable(node.start, "Private fields can not be deleted")
else sawUnary = true
expr = this.finishNode(node, update ? "UpdateExpression" : "UnaryExpression")
} else {
Expand Down Expand Up @@ -307,9 +310,15 @@ pp.parseSubscript = function(base, startPos, startLoc, noCalls, maybeAsyncArrow,
if (computed || (optional && this.type !== tt.parenL && this.type !== tt.backQuote) || this.eat(tt.dot)) {
let node = this.startNodeAt(startPos, startLoc)
node.object = base
node.property = computed ? this.parseExpression() : this.parseIdent(this.options.allowReserved !== "never")
if (computed) {
node.property = this.parseExpression()
this.expect(tt.bracketR)
} else if (this.type === tt.privateId && base.type !== "Super") {
node.property = this.parsePrivateIdent()
} else {
node.property = this.parseIdent(this.options.allowReserved !== "never")
}
node.computed = !!computed
if (computed) this.expect(tt.bracketR)
if (optionalSupported) {
node.optional = optional
}
Expand Down Expand Up @@ -964,6 +973,8 @@ pp.checkUnreserved = function({start, end, name}) {
this.raiseRecoverable(start, "Cannot use 'yield' as identifier inside a generator")
if (this.inAsync && name === "await")
this.raiseRecoverable(start, "Cannot use 'await' as identifier inside an async function")
if (this.currentThisScope().inClassFieldInit && name === "arguments")
this.raiseRecoverable(start, "Cannot use 'arguments' in class field initializer")
if (this.keywords.test(name))
this.raise(start, `Unexpected keyword '${name}'`)
if (this.options.ecmaVersion < 6 &&
Expand Down Expand Up @@ -1008,6 +1019,26 @@ pp.parseIdent = function(liberal, isBinding) {
return node
}

pp.parsePrivateIdent = function() {
const node = this.startNode()
if (this.type === tt.privateId) {
node.name = this.value
} else {
this.unexpected()
}
this.next()
this.finishNode(node, "PrivateIdentifier")

// For validating existence
if (this.privateNameStack.length === 0) {
this.raise(node.start, `Private field '#${node.name}' must be declared in an enclosing class`)
} else {
this.privateNameStack[this.privateNameStack.length - 1].used.push(node)
}

return node
}

// Parses yield expression inside generator.

pp.parseYield = function(noIn) {
Expand Down
2 changes: 2 additions & 0 deletions acorn/src/scope.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class Scope {
this.lexical = []
// A list of lexically-declared FunctionDeclaration names in the current lexical scope
this.functions = []
// A switch to disallow the identifier reference 'arguments'
this.inClassFieldInit = false
}
}

Expand Down
10 changes: 9 additions & 1 deletion acorn/src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export class Parser {

// For RegExp validation
this.regexpState = null

// The stack of private names.
// Each element has two properties: 'declared' and 'used'.
// When it exited from the outermost class definition, all used private names must be declared.
this.privateNameStack = []
}

parse() {
Expand All @@ -94,7 +99,10 @@ export class Parser {
get inFunction() { return (this.currentVarScope().flags & SCOPE_FUNCTION) > 0 }
get inGenerator() { return (this.currentVarScope().flags & SCOPE_GENERATOR) > 0 }
get inAsync() { return (this.currentVarScope().flags & SCOPE_ASYNC) > 0 }
get allowSuper() { return (this.currentThisScope().flags & SCOPE_SUPER) > 0 }
get allowSuper() {
const {flags, inClassFieldInit} = this.currentThisScope()
return (flags & SCOPE_SUPER) > 0 || inClassFieldInit
}
get allowDirectSuper() { return (this.currentThisScope().flags & SCOPE_DIRECT_SUPER) > 0 }
get treatFunctionsAsVar() { return this.treatFunctionsAsVarInScope(this.currentScope()) }
get inNonArrowFunction() { return (this.currentThisScope().flags & SCOPE_FUNCTION) > 0 }
Expand Down
182 changes: 149 additions & 33 deletions acorn/src/statement.js
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,8 @@ pp.parseClass = function(node, isStatement) {

this.parseClassId(node, isStatement)
this.parseClassSuper(node)
let classBody = this.startNode()
const privateNameMap = this.enterClassBody()
const classBody = this.startNode()
let hadConstructor = false
classBody.body = []
this.expect(tt.braceL)
Expand All @@ -585,73 +586,132 @@ pp.parseClass = function(node, isStatement) {
if (element.type === "MethodDefinition" && element.kind === "constructor") {
if (hadConstructor) this.raise(element.start, "Duplicate constructor in the same class")
hadConstructor = true
} else if (element.key.type === "PrivateIdentifier" && isPrivateNameConflicted(privateNameMap, element)) {
this.raiseRecoverable(element.key.start, `Identifier '#${element.key.name}' has already been declared`)
}
}
}
this.strict = oldStrict
this.next()
node.body = this.finishNode(classBody, "ClassBody")
this.exitClassBody()
return this.finishNode(node, isStatement ? "ClassDeclaration" : "ClassExpression")
}

pp.parseClassElement = function(constructorAllowsSuper) {
if (this.eat(tt.semi)) return null

let method = this.startNode()
let node = this.startNode()

const tryContextual = (k, noLineBreak = false) => {
const start = this.start, startLoc = this.startLoc
if (!this.eatContextual(k)) return false
if (this.type !== tt.parenL && (!noLineBreak || !this.canInsertSemicolon())) return true
if (method.key) this.unexpected()
method.computed = false
method.key = this.startNodeAt(start, startLoc)
method.key.name = k
this.finishNode(method.key, "Identifier")
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
}
node.computed = false
node.key = this.startNodeAt(start, startLoc)
node.key.name = k
this.finishNode(node.key, "Identifier")
return false
}

method.kind = "method"
method.static = tryContextual("static")
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)) {
isAsync = true
isGenerator = this.options.ecmaVersion >= 9 && this.eat(tt.star)
} else if (tryContextual("get")) {
method.kind = "get"
kind = "get"
} else if (tryContextual("set")) {
method.kind = "set"
kind = "set"
}
}
if (!node.key) this.parseClassElementName(node)

if (this.options.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.
if (isConstructor && kind !== "method") this.raise(node.key.start, "Constructor can't have get/set modifier")
node.kind = isConstructor ? "constructor" : kind
this.parseClassMethod(node, isGenerator, isAsync, allowsDirectSuper)
} else {
this.parseClassField(node)
}

return node
}

pp.parseClassElementName = function(element) {
if (this.type === tt.privateId) {
if (this.value === "constructor") {
this.raise(this.start, "Classes can't have an element named '#constructor'")
}
element.computed = false
element.key = this.parsePrivateIdent()
} else {
this.parsePropertyName(element)
}
if (!method.key) this.parsePropertyName(method)
let {key} = method
let allowsDirectSuper = false
if (!method.computed && !method.static && (key.type === "Identifier" && key.name === "constructor" ||
key.type === "Literal" && key.value === "constructor")) {
if (method.kind !== "method") this.raise(key.start, "Constructor can't have get/set modifier")
}

pp.parseClassMethod = function(method, isGenerator, isAsync, allowsDirectSuper) {
// Check key and flags
const key = method.key
if (method.kind === "constructor") {
if (isGenerator) this.raise(key.start, "Constructor can't be a generator")
if (isAsync) this.raise(key.start, "Constructor can't be an async method")
method.kind = "constructor"
allowsDirectSuper = constructorAllowsSuper
} else if (method.static && key.type === "Identifier" && key.name === "prototype") {
} else if (method.static && checkKeyName(method, "prototype")) {
this.raise(key.start, "Classes may not have a static property named prototype")
}
this.parseClassMethod(method, isGenerator, isAsync, allowsDirectSuper)
if (method.kind === "get" && method.value.params.length !== 0)
this.raiseRecoverable(method.value.start, "getter should have no params")
if (method.kind === "set" && method.value.params.length !== 1)
this.raiseRecoverable(method.value.start, "setter should have exactly one param")
if (method.kind === "set" && method.value.params[0].type === "RestElement")
this.raiseRecoverable(method.value.params[0].start, "Setter cannot use rest params")
return method
}

pp.parseClassMethod = function(method, isGenerator, isAsync, allowsDirectSuper) {
method.value = this.parseMethod(isGenerator, isAsync, allowsDirectSuper)
// Parse value
const value = method.value = this.parseMethod(isGenerator, isAsync, allowsDirectSuper)

// Check value
if (method.kind === "get" && value.params.length !== 0)
this.raiseRecoverable(value.start, "getter should have no params")
if (method.kind === "set" && value.params.length !== 1)
this.raiseRecoverable(value.start, "setter should have exactly one param")
if (method.kind === "set" && value.params[0].type === "RestElement")
this.raiseRecoverable(value.params[0].start, "Setter cannot use rest params")

return this.finishNode(method, "MethodDefinition")
}

pp.parseClassField = function(field) {
if (checkKeyName(field, "constructor")) {
this.raise(field.key.start, "Classes can't have a field named 'constructor'")
} else if (field.static && checkKeyName(field, "prototype")) {
this.raise(field.key.start, "Classes can't have a static field named 'prototype'")
}

if (this.eat(tt.eq)) {
// To raise SyntaxError if 'arguments' exists in the initializer.
const scope = this.currentThisScope()
const inClassFieldInit = scope.inClassFieldInit
scope.inClassFieldInit = true
field.value = this.parseMaybeAssign()
scope.inClassFieldInit = inClassFieldInit
} else {
field.value = null
}
this.semicolon()

return this.finishNode(field, "PropertyDefinition")
}

pp.parseClassId = function(node, isStatement) {
if (this.type === tt.name) {
node.id = this.parseIdent()
Expand All @@ -668,6 +728,62 @@ pp.parseClassSuper = function(node) {
node.superClass = this.eat(tt._extends) ? this.parseExprSubscripts() : null
}

pp.enterClassBody = function() {
const element = {declared: Object.create(null), used: []}
this.privateNameStack.push(element)
return element.declared
}

pp.exitClassBody = function() {
const {declared, used} = this.privateNameStack.pop()
const len = this.privateNameStack.length
const parent = len === 0 ? null : this.privateNameStack[len - 1]
for (let i = 0; i < used.length; ++i) {
const id = used[i]
if (!has(declared, id.name)) {
if (parent) {
parent.used.push(id)
} else {
this.raiseRecoverable(id.start, `Private field '#${id.name}' must be declared in an enclosing class`)
}
}
}
}

function isPrivateNameConflicted(privateNameMap, element) {
const name = element.key.name
const curr = privateNameMap[name]

let next = "true"
if (element.type === "MethodDefinition" && (element.kind === "get" || element.kind === "set")) {
next = (element.static ? "s" : "i") + element.kind
}

// `class { get #a(){}; static set #a(_){} }` is also conflict.
if (
curr === "iget" && next === "iset" ||
curr === "iset" && next === "iget" ||
curr === "sget" && next === "sset" ||
curr === "sset" && next === "sget"
) {
privateNameMap[name] = "true"
return false
} else if (!curr) {
privateNameMap[name] = next
return false
} else {
return true
}
}

function checkKeyName(node, name) {
const {computed, key} = node
return !computed && (
key.type === "Identifier" && key.name === name ||
key.type === "Literal" && key.value === name
)
}

// Parses module export declaration.

pp.parseExport = function(node, exports) {
Expand Down
17 changes: 17 additions & 0 deletions acorn/src/tokenize.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,20 @@ pp.readToken_question = function() { // '?'
return this.finishOp(tt.question, 1)
}

pp.readToken_numberSign = function() { // '#'
const ecmaVersion = this.options.ecmaVersion
let code = "#"
if (ecmaVersion >= 13) {
++this.pos
code = this.fullCharCodeAtPos()
if (isIdentifierStart(code, true) || code === 92 /* '\' */) {
return this.finishToken(tt.privateId, this.readWord1())
}
}

this.raise(this.pos, "Unexpected character '" + codePointToString(code) + "'")
}

pp.getTokenFromCode = function(code) {
switch (code) {
// The interpretation of a dot depends on whether it is followed
Expand Down Expand Up @@ -385,6 +399,9 @@ pp.getTokenFromCode = function(code) {

case 126: // '~'
return this.finishOp(tt.prefix, 1)

case 35: // '#'
return this.readToken_numberSign()
}

this.raise(this.pos, "Unexpected character '" + codePointToString(code) + "'")
Expand Down
1 change: 1 addition & 0 deletions acorn/src/tokentype.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const types = {
regexp: new TokenType("regexp", startsExpr),
string: new TokenType("string", startsExpr),
name: new TokenType("name", startsExpr),
privateId: new TokenType("privateId", startsExpr),
eof: new TokenType("eof"),

// Punctuation token types.
Expand Down
11 changes: 3 additions & 8 deletions bin/run_test262.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,13 @@ const run = require("test262-parser-runner")
const parse = require("../acorn").parse

const unsupportedFeatures = [
"class-fields-private",
"class-fields-public",
"class-methods-private",
"class-static-fields-private",
"class-static-fields-public",
"class-static-methods-private",
"regexp-match-indices",
"arbitrary-module-namespace-names"
"arbitrary-module-namespace-names",
"class-fields-private-in"
];

run(
(content, {sourceType}) => parse(content, {sourceType, ecmaVersion: 12, allowHashBang: true, allowAwaitOutsideFunction: sourceType === "module"}),
(content, {sourceType}) => parse(content, {sourceType, ecmaVersion: 13, allowHashBang: true, allowAwaitOutsideFunction: sourceType === "module"}),
{
testsDirectory: path.dirname(require.resolve("test262/package.json")),
skip: test => (test.attrs.features && unsupportedFeatures.some(f => test.attrs.features.includes(f))),
Expand Down
Loading

0 comments on commit 4c33507

Please sign in to comment.