Skip to content

Commit

Permalink
JS: add support for class field definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Mar 1, 2021
1 parent 961096a commit 7ec26ab
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 16 deletions.
24 changes: 21 additions & 3 deletions js/ast.go
Expand Up @@ -888,11 +888,26 @@ func (n MethodDecl) String() string {
return "Method(" + s[1:] + ")"
}

// FieldDefinition is a field definition in a class declaration.
type FieldDefinition struct {
Name PropertyName
Init IExpr
}

func (n FieldDefinition) String() string {
s := "Definition(" + n.Name.String()
if n.Init != nil {
s += " = " + n.Init.String()
}
return s + ")"
}

// ClassDecl is a class declaration.
type ClassDecl struct {
Name *Var // can be nil
Extends IExpr // can be nil
Methods []MethodDecl
Name *Var // can be nil
Extends IExpr // can be nil
Definitions []FieldDefinition
Methods []MethodDecl
}

func (n ClassDecl) String() string {
Expand All @@ -903,6 +918,9 @@ func (n ClassDecl) String() string {
if n.Extends != nil {
s += " extends " + n.Extends.String()
}
for _, item := range n.Definitions {
s += " " + item.String()
}
for _, item := range n.Methods {
s += " " + item.String()
}
Expand Down
23 changes: 14 additions & 9 deletions js/lex.go
Expand Up @@ -185,13 +185,21 @@ func (l *Lexer) Next() (TokenType, []byte) {
case '`':
l.templateLevels = append(l.templateLevels, l.level)
return l.consumeTemplateToken(), l.r.Shift()
case '#':
l.r.Move(1)
if l.consumeIdentifierToken() {
return PrivateIdentifierToken, l.r.Shift()
}
return ErrorToken, nil
default:
if tt := l.consumeIdentifierToken(); tt != ErrorToken {
if l.consumeIdentifierToken() {
if prevNumericLiteral {
l.err = parse.NewErrorLexer(l.r, "unexpected identifier after number")
return ErrorToken, nil
} else if keyword, ok := Keywords[string(l.r.Lexeme())]; ok {
return keyword, l.r.Shift()
}
return tt, l.r.Shift()
return IdentifierToken, l.r.Shift()
}
if 0xC0 <= c {
if l.consumeWhitespace() {
Expand Down Expand Up @@ -477,18 +485,18 @@ func (l *Lexer) consumeOperatorToken() TokenType {
return opTokens[c]
}

func (l *Lexer) consumeIdentifierToken() TokenType {
func (l *Lexer) consumeIdentifierToken() bool {
c := l.r.Peek(0)
if identifierStartTable[c] {
l.r.Move(1)
} else if 0xC0 <= c {
if r, n := l.r.PeekRune(0); unicode.IsOneOf(identifierStart, r) {
l.r.Move(n)
} else {
return ErrorToken
return false
}
} else if !l.consumeUnicodeEscape() {
return ErrorToken
return false
}
for {
c := l.r.Peek(0)
Expand All @@ -504,10 +512,7 @@ func (l *Lexer) consumeIdentifierToken() TokenType {
break
}
}
if keyword, ok := Keywords[string(l.r.Lexeme())]; ok {
return keyword
}
return IdentifierToken
return true
}

func (l *Lexer) consumeNumericToken() TokenType {
Expand Down
42 changes: 38 additions & 4 deletions js/parse.go
Expand Up @@ -931,12 +931,18 @@ func (p *Parser) parseAnyClass(inExpr bool) (classDecl ClassDecl) {
p.next()
break
}
classDecl.Methods = append(classDecl.Methods, p.parseMethod())

method, definition := p.parseClassElement()
if method.Name.IsSet() {
classDecl.Methods = append(classDecl.Methods, method)
} else {
classDecl.Definitions = append(classDecl.Definitions, definition)
}
}
return
}

func (p *Parser) parseMethod() (method MethodDecl) {
func (p *Parser) parseClassElement() (method MethodDecl, definition FieldDefinition) {
var data []byte
if p.tt == StaticToken {
method.Static = true
Expand Down Expand Up @@ -967,6 +973,7 @@ func (p *Parser) parseMethod() (method MethodDecl) {
p.next()
}

isFieldDefinition := false
if data != nil && p.tt == OpenParenToken {
method.Name.Literal = LiteralExpr{IdentifierToken, data}
if method.Async || method.Get || method.Set {
Expand All @@ -976,8 +983,29 @@ func (p *Parser) parseMethod() (method MethodDecl) {
} else {
method.Static = false
}
} else if data != nil && (p.tt == EqToken || p.tt == SemicolonToken || p.tt == CloseBraceToken) {
method.Name.Literal = LiteralExpr{IdentifierToken, data}
isFieldDefinition = true
} else if data == nil && p.tt == PrivateIdentifierToken {
method.Name.Literal = LiteralExpr{p.tt, p.data}
p.next()
isFieldDefinition = true
} else {
method.Name = p.parsePropertyName("method definition")
if data == nil && p.tt != OpenParenToken {
isFieldDefinition = true
}
}

if isFieldDefinition {
// FieldDefinition
definition.Name = method.Name
if p.tt == EqToken {
p.next()
definition.Init = p.parseExpression(OpAssign)
}
method = MethodDecl{}
return
}

parent := p.enterScope(&method.Body.Scope, true)
Expand Down Expand Up @@ -1755,15 +1783,18 @@ func (p *Parser) parseExpressionSuffix(left IExpr, prec, precLeft OpPrec) IExpr
return nil
}
p.next()
if !IsIdentifierName(p.tt) {
if !IsIdentifierName(p.tt) && p.tt != PrivateIdentifierToken {
p.fail("dot expression", IdentifierToken)
return nil
}
exprPrec := OpMember
if precLeft < OpMember {
exprPrec = OpCall
}
left = &DotExpr{left, LiteralExpr{IdentifierToken, p.data}, exprPrec}
if p.tt != PrivateIdentifierToken {
p.tt = IdentifierToken
}
left = &DotExpr{left, LiteralExpr{p.tt, p.data}, exprPrec}
p.next()
if precLeft < OpMember {
precLeft = OpCall
Expand Down Expand Up @@ -1841,6 +1872,9 @@ func (p *Parser) parseExpressionSuffix(left IExpr, prec, precLeft OpPrec) IExpr
} else if IsIdentifierName(p.tt) {
left = &OptChainExpr{left, &LiteralExpr{IdentifierToken, p.data}}
p.next()
} else if p.tt == PrivateIdentifierToken {
left = &OptChainExpr{left, &LiteralExpr{p.tt, p.data}}
p.next()
} else {
p.fail("optional chaining expression", IdentifierToken, OpenParenToken, OpenBracketToken, TemplateToken)
return nil
Expand Down
7 changes: 7 additions & 0 deletions js/parse_test.go
Expand Up @@ -106,6 +106,13 @@ func TestParse(t *testing.T) {
{"class A { static() {} }", "Decl(class A Method(static Params() Stmt({ })))"},
{"class A { static a(b) {} }", "Decl(class A Method(static a Params(Binding(b)) Stmt({ })))"},
{"class A { [5](b) {} }", "Decl(class A Method([5] Params(Binding(b)) Stmt({ })))"},
{"class A { field }", "Decl(class A Definition(field))"},
{"class A { #field }", "Decl(class A Definition(#field))"},
{"class A { field=5 }", "Decl(class A Definition(field = 5))"},
{"class A { #field=5 }", "Decl(class A Definition(#field = 5))"},
{"class A { get }", "Decl(class A Definition(get))"},
{"class A { field static get method(){} }", "Decl(class A Definition(field) Method(static get method Params() Stmt({ })))"},
//{"class A { get get get(){} }", "Decl(class A Definition(get) Method(get get Params() Stmt({ })))"}, // doesn't look like this should be supported
{"`tmpl`", "Stmt(`tmpl`)"},
{"`tmpl${x}`", "Stmt(`tmpl${x}`)"},
{"`tmpl${x}tmpl${x}`", "Stmt(`tmpl${x}tmpl${x}`)"},
Expand Down
3 changes: 3 additions & 0 deletions js/tokentype.go
Expand Up @@ -18,6 +18,7 @@ const (
TemplateMiddleToken
TemplateEndToken
RegExpToken
PrivateIdentifierToken
)

// Numeric token values.
Expand Down Expand Up @@ -356,6 +357,8 @@ func (tt TokenType) Bytes() []byte {
return []byte("TemplateEnd")
case RegExpToken:
return []byte("RegExp")
case PrivateIdentifierToken:
return []byte("PrivateIdentifier")
case NumericToken:
return []byte("Numeric")
case DecimalToken:
Expand Down

0 comments on commit 7ec26ab

Please sign in to comment.