Skip to content

Commit 45afb3e

Browse files
authored
Merge pull request #9 from x1unix/feat/go-report-error
Feature: report Go code error
2 parents 71c363e + e2ac284 commit 45afb3e

File tree

16 files changed

+570
-8
lines changed

16 files changed

+570
-8
lines changed

build.mk

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
TOOLS ?= ./tools
2+
PUBLIC_DIR ?= $(UI)/public
3+
WEBWORKER_PKG ?= ./cmd/webworker
24

35
.PHONY: clean
46
clean:
@@ -25,8 +27,14 @@ build-ui:
2527
@echo "- Building UI..."
2628
cd $(UI) && yarn build
2729

30+
.PHONY:build-webworker
31+
build-webworker:
32+
@echo "Building Go Webworker module..." && \
33+
GOOS=js GOARCH=wasm go build -o $(PUBLIC_DIR)/worker.wasm $(WEBWORKER_PKG) && \
34+
cp "$$(go env GOROOT)/misc/wasm/wasm_exec.js" $(PUBLIC_DIR)
35+
2836
.PHONY: build
29-
build: clean preinstall collect-meta build-server build-ui
37+
build: clean preinstall collect-meta build-server build-webworker build-ui
3038
@echo "- Copying assets..."
3139
cp -rf ./data $(TARGET)/data
3240
mv $(UI)/build $(TARGET)/public

build/Dockerfile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
FROM node:13-alpine as ui-build
22
COPY web /tmp/web
33
WORKDIR /tmp/web
4-
ARG APP_VERSION="1.0.0"
5-
ARG GITHUB_URL="https://github.com/x1unix/go-playground"
4+
ARG APP_VERSION=1.0.0
5+
ARG GITHUB_URL=https://github.com/x1unix/go-playground
66
RUN yarn install --silent && REACT_APP_VERSION=$APP_VERSION REACT_APP_GITHUB_URL=$GITHUB_URL yarn build
77

88
FROM golang:1.13-alpine as build
@@ -11,7 +11,9 @@ COPY cmd ./cmd
1111
COPY pkg ./pkg
1212
COPY go.mod .
1313
COPY go.sum .
14-
RUN go build -o server ./cmd/playground
14+
RUN go build -o server ./cmd/playground && \
15+
GOOS=js GOARCH=wasm go build -o ./worker.wasm ./cmd/webworker && \
16+
cp $(go env GOROOT)/misc/wasm/wasm_exec.js .
1517

1618
FROM golang:1.13-alpine as production
1719
WORKDIR /opt/playground
@@ -21,5 +23,7 @@ ENV APP_DEBUG=false
2123
COPY data ./data
2224
COPY --from=ui-build /tmp/web/build ./public
2325
COPY --from=build /tmp/playground/server .
26+
COPY --from=build /tmp/playground/worker.wasm ./public
27+
COPY --from=build /tmp/playground/wasm_exec.js ./public
2428
EXPOSE 8000
2529
ENTRYPOINT /opt/playground/server -f=/opt/playground/data/packages.json -addr=:8000 -clean-interval=${APP_CLEAN_INTERVAL} -debug=${APP_DEBUG}

cmd/webworker/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# WebWorker
2+
3+
`webworker` binary exports collection of tools used by UI web worker to analyze Go code and report
4+
code errors.
5+
6+
Package should be compiled as **WebAssembly** binary and loaded by JS WebWorker.

cmd/webworker/webworker.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"syscall/js"
7+
8+
"github.com/x1unix/go-playground/pkg/analyzer/check"
9+
10+
"github.com/x1unix/go-playground/pkg/worker"
11+
)
12+
13+
type void = struct{}
14+
15+
var (
16+
done = make(chan void, 0)
17+
onResult js.Value
18+
)
19+
20+
func main() {
21+
entrypoint, err := getEntrypointFunction()
22+
if err != nil {
23+
panic(err)
24+
}
25+
26+
// prepare exports object
27+
analyzeCodeCb := worker.FuncOf(analyzeCode)
28+
exitCb := js.FuncOf(exit)
29+
module := map[string]interface{}{
30+
"analyzeCode": analyzeCodeCb,
31+
"exit": exitCb,
32+
}
33+
34+
defer analyzeCodeCb.Release()
35+
defer exitCb.Release()
36+
37+
entrypoint.Invoke(js.ValueOf(module))
38+
<-done
39+
fmt.Println("Go: exit")
40+
}
41+
42+
func getEntrypointFunction() (js.Value, error) {
43+
if len(os.Args) < 2 {
44+
return js.Value{}, fmt.Errorf("WASM module requires at least 2 arguments: 'js' and entrypoint function name")
45+
}
46+
47+
entrypointName := os.Args[1]
48+
entrypoint := js.Global().Get(entrypointName)
49+
switch t := entrypoint.Type(); t {
50+
case js.TypeFunction:
51+
return entrypoint, nil
52+
case js.TypeUndefined:
53+
return js.Value{}, fmt.Errorf("function %q doesn't exists on global JS scope", entrypointName)
54+
default:
55+
return js.Value{}, fmt.Errorf("%q should be callable JS function, but got %d instead", entrypointName, t)
56+
}
57+
}
58+
59+
func exit(this js.Value, args []js.Value) interface{} {
60+
go func() {
61+
done <- void{}
62+
}()
63+
return nil
64+
}
65+
66+
func analyzeCode(this js.Value, args worker.Args) (interface{}, error) {
67+
var code string
68+
if err := args.Bind(&code); err != nil {
69+
return nil, err
70+
}
71+
72+
return check.Check(code)
73+
}

