Forráskód Böngészése

Introduced options for parser. Added options to set a custom source map loader or to disable source map support. See #235.

Dmitry Panov 4 éve
szülő
commit
10e5c75992
6 módosított fájl, 188 hozzáadás és 27 törlés
  1. 37 6
      parser/parser.go
  2. 41 0
      parser/parser_test.go
  3. 32 12
      parser/statement.go
  4. 22 2
      parser/testutil_test.go
  5. 27 7
      runtime.go
  6. 29 0
      runtime_test.go

+ 37 - 6
parser/parser.go

@@ -52,6 +52,32 @@ 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
@@ -79,18 +105,23 @@ type _parser struct {
 	}
 
 	mode Mode
+	opts options
 
 	file *file.File
 }
 
-func _newParser(filename, src string, base int) *_parser {
-	return &_parser{
+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 {
@@ -133,7 +164,7 @@ func ReadSource(filename string, src interface{}) ([]byte, error) {
 //      // 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) {
+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
@@ -146,7 +177,7 @@ func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mod
 			base = fileSet.AddFile(filename, str)
 		}
 
-		parser := _newParser(filename, str, base)
+		parser := _newParser(filename, str, base, options...)
 		parser.mode = mode
 		return parser.parse()
 	}
@@ -157,11 +188,11 @@ func ParseFile(fileSet *file.FileSet, filename string, src interface{}, mode Mod
 //
 // The parameter list, if any, should be a comma-separated list of identifiers.
 //
-func ParseFunction(parameterList, body string) (*ast.FunctionLiteral, error) {
+func ParseFunction(parameterList, body string, options ...Option) (*ast.FunctionLiteral, error) {
 
 	src := "(function(" + parameterList + ") {\n" + body + "\n})"
 
-	parser := _newParser("", src, 1)
+	parser := _newParser("", src, 1, options...)
 	program, err := parser.parse()
 	if err != nil {
 		return nil, err

+ 41 - 0
parser/parser_test.go

@@ -1031,3 +1031,44 @@ var x = {};
 		is(extractSourceMapLine(modSrc+"\n\n\n\n"), "//# sourceMappingURL=delme.js.map")
 	})
 }
+
+func TestSourceMapOptions(t *testing.T) {
+	tt(t, func() {
+		count := 0
+		requestedPath := ""
+		loader := func(p string) ([]byte, error) {
+			count++
+			requestedPath = p
+			return nil, nil
+		}
+		src := `"use strict";
+var x = {};
+//# sourceMappingURL=delme.js.map`
+		_, err := ParseFile(nil, "delme.js", src, 0, WithSourceMapLoader(loader))
+		is(err, nil)
+		is(count, 1)
+		is(requestedPath, "delme.js.map")
+
+		count = 0
+		_, err = ParseFile(nil, "", src, 0, WithSourceMapLoader(loader))
+		is(err, nil)
+		is(count, 1)
+		is(requestedPath, "delme.js.map")
+
+		count = 0
+		_, err = ParseFile(nil, "delme.js", src, 0, WithDisableSourceMaps)
+		is(err, nil)
+		is(count, 0)
+
+		_, err = ParseFile(nil, "/home/user/src/delme.js", src, 0, WithSourceMapLoader(loader))
+		is(err, nil)
+		is(count, 1)
+		is(requestedPath, "/home/user/src/delme.js.map")
+
+		count = 0
+		_, err = ParseFile(nil, "https://site.com/delme.js", src, 0, WithSourceMapLoader(loader))
+		is(err, nil)
+		is(count, 1)
+		is(requestedPath, "https://site.com/delme.js.map")
+	})
+}

+ 32 - 12
parser/statement.go

@@ -2,13 +2,14 @@ package parser
 
 import (
 	"encoding/base64"
+	"fmt"
 	"github.com/dop251/goja/ast"
 	"github.com/dop251/goja/file"
 	"github.com/dop251/goja/token"
 	"github.com/go-sourcemap/sourcemap"
 	"io/ioutil"
 	"net/url"
-	"os"
+	"path"
 	"strings"
 )
 
@@ -606,38 +607,57 @@ func extractSourceMapLine(str string) string {
 }
 
 func (self *_parser) parseSourceMap() *sourcemap.Consumer {
+	if self.opts.disableSourceMaps {
+		return nil
+	}
 	if smLine := extractSourceMapLine(self.str); smLine != "" {
 		urlIndex := strings.Index(smLine, "=")
 		urlStr := smLine[urlIndex+1:]
 
 		var data []byte
+		var err error
 		if strings.HasPrefix(urlStr, "data:application/json") {
 			b64Index := strings.Index(urlStr, ",")
 			b64 := urlStr[b64Index+1:]
-			if d, err := base64.StdEncoding.DecodeString(b64); err == nil {
-				data = d
-			}
+			data, err = base64.StdEncoding.DecodeString(b64)
 		} else {
-			if smUrl, err := url.Parse(urlStr); err == nil {
-				if smUrl.Scheme == "" || smUrl.Scheme == "file" {
-					if f, err := os.Open(smUrl.Path); err == nil {
-						if d, err := ioutil.ReadAll(f); err == nil {
-							data = d
-						}
+			var smUrl *url.URL
+			if smUrl, err = url.Parse(urlStr); err == nil {
+				p := smUrl.Path
+				if !strings.HasPrefix(p, "/") {
+					baseName := self.file.Name()
+					baseUrl, err1 := url.Parse(baseName)
+					if err1 == nil && baseUrl.Scheme != "" {
+						baseUrl.Path = path.Join(path.Dir(baseUrl.Path), p)
+						p = baseUrl.String()
+					} else {
+						p = path.Join(path.Dir(baseName), p)
 					}
+				}
+				if self.opts.sourceMapLoader != nil {
+					data, err = self.opts.sourceMapLoader(p)
 				} else {
-					// Not implemented - compile error?
-					return nil
+					if smUrl.Scheme == "" || smUrl.Scheme == "file" {
+						data, err = ioutil.ReadFile(p)
+					} else {
+						err = fmt.Errorf("unsupported source map URL scheme: %s", smUrl.Scheme)
+					}
 				}
 			}
 		}
 
+		if err != nil {
+			self.error(file.Idx(0), "Could not load source map: %v", err)
+			return nil
+		}
 		if data == nil {
 			return nil
 		}
 
 		if sm, err := sourcemap.Parse(self.file.Name(), data); err == nil {
 			return sm
+		} else {
+			self.error(file.Idx(0), "Could not parse source map: %v", err)
 		}
 	}
 	return nil

+ 22 - 2
parser/testutil_test.go

@@ -12,8 +12,28 @@ import (
 func tt(t *testing.T, f func()) {
 	defer func() {
 		if x := recover(); x != nil {
-			_, file, line, _ := runtime.Caller(4)
-			t.Errorf("Error at %s:%d: %v", filepath.Base(file), line, x)
+			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)
+			}
 		}
 	}()
 

+ 27 - 7
runtime.go

@@ -161,6 +161,7 @@ type Runtime struct {
 	rand            RandSource
 	now             Now
 	_collator       *collate.Collator
+	parserOptions   []parser.Option
 
 	symbolRegistry map[unistring.String]*Symbol
 
@@ -1111,8 +1112,17 @@ func MustCompile(name, src string, strict bool) *Program {
 	return prg
 }
 
-func compile(name, src string, strict, eval bool) (p *Program, err error) {
-	prg, err1 := parser.ParseFile(nil, name, src, 0)
+// Parse takes a source string and produces a parsed AST. Use this function if you want to pass options
+// to the parser, e.g.:
+//
+//  p, err := Parse("test.js", "var a = true", parser.WithDisableSourceMaps)
+//  if err != nil { /* ... */ }
+//  prg, err := CompileAST(p, true)
+//  // ...
+//
+// Otherwise use Compile which combines both steps.
+func Parse(name, src string, options ...parser.Option) (prg *js_ast.Program, err error) {
+	prg, err1 := parser.ParseFile(nil, name, src, 0, options...)
 	if err1 != nil {
 		switch err1 := err1.(type) {
 		case parser.ErrorList:
@@ -1131,12 +1141,17 @@ func compile(name, src string, strict, eval bool) (p *Program, err error) {
 				Message: err1.Error(),
 			},
 		}
-		return
 	}
+	return
+}
 
-	p, err = compileAST(prg, strict, eval)
+func compile(name, src string, strict, eval bool, parserOptions ...parser.Option) (p *Program, err error) {
+	prg, err := Parse(name, src, parserOptions...)
+	if err != nil {
+		return
+	}
 
-	return
+	return compileAST(prg, strict, eval)
 }
 
 func compileAST(prg *js_ast.Program, strict, eval bool) (p *Program, err error) {
@@ -1162,7 +1177,7 @@ func compileAST(prg *js_ast.Program, strict, eval bool) (p *Program, err error)
 }
 
 func (r *Runtime) compile(name, src string, strict, eval bool) (p *Program, err error) {
-	p, err = compile(name, src, strict, eval)
+	p, err = compile(name, src, strict, eval, r.parserOptions...)
 	if err != nil {
 		switch x1 := err.(type) {
 		case *CompilerSyntaxError:
@@ -1185,7 +1200,7 @@ func (r *Runtime) RunString(str string) (Value, error) {
 
 // RunScript executes the given string in the global context.
 func (r *Runtime) RunScript(name, src string) (Value, error) {
-	p, err := Compile(name, src, false)
+	p, err := r.compile(name, src, false, false)
 
 	if err != nil {
 		return nil, err
@@ -1984,6 +1999,11 @@ func (r *Runtime) SetTimeSource(now Now) {
 	r.now = now
 }
 
+// SetParserOptions sets parser options to be used by RunString, RunScript and eval() within the code.
+func (r *Runtime) SetParserOptions(opts ...parser.Option) {
+	r.parserOptions = opts
+}
+
 // New is an equivalent of the 'new' operator allowing to call it directly from Go.
 func (r *Runtime) New(construct Value, args ...Value) (o *Object, err error) {
 	err = tryFunc(func() {

+ 29 - 0
runtime_test.go

@@ -9,6 +9,8 @@ import (
 	"strconv"
 	"testing"
 	"time"
+
+	"github.com/dop251/goja/parser"
 )
 
 func TestGlobalObjectProto(t *testing.T) {
@@ -1819,6 +1821,33 @@ func ExampleRuntime_NewArray() {
 	// Output: 1 2 true
 }
 
+func ExampleRuntime_SetParserOptions() {
+	vm := New()
+	vm.SetParserOptions(parser.WithDisableSourceMaps)
+
+	res, err := vm.RunString(`
+	"I did not hang!";
+//# sourceMappingURL=/dev/zero`)
+
+	if err != nil {
+		panic(err)
+	}
+	fmt.Println(res.String())
+	// Output: I did not hang!
+}
+
+func TestRuntime_SetParserOptions_Eval(t *testing.T) {
+	vm := New()
+	vm.SetParserOptions(parser.WithDisableSourceMaps)
+
+	_, err := vm.RunString(`
+	eval("//# sourceMappingURL=/dev/zero");
+	`)
+	if err != nil {
+		t.Fatal(err)
+	}
+}
+
 /*
 func TestArrayConcatSparse(t *testing.T) {
 function foo(a,b,c)