refactor(common): 重构 Conn 实体并优化地图进入逻辑

- 优化 Conn 实体的 SendPack 方法,提高代码复用性
- 添加 goja 模块到 go.work 文件
- 重构地图进入逻辑,增加玩家广播和刷怪功能
- 调整 OutInfo 结构中的 Vip 和 Viped 字段类型
- 简化 MonsterRefresh 结构体定义
This commit is contained in:
2025-08-18 00:38:14 +08:00
parent 9a6587a2da
commit 10eed9418c
142 changed files with 77533 additions and 17 deletions

View File

@@ -0,0 +1,184 @@
# parser
--
import "github.com/dop251/goja/parser"
Package parser implements a parser for JavaScript. Borrowed from https://github.com/robertkrimen/otto/tree/master/parser
import (
"github.com/dop251/goja/parser"
)
Parse and return an AST
filename := "" // A filename is optional
src := `
// Sample xyzzy example
(function(){
if (3.14159 > 0) {
console.log("Hello, World.");
return;
}
var xyzzy = NaN;
console.log("Nothing happens.");
return xyzzy;
})();
`
// Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
program, err := parser.ParseFile(nil, filename, src, 0)
### Warning
The parser and AST interfaces are still works-in-progress (particularly where
node types are concerned) and may change in the future.
## Usage
#### func ParseFile
```go
func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode) (*ast.Program, error)
```
ParseFile parses the source code of a single JavaScript/ECMAScript source file
and returns the corresponding ast.Program node.
If fileSet == nil, ParseFile parses source without a FileSet. If fileSet != nil,
ParseFile first adds filename and src to fileSet.
The filename argument is optional and is used for labelling errors, etc.
src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST
always be in UTF-8.
// Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
program, err := parser.ParseFile(nil, "", `if (abc > 1) {}`, 0)
#### func ParseFunction
```go
func ParseFunction(parameterList, body string) (*ast.FunctionLiteral, error)
```
ParseFunction parses a given parameter list and body as a function and returns
the corresponding ast.FunctionLiteral node.
The parameter list, if any, should be a comma-separated list of identifiers.
#### func ReadSource
```go
func ReadSource(filename string, src interface{}) ([]byte, error)
```
#### func TransformRegExp
```go
func TransformRegExp(pattern string) (string, error)
```
TransformRegExp transforms a JavaScript pattern into a Go "regexp" pattern.
re2 (Go) cannot do backtracking, so the presence of a lookahead (?=) (?!) or
backreference (\1, \2, ...) will cause an error.
re2 (Go) has a different definition for \s: [\t\n\f\r ]. The JavaScript
definition, on the other hand, also includes \v, Unicode "Separator, Space",
etc.
If the pattern is invalid (not valid even in JavaScript), then this function
returns the empty string and an error.
If the pattern is valid, but incompatible (contains a lookahead or
backreference), then this function returns the transformation (a non-empty
string) AND an error.
#### type Error
```go
type Error struct {
Position file.Position
Message string
}
```
An Error represents a parsing error. It includes the position where the error
occurred and a message/description.
#### func (Error) Error
```go
func (self Error) Error() string
```
#### type ErrorList
```go
type ErrorList []*Error
```
ErrorList is a list of *Errors.
#### func (*ErrorList) Add
```go
func (self *ErrorList) Add(position file.Position, msg string)
```
Add adds an Error with given position and message to an ErrorList.
#### func (ErrorList) Err
```go
func (self ErrorList) Err() error
```
Err returns an error equivalent to this ErrorList. If the list is empty, Err
returns nil.
#### func (ErrorList) Error
```go
func (self ErrorList) Error() string
```
Error implements the Error interface.
#### func (ErrorList) Len
```go
func (self ErrorList) Len() int
```
#### func (ErrorList) Less
```go
func (self ErrorList) Less(i, j int) bool
```
#### func (*ErrorList) Reset
```go
func (self *ErrorList) Reset()
```
Reset resets an ErrorList to no errors.
#### func (ErrorList) Sort
```go
func (self ErrorList) Sort()
```
#### func (ErrorList) Swap
```go
func (self ErrorList) Swap(i, j int)
```
#### type Mode
```go
type Mode uint
```
A Mode value is a set of flags (or 0). They control optional parser
functionality.
--
**godocdown** http://github.com/robertkrimen/godocdown

View File

