playground: add vet

Playground runs a program through the "go vet" command and
displays found errors before the output of a program.
Vet check is performed for successfully compiled program only.
Vet errors are highlighted in the code editor.

Depends on the CL 107455.

Fixes golang/go#7597
Fixes golang/go#24576

Change-Id: I316a4a66e701f2a179aeafc82efc8fe31a543c11
Reviewed-on: https://go-review.googlesource.com/100776
Reviewed-by: Bryan C. Mills <bcmills@google.com>
Reviewed-by: Andrew Bonventre <andybons@golang.org>
diff --git a/edit.html b/edit.html
index 99fabd4..f3cd3be 100644
--- a/edit.html
+++ b/edit.html
@@ -30,7 +30,8 @@
 				'shareURLEl':   '#shareURL',
 				{{end}}
 				'enableHistory': true,
-				'enableShortcuts': true
+				'enableShortcuts': true,
+				'enableVet': true
 			});
 			playgroundEmbed({
 				'codeEl':       '#code',
diff --git a/sandbox.go b/sandbox.go
index 97d9b23..84c4b25 100644
--- a/sandbox.go
+++ b/sandbox.go
@@ -62,7 +62,7 @@
 	}
 
 	resp := &response{}
-	key := cacheKey(req.Body)
+	key := cacheKey("prog", req.Body)
 	if err := s.cache.Get(key, resp); err != nil {
 		if err != memcache.ErrCacheMiss {
 			s.log.Errorf("s.cache.Get(%q, &response): %v", key, err)
@@ -89,10 +89,10 @@
 	}
 }
 
-func cacheKey(body string) string {
+func cacheKey(prefix, body string) string {
 	h := sha256.New()
 	io.WriteString(h, body)
-	return fmt.Sprintf("prog-%s-%x", runtime.Version(), h.Sum(nil))
+	return fmt.Sprintf("%s-%s-%x", prefix, runtime.Version(), h.Sum(nil))
 }
 
 // isTestFunc tells whether fn has the type of a testing function.
diff --git a/server.go b/server.go
index 2f6dab1..7035d1d 100644
--- a/server.go
+++ b/server.go
@@ -50,6 +50,7 @@
 func (s *server) init() {
 	s.mux.HandleFunc("/", s.handleEdit)
 	s.mux.HandleFunc("/fmt", handleFmt)
+	s.mux.HandleFunc("/vet", s.handleVet)
 	s.mux.HandleFunc("/share", s.handleShare)
 	s.mux.HandleFunc("/compile", s.handleCompile)
 	s.mux.HandleFunc("/playground.js", s.handlePlaygroundJS)
diff --git a/vet.go b/vet.go
new file mode 100644
index 0000000..1dfa215
--- /dev/null
+++ b/vet.go
@@ -0,0 +1,101 @@
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+
+	"github.com/bradfitz/gomemcache/memcache"
+)
+
+// handleVet performs a vet check on source code, trying cache for results first.
+// It serves an empty response if no errors were found by the "vet" tool.
+func (s *server) handleVet(w http.ResponseWriter, r *http.Request) {
+	// TODO(ysmolsky): refactor common code in this function and handleCompile.
+	// See golang.org/issue/24535.
+	var req request
+	// Until programs that depend on golang.org/x/tools/godoc/static/playground.js
+	// are updated to always send JSON, this check is in place.
+	if b := r.FormValue("body"); b != "" {
+		req.Body = b
+	} else if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		s.log.Errorf("error decoding request: %v", err)
+		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
+		return
+	}
+
+	resp := &response{}
+	key := cacheKey("vet", req.Body)
+	if err := s.cache.Get(key, resp); err != nil {
+		if err != memcache.ErrCacheMiss {
+			s.log.Errorf("s.cache.Get(%q, &response): %v", key, err)
+		}
+		resp, err = s.vetCheck(&req)
+		if err != nil {
+			s.log.Errorf("error checking vet: %v", err)
+			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+			return
+		}
+		if err := s.cache.Set(key, resp); err != nil {
+			s.log.Errorf("cache.Set(%q, resp): %v", key, err)
+		}
+	}
+
+	var buf bytes.Buffer
+	if err := json.NewEncoder(&buf).Encode(resp); err != nil {
+		s.log.Errorf("error encoding response: %v", err)
+		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+		return
+	}
+	if _, err := io.Copy(w, &buf); err != nil {
+		s.log.Errorf("io.Copy(w, &buf): %v", err)
+		return
+	}
+}
+
+// vetCheck runs the "vet" tool on the source code in req.Body.
+// In case of no errors it returns an empty, non-nil *response.
+// Otherwise &response.Errors contains found errors.
+func (s *server) vetCheck(req *request) (*response, error) {
+	tmpDir, err := ioutil.TempDir("", "vet")
+	if err != nil {
+		return nil, fmt.Errorf("error creating temp directory: %v", err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	in := filepath.Join(tmpDir, "main.go")
+	if err := ioutil.WriteFile(in, []byte(req.Body), 0400); err != nil {
+		return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
+	}
+
+	cmd := exec.Command("go", "vet", in)
+	out, err := cmd.CombinedOutput()
+	if err == nil {
+		return &response{}, nil
+	}
+
+	if _, ok := err.(*exec.ExitError); !ok {
+		return nil, fmt.Errorf("error vetting go source: %v", err)
+	}
+
+	// Rewrite compiler errors to refer to progName
+	// instead of '/tmp/sandbox1234/main.go'.
+	errs := strings.Replace(string(out), in, progName, -1)
+
+	// "go vet", invoked with a file name, puts this odd
+	// message before any compile errors; strip it.
+	errs = strings.Replace(errs, "# command-line-arguments\n", "", 1)
+
+	return &response{Errors: errs}, nil
+}