345 lines
8.2 KiB
Go
345 lines
8.2 KiB
Go
|
/*
|
||
|
Package parser implements a parser for JavaScript.
|
||
|
|
||
|
import (
|
||
|
"github.com/robertkrimen/otto/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"
|
||
|
"encoding/base64"
|
||
|
"errors"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
|
||
|
"github.com/robertkrimen/otto/ast"
|
||
|
"github.com/robertkrimen/otto/file"
|
||
|
"github.com/robertkrimen/otto/token"
|
||
|
"gopkg.in/sourcemap.v1"
|
||
|
)
|
||
|
|
||
|
// 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)
|
||
|
StoreComments // Store the comments from source to the comments map
|
||
|
)
|
||
|
|
||
|
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
|
||
|
|
||
|
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
|
||
|
|
||
|
file *file.File
|
||
|
|
||
|
comments *ast.Comments
|
||
|
}
|
||
|
|
||
|
type Parser interface {
|
||
|
Scan() (tkn token.Token, literal string, idx file.Idx)
|
||
|
}
|
||
|
|
||
|
func _newParser(filename, src string, base int, sm *sourcemap.Consumer) *_parser {
|
||
|
return &_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).WithSourceMap(sm),
|
||
|
comments: ast.NewComments(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Returns a new Parser.
|
||
|
func NewParser(filename, src string) Parser {
|
||
|
return _newParser(filename, src, 1, nil)
|
||
|
}
|
||
|
|
||
|
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 ioutil.ReadFile(filename)
|
||
|
}
|
||
|
|
||
|
func ReadSourceMap(filename string, src interface{}) (*sourcemap.Consumer, error) {
|
||
|
if src == nil {
|
||
|
return nil, nil
|
||
|
}
|
||
|
|
||
|
switch src := src.(type) {
|
||
|
case string:
|
||
|
return sourcemap.Parse(filename, []byte(src))
|
||
|
case []byte:
|
||
|
return sourcemap.Parse(filename, src)
|
||
|
case *bytes.Buffer:
|
||
|
if src != nil {
|
||
|
return sourcemap.Parse(filename, src.Bytes())
|
||
|
}
|
||
|
case io.Reader:
|
||
|
var bfr bytes.Buffer
|
||
|
if _, err := io.Copy(&bfr, src); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return sourcemap.Parse(filename, bfr.Bytes())
|
||
|
case *sourcemap.Consumer:
|
||
|
return src, nil
|
||
|
}
|
||
|
|
||
|
return nil, errors.New("invalid sourcemap type")
|
||
|
}
|
||
|
|
||
|
func ParseFileWithSourceMap(fileSet *file.FileSet, filename string, javascriptSource, sourcemapSource interface{}, mode Mode) (*ast.Program, error) {
|
||
|
src, err := ReadSource(filename, javascriptSource)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if sourcemapSource == nil {
|
||
|
lines := bytes.Split(src, []byte("\n"))
|
||
|
lastLine := lines[len(lines)-1]
|
||
|
if bytes.HasPrefix(lastLine, []byte("//# sourceMappingURL=data:application/json")) {
|
||
|
bits := bytes.SplitN(lastLine, []byte(","), 2)
|
||
|
if len(bits) == 2 {
|
||
|
if d, err := base64.StdEncoding.DecodeString(string(bits[1])); err == nil {
|
||
|
sourcemapSource = d
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sm, err := ReadSourceMap(filename, sourcemapSource)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
base := 1
|
||
|
if fileSet != nil {
|
||
|
base = fileSet.AddFile(filename, string(src))
|
||
|
}
|
||
|
|
||
|
parser := _newParser(filename, string(src), base, sm)
|
||
|
parser.mode = mode
|
||
|
program, err := parser.parse()
|
||
|
program.Comments = parser.comments.CommentMap
|
||
|
|
||
|
return program, err
|
||
|
}
|
||
|
|
||
|
// 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) (*ast.Program, error) {
|
||
|
return ParseFileWithSourceMap(fileSet, filename, src, nil, mode)
|
||
|
}
|
||
|
|
||
|
// 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) (*ast.FunctionLiteral, error) {
|
||
|
|
||
|
src := "(function(" + parameterList + ") {\n" + body + "\n})"
|
||
|
|
||
|
parser := _newParser("", src, 1, nil)
|
||
|
program, err := parser.parse()
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return program.Body[0].(*ast.ExpressionStatement).Expression.(*ast.FunctionLiteral), nil
|
||
|
}
|
||
|
|
||
|
// Scan reads a single token from the source at the current offset, increments the offset and
|
||
|
// returns the token.Token token, a string literal representing the value of the token (if applicable)
|
||
|
// and it's current file.Idx index.
|
||
|
func (self *_parser) Scan() (tkn token.Token, literal string, idx file.Idx) {
|
||
|
return self.scan()
|
||
|
}
|
||
|
|
||
|
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.next()
|
||
|
program := self.parseProgram()
|
||
|
if false {
|
||
|
self.errors.Sort()
|
||
|
}
|
||
|
|
||
|
if self.mode&StoreComments != 0 {
|
||
|
self.comments.CommentMap.AddComments(program, self.comments.FetchAll(), ast.TRAILING)
|
||
|
}
|
||
|
|
||
|
return program, self.errors.Err()
|
||
|
}
|
||
|
|
||
|
func (self *_parser) next() {
|
||
|
self.token, self.literal, 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 lineCount(str string) (int, int) {
|
||
|
line, last := 0, -1
|
||
|
pair := false
|
||
|
for index, chr := range str {
|
||
|
switch chr {
|
||
|
case '\r':
|
||
|
line += 1
|
||
|
last = index
|
||
|
pair = true
|
||
|
continue
|
||
|
case '\n':
|
||
|
if !pair {
|
||
|
line += 1
|
||
|
}
|
||
|
last = index
|
||
|
case '\u2028', '\u2029':
|
||
|
line += 1
|
||
|
last = index + 2
|
||
|
}
|
||
|
pair = false
|
||
|
}
|
||
|
return line, last
|
||
|
}
|
||
|
|
||
|
func (self *_parser) position(idx file.Idx) file.Position {
|
||
|
position := file.Position{}
|
||
|
offset := int(idx) - self.base
|
||
|
str := self.str[:offset]
|
||
|
position.Filename = self.file.Name()
|
||
|
line, last := lineCount(str)
|
||
|
position.Line = 1 + line
|
||
|
if last >= 0 {
|
||
|
position.Column = offset - last
|
||
|
} else {
|
||
|
position.Column = 1 + len(str)
|
||
|
}
|
||
|
|
||
|
return position
|
||
|
}
|