@@ -0,0 +1,176 @@
package parser
import (
"fmt"
"sort"
"github.com/dop251/goja/file"
"github.com/dop251/goja/token"
)
const (
err_UnexpectedToken = "Unexpected token %v"
err_UnexpectedEndOfInput = "Unexpected end of input"
err_UnexpectedEscape = "Unexpected escape"
)
// UnexpectedNumber: 'Unexpected number',
// UnexpectedString: 'Unexpected string',
// UnexpectedIdentifier: 'Unexpected identifier',
// UnexpectedReserved: 'Unexpected reserved word',
// NewlineAfterThrow: 'Illegal newline after throw',
// InvalidRegExp: 'Invalid regular expression',
// UnterminatedRegExp: 'Invalid regular expression: missing /',
// InvalidLHSInAssignment: 'Invalid left-hand side in assignment',
// InvalidLHSInForIn: 'Invalid left-hand side in for-in',
// MultipleDefaultsInSwitch: 'More than one default clause in switch statement',
// NoCatchOrFinally: 'Missing catch or finally after try',
// UnknownLabel: 'Undefined label \'%0\'',
// Redeclaration: '%0 \'%1\' has already been declared',
// IllegalContinue: 'Illegal continue statement',
// IllegalBreak: 'Illegal break statement',
// IllegalReturn: 'Illegal return statement',
// StrictModeWith: 'Strict mode code may not include a with statement',
// StrictCatchVariable: 'Catch variable may not be eval or arguments in strict mode',
// StrictVarName: 'Variable name may not be eval or arguments in strict mode',
// StrictParamName: 'Parameter name eval or arguments is not allowed in strict mode',
// StrictParamDupe: 'Strict mode function may not have duplicate parameter names',
// StrictFunctionName: 'Function name may not be eval or arguments in strict mode',
// StrictOctalLiteral: 'Octal literals are not allowed in strict mode.',
// StrictDelete: 'Delete of an unqualified identifier in strict mode.',
// StrictDuplicateProperty: 'Duplicate data property in object literal not allowed in strict mode',
// AccessorDataProperty: 'Object literal may not have data and accessor property with the same name',
// AccessorGetSet: 'Object literal may not have multiple get/set accessors with the same name',
// StrictLHSAssignment: 'Assignment to eval or arguments is not allowed in strict mode',
// StrictLHSPostfix: 'Postfix increment/decrement may not have eval or arguments operand in strict mode',
// StrictLHSPrefix: 'Prefix increment/decrement may not have eval or arguments operand in strict mode',
// StrictReservedWord: 'Use of future reserved word in strict mode'
// A SyntaxError is a description of an ECMAScript syntax error.
// An Error represents a parsing error. It includes the position where the error occurred and a message/description.
type Error struct {
Position file.Position
Message string
}
// FIXME Should this be "SyntaxError"?
func (self Error) Error() string {
filename := self.Position.Filename
if filename == "" {
filename = "(anonymous)"
}
return fmt.Sprintf("%s: Line %d:%d %s",
filename,
self.Position.Line,
self.Position.Column,
self.Message,
)
}
func (self *_parser) error(place interface{}, msg string, msgValues ...interface{}) *Error {
idx := file.Idx(0)
switch place := place.(type) {
case int:
idx = self.idxOf(place)
case file.Idx:
if place == 0 {
idx = self.idxOf(self.chrOffset)
} else {
idx = place
}
default:
panic(fmt.Errorf("error(%T, ...)", place))
}
position := self.position(idx)
msg = fmt.Sprintf(msg, msgValues...)
self.errors.Add(position, msg)
return self.errors[len(self.errors)-1]
}
func (self *_parser) errorUnexpected(idx file.Idx, chr rune) error {
if chr == -1 {
return self.error(idx, err_UnexpectedEndOfInput)
}
return self.error(idx, err_UnexpectedToken, token.ILLEGAL)
}
func (self *_parser) errorUnexpectedToken(tkn token.Token) error {
switch tkn {
case token.EOF:
return self.error(file.Idx(0), err_UnexpectedEndOfInput)
}
value := tkn.String()
switch tkn {
case token.BOOLEAN, token.NULL:
value = self.literal
case token.IDENTIFIER:
return self.error(self.idx, "Unexpected identifier")
case token.KEYWORD:
// TODO Might be a future reserved word
return self.error(self.idx, "Unexpected reserved word")
case token.ESCAPED_RESERVED_WORD:
return self.error(self.idx, "Keyword must not contain escaped characters")
case token.NUMBER:
return self.error(self.idx, "Unexpected number")
case token.STRING:
return self.error(self.idx, "Unexpected string")
}
return self.error(self.idx, err_UnexpectedToken, value)
}
// ErrorList is a list of *Errors.
type ErrorList []*Error
// Add adds an Error with given position and message to an ErrorList.
func (self *ErrorList) Add(position file.Position, msg string) {
*self = append(*self, &Error{position, msg})
}
// Reset resets an ErrorList to no errors.
func (self *ErrorList) Reset() { *self = (*self)[0:0] }
func (self ErrorList) Len() int { return len(self) }
func (self ErrorList) Swap(i, j int) { self[i], self[j] = self[j], self[i] }
func (self ErrorList) Less(i, j int) bool {
x := &self[i].Position
y := &self[j].Position
if x.Filename < y.Filename {
return true
}
if x.Filename == y.Filename {
if x.Line < y.Line {
return true
}
if x.Line == y.Line {
return x.Column < y.Column
}
}
return false
}
func (self ErrorList) Sort() {
sort.Sort(self)
}
// Error implements the Error interface.
func (self ErrorList) Error() string {
switch len(self) {
case 0:
return "no errors"
case 1:
return self[0].Error()
}
return fmt.Sprintf("%s (and %d more errors)", self[0].Error(), len(self)-1)
}
// Err returns an error equivalent to this ErrorList.
// If the list is empty, Err returns nil.
func (self ErrorList) Err() error {
if len(self) == 0 {
return nil
}
return self
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,401 @@
package parser
import (
"testing"
"github.com/dop251/goja/file"
"github.com/dop251/goja/token"
"github.com/dop251/goja/unistring"
)
func TestLexer(t *testing.T) {
tt(t, func() {
setup := func(src string) *_parser {
parser := newParser("", src)
return parser
}
test := func(src string, test ...interface{}) {
parser := setup(src)
for len(test) > 0 {
tkn, literal, _, idx := parser.scan()
if len(test) > 0 {
is(tkn, test[0].(token.Token))
test = test[1:]
}
if len(test) > 0 {
is(literal, unistring.String(test[0].(string)))
test = test[1:]
}
if len(test) > 0 {
// FIXME terst, Fix this so that cast to file.Idx is not necessary?
is(idx, file.Idx(test[0].(int)))
test = test[1:]
}
}
}
test("",
token.EOF, "", 1,
)
test("#!",
token.EOF, "", 3,
)
test("#!\n1",
token.NUMBER, "1", 4,
token.EOF, "", 5,
)
test("1",
token.NUMBER, "1", 1,
token.EOF, "", 2,
)
test(".0",
token.NUMBER, ".0", 1,
token.EOF, "", 3,
)
test("abc",
token.IDENTIFIER, "abc", 1,
token.EOF, "", 4,
)
test("abc(1)",
token.IDENTIFIER, "abc", 1,
token.LEFT_PARENTHESIS, "", 4,
token.NUMBER, "1", 5,
token.RIGHT_PARENTHESIS, "", 6,
token.EOF, "", 7,
)
test(".",
token.PERIOD, "", 1,
token.EOF, "", 2,
)
test("===.",
token.STRICT_EQUAL, "", 1,
token.PERIOD, "", 4,
token.EOF, "", 5,
)
test(">>>=.0",
token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1,
token.NUMBER, ".0", 5,
token.EOF, "", 7,
)
test(">>>=0.0.",
token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1,
token.NUMBER, "0.0", 5,
token.PERIOD, "", 8,
token.EOF, "", 9,
)
test("\"abc\"",
token.STRING, "\"abc\"", 1,
token.EOF, "", 6,
)
test("abc = //",
token.IDENTIFIER, "abc", 1,
token.ASSIGN, "", 5,
token.EOF, "", 9,
)
test("abc = 1 / 2",
token.IDENTIFIER, "abc", 1,
token.ASSIGN, "", 5,
token.NUMBER, "1", 7,
token.SLASH, "", 9,
token.NUMBER, "2", 11,
token.EOF, "", 12,
)
test("xyzzy = 'Nothing happens.'",
token.IDENTIFIER, "xyzzy", 1,
token.ASSIGN, "", 7,
token.STRING, "'Nothing happens.'", 9,
token.EOF, "", 27,
)
test("abc = !false",
token.IDENTIFIER, "abc", 1,
token.ASSIGN, "", 5,
token.NOT, "", 7,
token.BOOLEAN, "false", 8,
token.EOF, "", 13,
)
test("abc = !!true",
token.IDENTIFIER, "abc", 1,
token.ASSIGN, "", 5,
token.NOT, "", 7,
token.NOT, "", 8,
token.BOOLEAN, "true", 9,
token.EOF, "", 13,
)
test("abc *= 1",
token.IDENTIFIER, "abc", 1,
token.MULTIPLY_ASSIGN, "", 5,
token.NUMBER, "1", 8,
token.EOF, "", 9,
)
test("if 1 else",
token.IF, "if", 1,
token.NUMBER, "1", 4,
token.ELSE, "else", 6,
token.EOF, "", 10,
)
test("null",
token.NULL, "null", 1,
token.EOF, "", 5,
)
test(`"\u007a\x79\u000a\x78"`,
token.STRING, "\"\\u007a\\x79\\u000a\\x78\"", 1,
token.EOF, "", 23,
)
test(`"[First line \
Second line \
Third line\
. ]"
`,
token.STRING, "\"[First line \\\nSecond line \\\n Third line\\\n. ]\"", 1,
token.EOF, "", 53,
)
test("/",
token.SLASH, "", 1,
token.EOF, "", 2,
)
test("var abc = \"abc\uFFFFabc\"",
token.VAR, "var", 1,
token.IDENTIFIER, "abc", 5,
token.ASSIGN, "", 9,
token.STRING, "\"abc\uFFFFabc\"", 11,
token.EOF, "", 22,
)
test(`'\t' === '\r'`,
token.STRING, "'\\t'", 1,
token.STRICT_EQUAL, "", 6,
token.STRING, "'\\r'", 10,
token.EOF, "", 14,
)
test(`var \u0024 = 1`,
token.VAR, "var", 1,
token.IDENTIFIER, "\\u0024", 5,
token.ASSIGN, "", 12,
token.NUMBER, "1", 14,
token.EOF, "", 15,
)
test("10e10000",
token.NUMBER, "10e10000", 1,
token.EOF, "", 9,
)
test(`var if var class`,
token.VAR, "var", 1,
token.IF, "if", 5,
token.VAR, "var", 8,
token.CLASS, "class", 12,
token.EOF, "", 17,
)
test(`-0`,
token.MINUS, "", 1,
token.NUMBER, "0", 2,
token.EOF, "", 3,
)
test(`.01`,
token.NUMBER, ".01", 1,
token.EOF, "", 4,
)
test(`.01e+2`,
token.NUMBER, ".01e+2", 1,
token.EOF, "", 7,
)
test(";",
token.SEMICOLON, "", 1,
token.EOF, "", 2,
)
test(";;",
token.SEMICOLON, "", 1,
token.SEMICOLON, "", 2,
token.EOF, "", 3,
)
test("//",
token.EOF, "", 3,
)
test(";;//",
token.SEMICOLON, "", 1,
token.SEMICOLON, "", 2,
token.EOF, "", 5,
)
test("1",
token.NUMBER, "1", 1,
)
test("12 123",
token.NUMBER, "12", 1,
token.NUMBER, "123", 4,
)
test("1.2 12.3",
token.NUMBER, "1.2", 1,
token.NUMBER, "12.3", 5,
)
test("/ /=",
token.SLASH, "", 1,
token.QUOTIENT_ASSIGN, "", 3,
)
test(`"abc"`,
token.STRING, `"abc"`, 1,
)
test(`'abc'`,
token.STRING, `'abc'`, 1,
)
test("++",
token.INCREMENT, "", 1,
)
test(">",
token.GREATER, "", 1,
)
test(">=",
token.GREATER_OR_EQUAL, "", 1,
)
test(">>",
token.SHIFT_RIGHT, "", 1,
)
test(">>=",
token.SHIFT_RIGHT_ASSIGN, "", 1,
)
test(">>>",
token.UNSIGNED_SHIFT_RIGHT, "", 1,
)
test(">>>=",
token.UNSIGNED_SHIFT_RIGHT_ASSIGN, "", 1,
)
test("1 \"abc\"",
token.NUMBER, "1", 1,
token.STRING, "\"abc\"", 3,
)
test(",",
token.COMMA, "", 1,
)
test("1, \"abc\"",
token.NUMBER, "1", 1,
token.COMMA, "", 2,
token.STRING, "\"abc\"", 4,
)
test("new abc(1, 3.14159);",
token.NEW, "new", 1,
token.IDENTIFIER, "abc", 5,
token.LEFT_PARENTHESIS, "", 8,
token.NUMBER, "1", 9,
token.COMMA, "", 10,
token.NUMBER, "3.14159", 12,
token.RIGHT_PARENTHESIS, "", 19,
token.SEMICOLON, "", 20,
)
test("1 == \"1\"",
token.NUMBER, "1", 1,
token.EQUAL, "", 3,
token.STRING, "\"1\"", 6,
)
test("1\n[]\n",
token.NUMBER, "1", 1,
token.LEFT_BRACKET, "", 3,
token.RIGHT_BRACKET, "", 4,
)
test("1\ufeff[]\ufeff",
token.NUMBER, "1", 1,
token.LEFT_BRACKET, "", 5,
token.RIGHT_BRACKET, "", 6,
)
test("x ?.30 : false",
token.IDENTIFIER, "x", 1,
token.QUESTION_MARK, "", 3,
token.NUMBER, ".30", 4,
token.COLON, "", 8,
token.BOOLEAN, "false", 10,
)
test("a\n?.b",
token.IDENTIFIER, "a", 1,
token.QUESTION_DOT, "", 3,
token.IDENTIFIER, "b", 5,
)
// ILLEGAL
test(`3ea`,
token.ILLEGAL, "3e", 1,
token.IDENTIFIER, "a", 3,
token.EOF, "", 4,
)
test(`3in`,
token.ILLEGAL, "3", 1,
token.IN, "in", 2,
token.EOF, "", 4,
)
test("\"Hello\nWorld\"",
token.ILLEGAL, "", 1,
token.IDENTIFIER, "World", 8,
token.ILLEGAL, "", 13,
token.EOF, "", 14,
)
test("\u203f = 10",
token.ILLEGAL, "", 1,
token.ASSIGN, "", 5,
token.NUMBER, "10", 7,
token.EOF, "", 9,
)
test(`"\x0G"`,
token.ILLEGAL, "\"\\x0G\"", 1,
//token.STRING, "\"\\x0G\"", 1,
token.EOF, "", 7,
)
})
}

View File

@@ -0,0 +1,890 @@
package parser
import (
"bytes"
"encoding/json"
"reflect"
"strings"
"testing"
"github.com/dop251/goja/ast"
)
func marshal(name string, children ...interface{}) interface{} {
if len(children) == 1 {
if name == "" {
return testMarshalNode(children[0])
}
return map[string]interface{}{
name: children[0],
}
}
map_ := map[string]interface{}{}
length := len(children) / 2
for i := 0; i < length; i++ {
name := children[i*2].(string)
value := children[i*2+1]
map_[name] = value
}
if name == "" {
return map_
}
return map[string]interface{}{
name: map_,
}
}
func testMarshalNode(node interface{}) interface{} {
switch node := node.(type) {
// Expression
case *ast.ArrayLiteral:
return marshal("Array", testMarshalNode(node.Value))
case *ast.AssignExpression:
return marshal("Assign",
"Left", testMarshalNode(node.Left),
"Right", testMarshalNode(node.Right),
)
case *ast.BinaryExpression:
return marshal("BinaryExpression",
"Operator", node.Operator.String(),
"Left", testMarshalNode(node.Left),
"Right", testMarshalNode(node.Right),
)
case *ast.BooleanLiteral:
return marshal("Literal", node.Value)
case *ast.CallExpression:
return marshal("Call",
"Callee", testMarshalNode(node.Callee),
"ArgumentList", testMarshalNode(node.ArgumentList),
)
case *ast.ConditionalExpression:
return marshal("Conditional",
"Test", testMarshalNode(node.Test),
"Consequent", testMarshalNode(node.Consequent),
"Alternate", testMarshalNode(node.Alternate),
)
case *ast.DotExpression:
return marshal("Dot",
"Left", testMarshalNode(node.Left),
"Member", node.Identifier.Name,
)
case *ast.NewExpression:
return marshal("New",
"Callee", testMarshalNode(node.Callee),
"ArgumentList", testMarshalNode(node.ArgumentList),
)
case *ast.NullLiteral:
return marshal("Literal", nil)
case *ast.NumberLiteral:
return marshal("Literal", node.Value)
case *ast.ObjectLiteral:
return marshal("Object", testMarshalNode(node.Value))
case *ast.RegExpLiteral:
return marshal("Literal", node.Literal)
case *ast.StringLiteral:
return marshal("Literal", node.Literal)
case *ast.Binding:
return marshal("Binding", "Target", testMarshalNode(node.Target),
"Initializer", testMarshalNode(node.Initializer))
// Statement
case *ast.Program:
return testMarshalNode(node.Body)
case *ast.BlockStatement:
return marshal("BlockStatement", testMarshalNode(node.List))
case *ast.EmptyStatement:
return "EmptyStatement"
case *ast.ExpressionStatement:
return testMarshalNode(node.Expression)
case *ast.ForInStatement:
return marshal("ForIn",
"Into", testMarshalNode(node.Into),
"Source", marshal("", node.Source),
"Body", marshal("", node.Body),
)
case *ast.FunctionLiteral:
return marshal("Function", testMarshalNode(node.Body))
case *ast.Identifier:
return marshal("Identifier", node.Name)
case *ast.IfStatement:
if_ := marshal("",
"Test", testMarshalNode(node.Test),
"Consequent", testMarshalNode(node.Consequent),
).(map[string]interface{})
if node.Alternate != nil {
if_["Alternate"] = testMarshalNode(node.Alternate)
}
return marshal("If", if_)
case *ast.LabelledStatement:
return marshal("Label",
"Name", node.Label.Name,
"Statement", testMarshalNode(node.Statement),
)
case *ast.PropertyKeyed:
return marshal("",
"Key", node.Key,
"Value", testMarshalNode(node.Value),
)
case *ast.ReturnStatement:
return marshal("Return", testMarshalNode(node.Argument))
case *ast.SequenceExpression:
return marshal("Sequence", testMarshalNode(node.Sequence))
case *ast.ThrowStatement:
return marshal("Throw", testMarshalNode(node.Argument))
case *ast.VariableStatement:
return marshal("Var", testMarshalNode(node.List))
// Special
case *ast.ForDeclaration:
return marshal("For-Into-Decl", testMarshalNode(node.Target))
case *ast.ForIntoVar:
return marshal("For-Into-Var", testMarshalNode(node.Binding))
}
{
value := reflect.ValueOf(node)
if value.Kind() == reflect.Slice {
tmp0 := []interface{}{}
for index := 0; index < value.Len(); index++ {
tmp0 = append(tmp0, testMarshalNode(value.Index(index).Interface()))
}
return tmp0
}
}
return nil
}
func testMarshal(node interface{}) string {
value, err := json.Marshal(testMarshalNode(node))
if err != nil {
panic(err)
}
return string(value)
}
func TestParserAST(t *testing.T) {
tt(t, func() {
test := func(inputOutput string) {
match := matchBeforeAfterSeparator.FindStringIndex(inputOutput)
input := strings.TrimSpace(inputOutput[0:match[0]])
wantOutput := strings.TrimSpace(inputOutput[match[1]:])
_, program, err := testParse(input)
is(err, nil)
haveOutput := testMarshal(program)
tmp0, tmp1 := bytes.Buffer{}, bytes.Buffer{}
json.Indent(&tmp0, []byte(haveOutput), "\t\t", " ")
json.Indent(&tmp1, []byte(wantOutput), "\t\t", " ")
is("\n\t\t"+tmp0.String(), "\n\t\t"+tmp1.String())
}
test(`
---
[]
`)
test(`
;
---
[
"EmptyStatement"
]
`)
test(`
;;;
---
[
"EmptyStatement",
"EmptyStatement",
"EmptyStatement"
]
`)
test(`
1; true; abc; "abc"; null;
---
[
{
"Literal": 1
},
{
"Literal": true
},
{
"Identifier": "abc"
},
{
"Literal": "\"abc\""
},
{
"Literal": null
}
]
`)
test(`
{ 1; null; 3.14159; ; }
---
[
{
"BlockStatement": [
{
"Literal": 1
},
{
"Literal": null
},
{
"Literal": 3.14159
},
"EmptyStatement"
]
}
]
`)
test(`
new abc();
---
[
{
"New": {
"ArgumentList": [],
"Callee": {
"Identifier": "abc"
}
}
}
]
`)
test(`
new abc(1, 3.14159)
---
[
{
"New": {
"ArgumentList": [
{
"Literal": 1
},
{
"Literal": 3.14159
}
],
"Callee": {
"Identifier": "abc"
}
}
}
]
`)
test(`
true ? false : true
---
[
{
"Conditional": {
"Alternate": {
"Literal": true
},
"Consequent": {
"Literal": false
},
"Test": {
"Literal": true
}
}
}
]
`)
test(`
true || false
---
[
{
"BinaryExpression": {
"Left": {
"Literal": true
},
"Operator": "||",
"Right": {
"Literal": false
}
}
}
]
`)
test(`
0 + { abc: true }
---
[
{
"BinaryExpression": {
"Left": {
"Literal": 0
},
"Operator": "+",
"Right": {
"Object": [
{
"Key": {
"Idx": 7,
"Literal": "abc",
"Value": "abc"
},
"Value": {
"Literal": true
}
}
]
}
}
}
]
`)
test(`
1 == "1"
---
[
{
"BinaryExpression": {
"Left": {
"Literal": 1
},
"Operator": "==",
"Right": {
"Literal": "\"1\""
}
}
}
]
`)
test(`
abc(1)
---
[
{
"Call": {
"ArgumentList": [
{
"Literal": 1
}
],
"Callee": {
"Identifier": "abc"
}
}
}
]
`)
test(`
Math.pow(3, 2)
---
[
{
"Call": {
"ArgumentList": [
{
"Literal": 3
},
{
"Literal": 2
}
],
"Callee": {
"Dot": {
"Left": {
"Identifier": "Math"
},
"Member": "pow"
}
}
}
}
]
`)
test(`
1, 2, 3
---
[
{
"Sequence": [
{
"Literal": 1
},
{
"Literal": 2
},
{
"Literal": 3
}
]
}
]
`)
test(`
/ abc /gim;
---
[
{
"Literal": "/ abc /gim"
}
]
`)
test(`
if (0)
1;
---
[
{
"If": {
"Consequent": {
"Literal": 1
},
"Test": {
"Literal": 0
}
}
}
]
`)
test(`
0+function(){
return;
}
---
[
{
"BinaryExpression": {
"Left": {
"Literal": 0
},
"Operator": "+",
"Right": {
"Function": {
"BlockStatement": [
{
"Return": null
}
]
}
}
}
}
]
`)
test(`
xyzzy // Ignore it
// Ignore this
// And this
/* And all..
... of this!
*/
"Nothing happens."
// And finally this
---
[
{
"Identifier": "xyzzy"
},
{
"Literal": "\"Nothing happens.\""
}
]
`)
test(`
((x & (x = 1)) !== 0)
---
[
{
"BinaryExpression": {
"Left": {
"BinaryExpression": {
"Left": {
"Identifier": "x"
},
"Operator": "\u0026",
"Right": {
"Assign": {
"Left": {
"Identifier": "x"
},
"Right": {
"Literal": 1
}
}
}
}
},
"Operator": "!==",
"Right": {
"Literal": 0
}
}
}
]
`)
test(`
{ abc: 'def' }
---
[
{
"BlockStatement": [
{
"Label": {
"Name": "abc",
"Statement": {
"Literal": "'def'"
}
}
}
]
}
]
`)
test(`
// This is not an object, this is a string literal with a label!
({ abc: 'def' })
---
[
{
"Object": [
{
"Key": {
"Idx": 77,
"Literal": "abc",
"Value": "abc"
},
"Value": {
"Literal": "'def'"
}
}
]
}
]
`)
test(`
[,]
---
[
{
"Array": [
null
]
}
]
`)
test(`
[,,]
---
[
{
"Array": [
null,
null
]
}
]
`)
test(`
({ get abc() {} })
---
[
{
"Object": [
{
"Key": {
"Idx": 8,
"Literal": "abc",
"Value": "abc"
},
"Value": {
"Function": {
"BlockStatement": []
}
}
}
]
}
]
`)
test(`
/abc/.source
---
[
{
"Dot": {
"Left": {
"Literal": "/abc/"
},
"Member": "source"
}
}
]
`)
test(`
xyzzy
throw new TypeError("Nothing happens.")
---
[
{
"Identifier": "xyzzy"
},
{
"Throw": {
"New": {
"ArgumentList": [
{
"Literal": "\"Nothing happens.\""
}
],
"Callee": {
"Identifier": "TypeError"
}
}
}
}
]
`)
// When run, this will call a type error to be thrown
// This is essentially the same as:
//
// var abc = 1(function(){})()
//
test(`
var abc = 1
(function(){
})()
---
[
{
"Var": [
{
"Binding": {
"Initializer": {
"Call": {
"ArgumentList": [],
"Callee": {
"Call": {
"ArgumentList": [
{
"Function": {
"BlockStatement": []
}
}
],
"Callee": {
"Literal": 1
}
}
}
}
},
"Target": {
"Identifier": "abc"
}
}
}
]
}
]
`)
test(`
"use strict"
---
[
{
"Literal": "\"use strict\""
}
]
`)
test(`
"use strict"
abc = 1 + 2 + 11
---
[
{
"Literal": "\"use strict\""
},
{
"Assign": {
"Left": {
"Identifier": "abc"
},
"Right": {
"BinaryExpression": {
"Left": {
"BinaryExpression": {
"Left": {
"Literal": 1
},
"Operator": "+",
"Right": {
"Literal": 2
}
}
},
"Operator": "+",
"Right": {
"Literal": 11
}
}
}
}
}
]
`)
test(`
abc = function() { 'use strict' }
---
[
{
"Assign": {
"Left": {
"Identifier": "abc"
},
"Right": {
"Function": {
"BlockStatement": [
{
"Literal": "'use strict'"
}
]
}
}
}
}
]
`)
test(`
for (var abc in def) {
}
---
[
{
"ForIn": {
"Body": {
"BlockStatement": []
},
"Into": {
"For-Into-Var": {
"Binding": {
"Initializer": null,
"Target": {
"Identifier": "abc"
}
}
}
},
"Source": {
"Identifier": "def"
}
}
}
]
`)
test(`
abc = {
'"': "'",
"'": '"',
}
---
[
{
"Assign": {
"Left": {
"Identifier": "abc"
},
"Right": {
"Object": [
{
"Key": {
"Idx": 21,
"Literal": "'\"'",
"Value": "\""
},
"Value": {
"Literal": "\"'\""
}
},
{
"Key": {
"Idx": 43,
"Literal": "\"'\"",
"Value": "'"
},
"Value": {
"Literal": "'\"'"
}
}
]
}
}
}
]
`)
})
}

View File

@@ -0,0 +1,268 @@
/*
Package parser implements a parser for JavaScript.
import (
"github.com/dop251/goja/parser"
)
Parse and return an AST
filename := "" // A filename is optional
src := `
// Sample xyzzy example
(function(){
if (3.14159 > 0) {
console.log("Hello, World.");
return;
}
var xyzzy = NaN;
console.log("Nothing happens.");
return xyzzy;
})();
`
// Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
program, err := parser.ParseFile(nil, filename, src, 0)
# Warning
The parser and AST interfaces are still works-in-progress (particularly where
node types are concerned) and may change in the future.
*/
package parser
import (
"bytes"
"errors"
"io"
"os"
"github.com/dop251/goja/ast"
"github.com/dop251/goja/file"
"github.com/dop251/goja/token"
"github.com/dop251/goja/unistring"
)
// A Mode value is a set of flags (or 0). They control optional parser functionality.
type Mode uint
const (
IgnoreRegExpErrors Mode = 1 << iota // Ignore RegExp compatibility errors (allow backtracking)
)
type options struct {
disableSourceMaps bool
sourceMapLoader func(path string) ([]byte, error)
}
// Option represents one of the options for the parser to use in the Parse methods. Currently supported are:
// WithDisableSourceMaps and WithSourceMapLoader.
type Option func(*options)
// WithDisableSourceMaps is an option to disable source maps support. May save a bit of time when source maps
// are not in use.
func WithDisableSourceMaps(opts *options) {
opts.disableSourceMaps = true
}
// WithSourceMapLoader is an option to set a custom source map loader. The loader will be given a path or a
// URL from the sourceMappingURL. If sourceMappingURL is not absolute it is resolved relatively to the name
// of the file being parsed. Any error returned by the loader will fail the parsing.
// Note that setting this to nil does not disable source map support, there is a default loader which reads
// from the filesystem. Use WithDisableSourceMaps to disable source map support.
func WithSourceMapLoader(loader func(path string) ([]byte, error)) Option {
return func(opts *options) {
opts.sourceMapLoader = loader
}
}
type _parser struct {
str string
length int
base int
chr rune // The current character
chrOffset int // The offset of current character
offset int // The offset after current character (may be greater than 1)
idx file.Idx // The index of token
token token.Token // The token
literal string // The literal of the token, if any
parsedLiteral unistring.String
scope *_scope
insertSemicolon bool // If we see a newline, then insert an implicit semicolon
implicitSemicolon bool // An implicit semicolon exists
errors ErrorList
recover struct {
// Scratch when trying to seek to the next statement, etc.
idx file.Idx
count int
}
mode Mode
opts options
file *file.File
}
func _newParser(filename, src string, base int, opts ...Option) *_parser {
p := &_parser{
chr: ' ', // This is set so we can start scanning by skipping whitespace
str: src,
length: len(src),
base: base,
file: file.NewFile(filename, src, base),
}
for _, opt := range opts {
opt(&p.opts)
}
return p
}
func newParser(filename, src string) *_parser {
return _newParser(filename, src, 1)
}
func ReadSource(filename string, src interface{}) ([]byte, error) {
if src != nil {
switch src := src.(type) {
case string:
return []byte(src), nil
case []byte:
return src, nil
case *bytes.Buffer:
if src != nil {
return src.Bytes(), nil
}
case io.Reader:
var bfr bytes.Buffer
if _, err := io.Copy(&bfr, src); err != nil {
return nil, err
}
return bfr.Bytes(), nil
}
return nil, errors.New("invalid source")
}
return os.ReadFile(filename)
}
// ParseFile parses the source code of a single JavaScript/ECMAScript source file and returns
// the corresponding ast.Program node.
//
// If fileSet == nil, ParseFile parses source without a FileSet.
// If fileSet != nil, ParseFile first adds filename and src to fileSet.
//
// The filename argument is optional and is used for labelling errors, etc.
//
// src may be a string, a byte slice, a bytes.Buffer, or an io.Reader, but it MUST always be in UTF-8.
//
// // Parse some JavaScript, yielding a *ast.Program and/or an ErrorList
// program, err := parser.ParseFile(nil, "", `if (abc > 1) {}`, 0)
func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mode, options ...Option) (*ast.Program, error) {
str, err := ReadSource(filename, src)
if err != nil {
return nil, err
}
{
str := string(str)
base := 1
if fileSet != nil {
base = fileSet.AddFile(filename, str)
}
parser := _newParser(filename, str, base, options...)
parser.mode = mode
return parser.parse()
}
}
// ParseFunction parses a given parameter list and body as a function and returns the
// corresponding ast.FunctionLiteral node.
//
// The parameter list, if any, should be a comma-separated list of identifiers.
func ParseFunction(parameterList, body string, options ...Option) (*ast.FunctionLiteral, error) {
src := "(function(" + parameterList + ") {\n" + body + "\n})"
parser := _newParser("", src, 1, options...)
program, err := parser.parse()
if err != nil {
return nil, err
}
return program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral), nil
}
func (self *_parser) slice(idx0, idx1 file.Idx) string {
from := int(idx0) - self.base
to := int(idx1) - self.base
if from >= 0 && to <= len(self.str) {
return self.str[from:to]
}
return ""
}
func (self *_parser) parse() (*ast.Program, error) {
self.openScope()
defer self.closeScope()
self.next()
program := self.parseProgram()
if false {
self.errors.Sort()
}
return program, self.errors.Err()
}
func (self *_parser) next() {
self.token, self.literal, self.parsedLiteral, self.idx = self.scan()
}
func (self *_parser) optionalSemicolon() {
if self.token == token.SEMICOLON {
self.next()
return
}
if self.implicitSemicolon {
self.implicitSemicolon = false
return
}
if self.token != token.EOF && self.token != token.RIGHT_BRACE {
self.expect(token.SEMICOLON)
}
}
func (self *_parser) semicolon() {
if self.token != token.RIGHT_PARENTHESIS && self.token != token.RIGHT_BRACE {
if self.implicitSemicolon {
self.implicitSemicolon = false
return
}
self.expect(token.SEMICOLON)
}
}
func (self *_parser) idxOf(offset int) file.Idx {
return file.Idx(self.base + offset)
}
func (self *_parser) expect(value token.Token) file.Idx {
idx := self.idx
if self.token != value {
self.errorUnexpectedToken(self.token)
}
self.next()
return idx
}
func (self *_parser) position(idx file.Idx) file.Position {
return self.file.Position(int(idx) - self.base)
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,458 @@
package parser
import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
)
const (
WhitespaceChars = " \f\n\r\t\v\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
Re2Dot = "[^\r\n\u2028\u2029]"
)
type regexpParseError struct {
offset int
err string
}
type RegexpErrorIncompatible struct {
regexpParseError
}
type RegexpSyntaxError struct {
regexpParseError
}
func (s regexpParseError) Error() string {
return s.err
}
type _RegExp_parser struct {
str string
length int
chr rune // The current character
chrOffset int // The offset of current character
offset int // The offset after current character (may be greater than 1)
err error
goRegexp strings.Builder
passOffset int
}
// TransformRegExp transforms a JavaScript pattern into a Go "regexp" pattern.
//
// re2 (Go) cannot do backtracking, so the presence of a lookahead (?=) (?!) or
// backreference (\1, \2, ...) will cause an error.
//
// re2 (Go) has a different definition for \s: [\t\n\f\r ].
// The JavaScript definition, on the other hand, also includes \v, Unicode "Separator, Space", etc.
//
// If the pattern is valid, but incompatible (contains a lookahead or backreference),
// then this function returns an empty string an error of type RegexpErrorIncompatible.
//
// If the pattern is invalid (not valid even in JavaScript), then this function
// returns an empty string and a generic error.
func TransformRegExp(pattern string) (transformed string, err error) {
if pattern == "" {
return "", nil
}
parser := _RegExp_parser{
str: pattern,
length: len(pattern),
}
err = parser.parse()
if err != nil {
return "", err
}
return parser.ResultString(), nil
}
func (self *_RegExp_parser) ResultString() string {
if self.passOffset != -1 {
return self.str[:self.passOffset]
}
return self.goRegexp.String()
}
func (self *_RegExp_parser) parse() (err error) {
self.read() // Pull in the first character
self.scan()
return self.err
}
func (self *_RegExp_parser) read() {
if self.offset < self.length {
self.chrOffset = self.offset
chr, width := rune(self.str[self.offset]), 1
if chr >= utf8.RuneSelf { // !ASCII
chr, width = utf8.DecodeRuneInString(self.str[self.offset:])
if chr == utf8.RuneError && width == 1 {
self.error(true, "Invalid UTF-8 character")
return
}
}
self.offset += width
self.chr = chr
} else {
self.chrOffset = self.length
self.chr = -1 // EOF
}
}
func (self *_RegExp_parser) stopPassing() {
self.goRegexp.Grow(3 * len(self.str) / 2)
self.goRegexp.WriteString(self.str[:self.passOffset])
self.passOffset = -1
}
func (self *_RegExp_parser) write(p []byte) {
if self.passOffset != -1 {
self.stopPassing()
}
self.goRegexp.Write(p)
}
func (self *_RegExp_parser) writeByte(b byte) {
if self.passOffset != -1 {
self.stopPassing()
}
self.goRegexp.WriteByte(b)
}
func (self *_RegExp_parser) writeString(s string) {
if self.passOffset != -1 {
self.stopPassing()
}
self.goRegexp.WriteString(s)
}
func (self *_RegExp_parser) scan() {
for self.chr != -1 {
switch self.chr {
case '\\':
self.read()
self.scanEscape(false)
case '(':
self.pass()
self.scanGroup()
case '[':
self.scanBracket()
case ')':
self.error(true, "Unmatched ')'")
return
case '.':
self.writeString(Re2Dot)
self.read()
default:
self.pass()
}
}
}
// (...)
func (self *_RegExp_parser) scanGroup() {
str := self.str[self.chrOffset:]
if len(str) > 1 { // A possibility of (?= or (?!
if str[0] == '?' {
ch := str[1]
switch {
case ch == '=' || ch == '!':
self.error(false, "re2: Invalid (%s) <lookahead>", self.str[self.chrOffset:self.chrOffset+2])
return
case ch == '<':
self.error(false, "re2: Invalid (%s) <lookbehind>", self.str[self.chrOffset:self.chrOffset+2])
return
case ch != ':':
self.error(true, "Invalid group")
return
}
}
}
for self.chr != -1 && self.chr != ')' {
switch self.chr {
case '\\':
self.read()
self.scanEscape(false)
case '(':
self.pass()
self.scanGroup()
case '[':
self.scanBracket()
case '.':
self.writeString(Re2Dot)
self.read()
default:
self.pass()
continue
}
}
if self.chr != ')' {
self.error(true, "Unterminated group")
return
}
self.pass()
}
// [...]
func (self *_RegExp_parser) scanBracket() {
str := self.str[self.chrOffset:]
if strings.HasPrefix(str, "[]") {
// [] -- Empty character class
self.writeString("[^\u0000-\U0001FFFF]")
self.offset += 1
self.read()
return
}
if strings.HasPrefix(str, "[^]") {
self.writeString("[\u0000-\U0001FFFF]")
self.offset += 2
self.read()
return
}
self.pass()
for self.chr != -1 {
if self.chr == ']' {
break
} else if self.chr == '\\' {
self.read()
self.scanEscape(true)
continue
}
self.pass()
}
if self.chr != ']' {
self.error(true, "Unterminated character class")
return
}
self.pass()
}
// \...
func (self *_RegExp_parser) scanEscape(inClass bool) {
offset := self.chrOffset
var length, base uint32
switch self.chr {
case '0', '1', '2', '3', '4', '5', '6', '7':
var value int64
size := 0
for {
digit := int64(digitValue(self.chr))
if digit >= 8 {
// Not a valid digit
break
}
value = value*8 + digit
self.read()
size += 1
}
if size == 1 { // The number of characters read
if value != 0 {
// An invalid backreference
self.error(false, "re2: Invalid \\%d <backreference>", value)
return
}
self.passString(offset-1, self.chrOffset)
return
}
tmp := []byte{'\\', 'x', '0', 0}
if value >= 16 {
tmp = tmp[0:2]
} else {
tmp = tmp[0:3]
}
tmp = strconv.AppendInt(tmp, value, 16)
self.write(tmp)
return
case '8', '9':
self.read()
self.error(false, "re2: Invalid \\%s <backreference>", self.str[offset:self.chrOffset])
return
case 'x':
self.read()
length, base = 2, 16
case 'u':
self.read()
if self.chr == '{' {
self.read()
length, base = 0, 16
} else {
length, base = 4, 16
}
case 'b':
if inClass {
self.write([]byte{'\\', 'x', '0', '8'})
self.read()
return
}
fallthrough
case 'B':
fallthrough
case 'd', 'D', 'w', 'W':
// This is slightly broken, because ECMAScript
// includes \v in \s, \S, while re2 does not
fallthrough
case '\\':
fallthrough
case 'f', 'n', 'r', 't', 'v':
self.passString(offset-1, self.offset)
self.read()
return
case 'c':
self.read()
var value int64
if 'a' <= self.chr && self.chr <= 'z' {
value = int64(self.chr - 'a' + 1)
} else if 'A' <= self.chr && self.chr <= 'Z' {
value = int64(self.chr - 'A' + 1)
} else {
self.writeByte('c')
return
}
tmp := []byte{'\\', 'x', '0', 0}
if value >= 16 {
tmp = tmp[0:2]
} else {
tmp = tmp[0:3]
}
tmp = strconv.AppendInt(tmp, value, 16)
self.write(tmp)
self.read()
return
case 's':
if inClass {
self.writeString(WhitespaceChars)
} else {
self.writeString("[" + WhitespaceChars + "]")
}
self.read()
return
case 'S':
if inClass {
self.error(false, "S in class")
return
} else {
self.writeString("[^" + WhitespaceChars + "]")
}
self.read()
return
default:
// $ is an identifier character, so we have to have
// a special case for it here
if self.chr == '$' || self.chr < utf8.RuneSelf && !isIdentifierPart(self.chr) {
// A non-identifier character needs escaping
self.passString(offset-1, self.offset)
self.read()
return
}
// Unescape the character for re2
self.pass()
return
}
// Otherwise, we're a \u.... or \x...
valueOffset := self.chrOffset
if length > 0 {
for length := length; length > 0; length-- {
digit := uint32(digitValue(self.chr))
if digit >= base {
// Not a valid digit
goto skip
}
self.read()
}
} else {
for self.chr != '}' && self.chr != -1 {
digit := uint32(digitValue(self.chr))
if digit >= base {
// Not a valid digit
goto skip
}
self.read()
}
}
if length == 4 || length == 0 {
self.write([]byte{
'\\',
'x',
'{',
})
self.passString(valueOffset, self.chrOffset)
if length != 0 {
self.writeByte('}')
}
} else if length == 2 {
self.passString(offset-1, valueOffset+2)
} else {
// Should never, ever get here...
self.error(true, "re2: Illegal branch in scanEscape")
return
}
return
skip:
self.passString(offset, self.chrOffset)
}
func (self *_RegExp_parser) pass() {
if self.passOffset == self.chrOffset {
self.passOffset = self.offset
} else {
if self.passOffset != -1 {
self.stopPassing()
}
if self.chr != -1 {
self.goRegexp.WriteRune(self.chr)
}
}
self.read()
}
func (self *_RegExp_parser) passString(start, end int) {
if self.passOffset == start {
self.passOffset = end
return
}
if self.passOffset != -1 {
self.stopPassing()
}
self.goRegexp.WriteString(self.str[start:end])
}
func (self *_RegExp_parser) error(fatal bool, msg string, msgValues ...interface{}) {
if self.err != nil {
return
}
e := regexpParseError{
offset: self.offset,
err: fmt.Sprintf(msg, msgValues...),
}
if fatal {
self.err = RegexpSyntaxError{e}
} else {
self.err = RegexpErrorIncompatible{e}
}
self.offset = self.length
self.chr = -1
}