pkg/analyzer/check/check.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package check
2+
3+
import (
4+
"go/parser"
5+
"go/scanner"
6+
"go/token"
7+
)
8+
9+
// Check checks Go code and returns check result
10+
func Check(src string) (*Result, error) {
11+
fset := token.NewFileSet()
12+
_, err := parser.ParseFile(fset, "main.go", src, parser.DeclarationErrors)
13+
if err == nil {
14+
return &Result{HasErrors: false}, nil
15+
}
16+
17+
if errList, ok := err.(scanner.ErrorList); ok {
18+
return &Result{HasErrors: true, Markers: errorsListToMarkers(errList)}, nil
19+
}
20+
21+
return nil, err
22+
}

pkg/analyzer/check/marker.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// package check checks provided Go code and reports syntax errors
2+
package check
3+
4+
import "go/scanner"
5+
6+
// MarkerSeverity is equivalent for MarkerSeverity type in monaco-editor
7+
type MarkerSeverity = int
8+
9+
const (
10+
Hint = MarkerSeverity(1)
11+
Info = MarkerSeverity(2)
12+
Warning = MarkerSeverity(3)
13+
Error = MarkerSeverity(8)
14+
)
15+
16+
// MarkerData is a structure defining a problem/warning/etc.
17+
// Equivalent to IMarkerData in 'monaco-editor'
18+
type MarkerData struct {
19+
Severity MarkerSeverity `json:"severity"`
20+
StartLineNumber int `json:"startLineNumber"`
21+
StartColumn int `json:"startColumn"`
22+
EndLineNumber int `json:"endLineNumber"`
23+
EndColumn int `json:"endColumn"`
24+
Message string `json:"message"`
25+
}
26+
27+
func errorsListToMarkers(errList scanner.ErrorList) []MarkerData {
28+
markers := make([]MarkerData, 0, len(errList))
29+
for _, err := range errList {
30+
markers = append(markers, MarkerData{
31+
Severity: Error,
32+
Message: err.Msg,
33+
StartLineNumber: err.Pos.Line,
34+
EndLineNumber: err.Pos.Line,
35+
StartColumn: err.Pos.Column - 1,
36+
EndColumn: err.Pos.Column,
37+
})
38+
}
39+
40+
return markers
41+
}
42+
43+
type Result struct {
44+
HasErrors bool `json:"hasErrors"`
45+
Markers []MarkerData `json:"markers"`
46+
}

pkg/worker/args.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package worker
2+
3+
import (
4+
"fmt"
5+
"syscall/js"
6+
)
7+
8+
// NewTypeError creates a new type error
9+
func NewTypeError(expType, gotType js.Type) error {
10+
return fmt.Errorf("value type should be %q, but got %q", expType, gotType)
11+
}
12+
13+
type ValueUnmarshaler interface {
14+
UnmarshalValue(js.Value) error
15+
}
16+
17+
// Args is collection if function call arguments
18+
type Args []js.Value
19+
20+
// BindIndex binds argument at specified index to passed value
21+
func (args Args) BindIndex(index int, dest interface{}) error {
22+
if len(args) <= index {
23+
return fmt.Errorf("function expects %d arguments, but %d were passed", index+1, len(args))
24+
}
25+
26+
return BindValue(args[index], dest)
27+
}
28+
29+
// Bind binds passed JS arguments to Go values
30+
//
31+
// Function supports *int, *bool, *string and ValueUnmarshaler values.
32+
func (args Args) Bind(targets ...interface{}) error {
33+
if len(args) != len(targets) {
34+
return fmt.Errorf("function expects %d arguments, but %d were passed", len(targets), len(args))
35+
}
36+
37+
for i, arg := range args {
38+
if err := BindValue(arg, targets[i]); err != nil {
39+
return fmt.Errorf("invalid argument %d type: %s", err)
40+
}
41+
}
42+
43+
return nil
44+
}
45+
46+
// BindValue binds JS value to specified target
47+
func BindValue(val js.Value, dest interface{}) error {
48+
valType := val.Type()
49+
switch v := dest.(type) {
50+
case *int:
51+
if valType != js.TypeNumber {
52+
return NewTypeError(js.TypeNumber, valType)
53+
}
54+
55+
*v = val.Int()
56+
case *bool:
57+
if valType != js.TypeBoolean {
58+
return NewTypeError(js.TypeBoolean, valType)
59+
}
60+
61+
*v = val.Bool()
62+
case *string:
63+
if valType != js.TypeString {
64+
return NewTypeError(js.TypeString, valType)
65+
}
66+
67+
*v = val.String()
68+
case ValueUnmarshaler:
69+
return v.UnmarshalValue(val)
70+
default:
71+
return fmt.Errorf("BindValue: unsupported JS type %q", valType)
72+
}
73+
74+
return nil
75+
}

pkg/worker/callback.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package worker
2+
3+
import "syscall/js"
4+
5+
// Callback is async function callback
6+
type Callback = func(interface{}, error)
7+
8+
func newCallbackFromValue(val js.Value) (Callback, error) {
9+
if typ := val.Type(); typ != js.TypeFunction {
10+
return nil, NewTypeError(js.TypeFunction, typ)
11+
}
12+
13+
return func(result interface{}, err error) {
14+
if err != nil {
15+
val.Invoke(js.ValueOf(NewErrorResponse(err).JSON()))
16+
}
17+
18+
if result == nil {
19+
val.Invoke()
20+
return
21+
}
22+
23+
val.Invoke(js.ValueOf(NewResponse(result, nil).JSON()))
24+
}, nil
25+
}

pkg/worker/response.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package worker
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
type Response struct {
9+
Error string `json:"error,omitempty"`
10+
Result interface{} `json:"result,omitempty"`
11+
}
12+
13+
func (r Response) JSON() string {
14+
data, err := json.Marshal(r)
15+
if err != nil {
16+
// Return manual JSON in case of error
17+
return fmt.Sprintf(`{"error": %q}`, err)
18+
}
19+
20+
return string(data)
21+
}
22+
23+
func NewErrorResponse(err error) Response {
24+
return Response{Error: err.Error()}
25+
}
26+
27+
func NewResponse(result interface{}, err error) Response {
28+
if err != nil {
29+
return Response{Error: err.Error()}
30+
}
31+
32+
return Response{Result: result}
33+
}

pkg/worker/worker.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Package worker contains Go web-worker WASM module bridge methods
2+
package worker
3+
4+
import (
5+
"fmt"
6+
"syscall/js"
7+
)
8+
9+
// Func is worker handler function
10+
type Func = func(this js.Value, args Args) (interface{}, error)
11+
12+
// ParseArgs parses async call arguments.
13+
//
14+
// Function expects the last argument to be a callable JS function
15+
func ParseArgs(allArgs []js.Value) (Args, Callback, error) {
16+
argLen := len(allArgs)
17+
if argLen == 0 {
18+
return nil, nil, fmt.Errorf("function requires at least 1 argument, but only 0 were passed")
19+
}
20+
21+
lastIndex := len(allArgs) - 1
22+
cb, err := newCallbackFromValue(allArgs[lastIndex:][0])
23+
if err != nil {
24+
return nil, nil, fmt.Errorf("last function argument should be callable (%s)", err)
25+
}
26+
27+
return allArgs[:lastIndex], cb, nil
28+
}
29+
30+
func callFunc(fn Func, this js.Value, jsArgs []js.Value) {
31+
args, callback, err := ParseArgs(jsArgs)
32+
if err != nil {
33+
js.Global().Get("console").Call("error", fmt.Sprintf("go worker: %s", err))
34+
panic(err)
35+
}
36+
37+
callback(fn(this, args))
38+
}
39+
40+
// FuncOf wraps function into js-compatible async function with callback
41+
func FuncOf(fn Func) js.Func {
42+
return js.FuncOf(func(this js.Value, args []js.Value) interface{} {
43+
go callFunc(fn, this, args)
44+
return nil
45+
})
46+
}

0 commit comments

Comments
 (0)