View File

@@ -0,0 +1,191 @@
package parser
import (
"regexp"
"testing"
)
func TestRegExp(t *testing.T) {
tt(t, func() {
{
// err
test := func(input string, expect interface{}) {
_, err := TransformRegExp(input)
_, incompat := err.(RegexpErrorIncompatible)
is(incompat, false)
is(err, expect)
}
test("[", "Unterminated character class")
test("(", "Unterminated group")
test("\\(?=)", "Unmatched ')'")
test(")", "Unmatched ')'")
test("0:(?)", "Invalid group")
test("(?)", "Invalid group")
test("(?U)", "Invalid group")
test("(?)|(?i)", "Invalid group")
test("(?P<w>)(?P<w>)(?P<D>)", "Invalid group")
}
{
// incompatible
test := func(input string, expectErr interface{}) {
_, err := TransformRegExp(input)
_, incompat := err.(RegexpErrorIncompatible)
is(incompat, true)
is(err, expectErr)
}
test(`<%([\s\S]+?)%>`, "S in class")
test("(?<=y)x", "re2: Invalid (?<) <lookbehind>")
test(`(?!test)`, "re2: Invalid (?!) <lookahead>")
test(`\1`, "re2: Invalid \\1 <backreference>")
test(`\8`, "re2: Invalid \\8 <backreference>")
}
{
// err
test := func(input string, expect string) {
result, err := TransformRegExp(input)
is(err, nil)
_, incompat := err.(RegexpErrorIncompatible)
is(incompat, false)
is(result, expect)
_, err = regexp.Compile(result)
is(err, nil)
}
test("", "")
test("abc", "abc")
test(`\abc`, `abc`)
test(`\a\b\c`, `a\bc`)
test(`\x`, `x`)
test(`\c`, `c`)
test(`\cA`, `\x01`)
test(`\cz`, `\x1a`)
test(`\ca`, `\x01`)
test(`\cj`, `\x0a`)
test(`\ck`, `\x0b`)
test(`\+`, `\+`)
test(`[\b]`, `[\x08]`)
test(`\u0z01\x\undefined`, `u0z01xundefined`)
test(`\\|'|\r|\n|\t|\u2028|\u2029`, `\\|'|\r|\n|\t|\x{2028}|\x{2029}`)
test("]", "]")
test("}", "}")
test("%", "%")
test("(%)", "(%)")
test("(?:[%\\s])", "(?:[%"+WhitespaceChars+"])")
test("[[]", "[[]")
test("\\101", "\\x41")
test("\\51", "\\x29")
test("\\051", "\\x29")
test("\\175", "\\x7d")
test("\\0", "\\0")
test("\\04", "\\x04")
test(`(.)^`, "("+Re2Dot+")^")
test(`\$`, `\$`)
test(`[G-b]`, `[G-b]`)
test(`[G-b\0]`, `[G-b\0]`)
test(`\k`, `k`)
test(`\x20`, `\x20`)
test(`😊`, `😊`)
test(`^.*`, `^`+Re2Dot+`*`)
test(`(\n)`, `(\n)`)
test(`(a(bc))`, `(a(bc))`)
test(`[]`, "[^\u0000-\U0001FFFF]")
test(`[^]`, "[\u0000-\U0001FFFF]")
test(`\s+`, "["+WhitespaceChars+"]+")
test(`\S+`, "[^"+WhitespaceChars+"]+")
}
})
}
func TestTransformRegExp(t *testing.T) {
tt(t, func() {
pattern, err := TransformRegExp(`\s+abc\s+`)
is(err, nil)
is(pattern, `[`+WhitespaceChars+`]+abc[`+WhitespaceChars+`]+`)
is(regexp.MustCompile(pattern).MatchString("\t abc def"), true)
})
tt(t, func() {
pattern, err := TransformRegExp(`\u{1d306}`)
is(err, nil)
is(pattern, `\x{1d306}`)
})
tt(t, func() {
pattern, err := TransformRegExp(`\u1234`)
is(err, nil)
is(pattern, `\x{1234}`)
})
}
func BenchmarkTransformRegExp(b *testing.B) {
f := func(reStr string, b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = TransformRegExp(reStr)
}
}
b.Run("Re", func(b *testing.B) {
f(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, b)
})
b.Run("Re2-1", func(b *testing.B) {
f(`(?=)^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, b)
})
b.Run("Re2-1", func(b *testing.B) {
f(`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$(?=)`, b)
})
}

View File

@@ -0,0 +1,50 @@
package parser
import (
"github.com/dop251/goja/ast"
"github.com/dop251/goja/unistring"
)
type _scope struct {
outer *_scope
allowIn bool
allowLet bool
inIteration bool
inSwitch bool
inFuncParams bool
inFunction bool
inAsync bool
allowAwait bool
allowYield bool
declarationList []*ast.VariableDeclaration
labels []unistring.String
}
func (self *_parser) openScope() {
self.scope = &_scope{
outer: self.scope,
allowIn: true,
}
}
func (self *_parser) closeScope() {
self.scope = self.scope.outer
}
func (self *_scope) declare(declaration *ast.VariableDeclaration) {
self.declarationList = append(self.declarationList, declaration)
}
func (self *_scope) hasLabel(name unistring.String) bool {
for _, label := range self.labels {
if label == name {
return true
}
}
if self.outer != nil && !self.inFunction {
// Crossing a function boundary to look for a label is verboten
return self.outer.hasLabel(name)
}
return false
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
package parser
import (
"fmt"
"path/filepath"
"runtime"
"testing"
)
// Quick and dirty replacement for terst
func tt(t *testing.T, f func()) {
defer func() {
if x := recover(); x != nil {
pcs := make([]uintptr, 16)
pcs = pcs[:runtime.Callers(1, pcs)]
frames := runtime.CallersFrames(pcs)
var file string
var line int
for {
frame, more := frames.Next()
// The line number here must match the line where f() is called (see below)
if frame.Line == 40 && filepath.Base(frame.File) == "testutil_test.go" {
break
}
if !more {
break
}
file, line = frame.File, frame.Line
}
if line > 0 {
t.Errorf("Error at %s:%d: %v", filepath.Base(file), line, x)
} else {
t.Errorf("Error at <unknown>: %v", x)
}
}
}()
f()
}
func is(a, b interface{}) {
as := fmt.Sprintf("%v", a)
bs := fmt.Sprintf("%v", b)
if as != bs {
panic(fmt.Errorf("%+v(%T) != %+v(%T)", a, a, b, b))
}
}