playground: support multiple input files in txtar format
Updates golang/go#32040
Updates golang/go#31944 (Notably, you can now include a go.mod file)
Change-Id: I56846e86d3d98fdf4cac388b5b284dbc187e3b36
Reviewed-on: https://go-review.googlesource.com/c/playground/+/177043
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
diff --git a/Dockerfile b/Dockerfile
index e6295da..27adf06 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -41,12 +41,17 @@
ENV GOCACHE /gocache
ENV GO111MODULE on
+COPY go.mod /go/src/playground/go.mod
+COPY go.sum /go/src/playground/go.sum
+WORKDIR /go/src/playground
+
# Pre-build some packages to speed final install later.
RUN go install cloud.google.com/go/compute/metadata
RUN go install cloud.google.com/go/datastore
RUN go install github.com/bradfitz/gomemcache/memcache
RUN go install golang.org/x/tools/godoc/static
RUN go install golang.org/x/tools/imports
+RUN go install github.com/rogpeppe/go-internal/txtar
# Add and compile playground daemon
COPY . /go/src/playground/
diff --git a/fmt.go b/fmt.go
index 09f50f2..c5aa943 100644
--- a/fmt.go
+++ b/fmt.go
@@ -20,25 +20,40 @@
}
func handleFmt(w http.ResponseWriter, r *http.Request) {
- var (
- in = []byte(r.FormValue("body"))
- out []byte
- err error
- )
- if r.FormValue("imports") != "" {
- out, err = imports.Process(progName, in, nil)
- } else {
- out, err = format.Source(in)
- }
- var resp fmtResponse
+ w.Header().Set("Content-Type", "application/json")
+
+ fs, err := splitFiles([]byte(r.FormValue("body")))
if err != nil {
- resp.Error = err.Error()
- // Prefix the error returned by format.Source.
- if !strings.HasPrefix(resp.Error, progName) {
- resp.Error = fmt.Sprintf("%v:%v", progName, resp.Error)
- }
- } else {
- resp.Body = string(out)
+ json.NewEncoder(w).Encode(fmtResponse{Error: err.Error()})
+ return
}
- json.NewEncoder(w).Encode(resp)
+
+ fixImports := r.FormValue("imports") != ""
+ for _, f := range fs.files {
+ if !strings.HasSuffix(f, ".go") {
+ continue
+ }
+ var out []byte
+ var err error
+ in := fs.m[f]
+ if fixImports {
+ // TODO: pass options to imports.Process so it
+ // can find symbols in sibling files.
+ out, err = imports.Process(progName, in, nil)
+ } else {
+ out, err = format.Source(in)
+ }
+ if err != nil {
+ errMsg := err.Error()
+ // Prefix the error returned by format.Source.
+ if !strings.HasPrefix(errMsg, f) {
+ errMsg = fmt.Sprintf("%v:%v", f, errMsg)
+ }
+ json.NewEncoder(w).Encode(fmtResponse{Error: errMsg})
+ return
+ }
+ fs.AddFile(f, out)
+ }
+
+ json.NewEncoder(w).Encode(fmtResponse{Body: string(fs.Format())})
}
diff --git a/fmt_test.go b/fmt_test.go
new file mode 100644
index 0000000..b8a1b9a
--- /dev/null
+++ b/fmt_test.go
@@ -0,0 +1,84 @@
+// Copyright 2019 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 (
+ "encoding/json"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+)
+
+func TestHandleFmt(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ body string
+ imports bool
+ want string
+ wantErr string
+ }{
+ {
+ name: "classic",
+ body: " package main\n func main( ) { }\n",
+ want: "package main\n\nfunc main() {}\n",
+ },
+ {
+ name: "classic_goimports",
+ body: " package main\nvar _ = fmt.Printf",
+ imports: true,
+ want: "package main\n\nimport \"fmt\"\n\nvar _ = fmt.Printf\n",
+ },
+ {
+ name: "single_go_with_header",
+ body: "-- prog.go --\n package main",
+ want: "-- prog.go --\npackage main\n",
+ },
+ {
+ name: "multi_go_with_header",
+ body: "-- prog.go --\n package main\n\n\n-- two.go --\n package main\n var X = 5",
+ want: "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n",
+ },
+ {
+ name: "multi_go_without_header",
+ body: " package main\n\n\n-- two.go --\n package main\n var X = 5",
+ want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n",
+ },
+ {
+ name: "only_format_go",
+ body: " package main\n\n\n-- go.mod --\n module foo\n",
+ want: "package main\n-- go.mod --\n module foo\n",
+ },
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ rec := httptest.NewRecorder()
+ form := url.Values{}
+ form.Set("body", tt.body)
+ if tt.imports {
+ form.Set("imports", "true")
+ }
+ req := httptest.NewRequest("POST", "/fmt", strings.NewReader(form.Encode()))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ handleFmt(rec, req)
+ resp := rec.Result()
+ if resp.StatusCode != 200 {
+ t.Fatalf("code = %v", resp.Status)
+ }
+ if ct := resp.Header.Get("Content-Type"); ct != "application/json" {
+ t.Fatalf("Content-Type = %q; want application/json", ct)
+ }
+ var got fmtResponse
+ if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
+ t.Fatal(err)
+ }
+ if got.Body != tt.want {
+ t.Errorf("wrong output\n got: %q\nwant: %q\n", got.Body, tt.want)
+ }
+ if got.Error != tt.wantErr {
+ t.Errorf("wrong error\n got err: %q\nwant err: %q\n", got.Error, tt.wantErr)
+ }
+ })
+ }
+}
diff --git a/go.mod b/go.mod
index f68be74..be07315 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@
require (
cloud.google.com/go v0.38.0
github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668
+ github.com/rogpeppe/go-internal v1.3.0
golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect
golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73
)
diff --git a/go.sum b/go.sum
index 8e9d91d..8a315b3 100644
--- a/go.sum
+++ b/go.sum
@@ -22,6 +22,12 @@
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -69,5 +75,7 @@
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/sandbox.go b/sandbox.go
index 56e9613..c4a865d 100644
--- a/sandbox.go
+++ b/sandbox.go
@@ -311,33 +311,57 @@
}
defer os.RemoveAll(tmpDir)
- src := []byte(req.Body)
- in := filepath.Join(tmpDir, progName)
- if err := ioutil.WriteFile(in, src, 0400); err != nil {
- return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
- }
-
- fset := token.NewFileSet()
-
- f, err := parser.ParseFile(fset, in, nil, parser.PackageClauseOnly)
- if err == nil && f.Name.Name != "main" {
- return &response{Errors: "package name must be main"}, nil
+ files, err := splitFiles([]byte(req.Body))
+ if err != nil {
+ return &response{Errors: err.Error()}, nil
}
var testParam string
- if code := getTestProg(src); code != nil {
- testParam = "-test.v"
- if err := ioutil.WriteFile(in, code, 0400); err != nil {
+ var buildPkgArg = "."
+ if files.Num() == 1 && len(files.Data(progName)) > 0 {
+ buildPkgArg = progName
+ src := files.Data(progName)
+ if code := getTestProg(src); code != nil {
+ testParam = "-test.v"
+ files.AddFile(progName, code)
+ }
+ }
+
+ useModules := allowModuleDownloads(files)
+ if !files.Contains("go.mod") && useModules {
+ files.AddFile("go.mod", []byte("module play\n"))
+ }
+
+ for f, src := range files.m {
+ // Before multi-file support we required that the
+ // program be in package main, so continue to do that
+ // for now. But permit anything in subdirectories to have other
+ // packages.
+ if !strings.Contains(f, "/") {
+ fset := token.NewFileSet()
+ f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly)
+ if err == nil && f.Name.Name != "main" {
+ return &response{Errors: "package name must be main"}, nil
+ }
+ }
+
+ in := filepath.Join(tmpDir, f)
+ if strings.Contains(f, "/") {
+ if err := os.MkdirAll(filepath.Dir(in), 0755); err != nil {
+ return nil, err
+ }
+ }
+ if err := ioutil.WriteFile(in, src, 0644); err != nil {
return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
}
}
exe := filepath.Join(tmpDir, "a.out")
goCache := filepath.Join(tmpDir, "gocache")
- cmd := exec.Command("go", "build", "-o", exe, in)
+ cmd := exec.Command("go", "build", "-o", exe, buildPkgArg)
+ cmd.Dir = tmpDir
var goPath string
cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOCACHE=" + goCache}
- useModules := allowModuleDownloads(src)
if useModules {
// Create a GOPATH just for modules to be downloaded
// into GOPATH/pkg/mod.
@@ -356,9 +380,8 @@
if _, ok := err.(*exec.ExitError); ok {
// Return compile errors to the user.
- // Rewrite compiler errors to refer to progName
- // instead of '/tmp/sandbox1234/prog.go'.
- errs := strings.Replace(string(out), in, progName, -1)
+ // Rewrite compiler errors to strip the tmpDir name.
+ errs := strings.Replace(string(out), tmpDir+"/", "", -1)
// "go build", invoked with a file name, puts this odd
// message before any compile errors; strip it.
@@ -422,8 +445,8 @@
// allowModuleDownloads reports whether the code snippet in src should be allowed
// to download modules.
-func allowModuleDownloads(src []byte) bool {
- if bytes.Contains(src, []byte(`"code.google.com/p/go-tour/`)) {
+func allowModuleDownloads(files *fileSet) bool {
+ if files.Num() == 1 && bytes.Contains(files.Data(progName), []byte(`"code.google.com/p/go-tour/`)) {
// This domain doesn't exist anymore but we want old snippets using
// these packages to still run, so the Dockerfile adds these packages
// at this name in $GOPATH. Any snippets using this old name wouldn't
diff --git a/server_test.go b/server_test.go
index 4ff2f03..9cc19da 100644
--- a/server_test.go
+++ b/server_test.go
@@ -1,6 +1,7 @@
// Copyright 2017 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 (
@@ -10,6 +11,7 @@
"io/ioutil"
"net/http"
"net/http/httptest"
+ "os"
"testing"
)
@@ -240,3 +242,35 @@
}
}
}
+
+func TestAllowModuleDownloads(t *testing.T) {
+ const envKey = "ALLOW_PLAY_MODULE_DOWNLOADS"
+ defer func(old string) { os.Setenv(envKey, old) }(os.Getenv(envKey))
+
+ tests := []struct {
+ src string
+ env string
+ want bool
+ }{
+ {src: "package main", want: true},
+ {src: "package main", env: "false", want: false},
+ {src: `import "code.google.com/p/go-tour/"`, want: false},
+ }
+ for i, tt := range tests {
+ if tt.env != "" {
+ os.Setenv(envKey, tt.env)
+ } else {
+ os.Setenv(envKey, "true")
+ }
+ files, err := splitFiles([]byte(tt.src))
+ if err != nil {
+ t.Errorf("%d. splitFiles = %v", i, err)
+ continue
+ }
+ got := allowModuleDownloads(files)
+ if got != tt.want {
+ t.Errorf("%d. allow = %v; want %v; files:\n%s", i, got, tt.want, filesAsString(files))
+ }
+ }
+
+}
diff --git a/tests.go b/tests.go
index 437da2b..18f6ea0 100644
--- a/tests.go
+++ b/tests.go
@@ -57,6 +57,9 @@
if resp.VetErrors != t.wantVetErrors {
stdlog.Fatalf("resp.VetErrs = %q, want %q", resp.VetErrors, t.wantVetErrors)
}
+ if t.withVet && (resp.VetErrors != "") == resp.VetOK {
+ stdlog.Fatalf("resp.VetErrs & VetOK inconsistent; VetErrs = %q; VetOK = %v", resp.VetErrors, resp.VetOK)
+ }
if len(resp.Events) == 0 {
stdlog.Fatalf("unexpected output: %q, want %q", "", t.want)
}
@@ -238,7 +241,7 @@
func ExampleNotExecuted() {
// Output: it should not run
}
-`, want: "", errors: "prog.go:4:20: undefined: testing\n"},
+`, want: "", errors: "./prog.go:4:20: undefined: testing\n"},
{
name: "test_with_import_ignored",
@@ -406,7 +409,7 @@
{
name: "compile_with_vet",
withVet: true,
- wantVetErrors: "prog.go:5:2: Printf format %v reads arg #1, but call has 0 args\n",
+ wantVetErrors: "./prog.go:5:2: Printf format %v reads arg #1, but call has 0 args\n",
prog: `
package main
import "fmt"
@@ -431,7 +434,7 @@
{
name: "compile_modules_with_vet",
withVet: true,
- wantVetErrors: "prog.go:6:2: Printf format %v reads arg #1, but call has 0 args\n",
+ wantVetErrors: "./prog.go:6:2: Printf format %v reads arg #1, but call has 0 args\n",
prog: `
package main
import ("fmt"; "github.com/bradfitz/iter")
@@ -441,4 +444,45 @@
}
`,
},
+
+ {
+ name: "multi_file_basic",
+ prog: `
+package main
+const foo = "bar"
+
+-- two.go --
+package main
+func main() {
+ println(foo)
+}
+`,
+ wantEvents: []Event{
+ {"bar\n", "stderr", 0},
+ },
+ },
+
+ {
+ name: "multi_file_use_package",
+ withVet: true,
+ prog: `
+package main
+
+import "play.test/foo"
+
+func main() {
+ foo.Hello()
+}
+
+-- go.mod --
+module play.test
+
+-- foo/foo.go --
+package foo
+
+import "fmt"
+
+func Hello() { fmt.Println("hello world") }
+`,
+ },
}
diff --git a/txtar.go b/txtar.go
new file mode 100644
index 0000000..9b699bf
--- /dev/null
+++ b/txtar.go
@@ -0,0 +1,121 @@
+// Copyright 2019 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"
+ "errors"
+ "fmt"
+ "path"
+ "strings"
+
+ "github.com/rogpeppe/go-internal/txtar"
+)
+
+// fileSet is a set of files.
+// The zero value for fileSet is an empty set ready to use.
+type fileSet struct {
+ files []string // filenames in user-provided order
+ m map[string][]byte // filename -> source
+ noHeader bool // whether the prog.go entry was implicit
+}
+
+// Data returns the content of the named file.
+// The fileSet retains ownership of the returned slice.
+func (fs *fileSet) Data(filename string) []byte { return fs.m[filename] }
+
+// Num returns the number of files in the set.
+func (fs *fileSet) Num() int { return len(fs.m) }
+
+// Contains reports whether fs contains the given filename.
+func (fs *fileSet) Contains(filename string) bool {
+ _, ok := fs.m[filename]
+ return ok
+}
+
+// AddFile adds a file to fs. If fs already contains filename, its
+// contents are replaced.
+func (fs *fileSet) AddFile(filename string, src []byte) {
+ had := fs.Contains(filename)
+ if fs.m == nil {
+ fs.m = make(map[string][]byte)
+ }
+ fs.m[filename] = src
+ if !had {
+ fs.files = append(fs.files, filename)
+ }
+}
+
+// Format returns fs formatted as a txtar archive.
+func (fs *fileSet) Format() []byte {
+ a := new(txtar.Archive)
+ if fs.noHeader {
+ a.Comment = fs.m[progName]
+ }
+ for i, f := range fs.files {
+ if i == 0 && f == progName && fs.noHeader {
+ continue
+ }
+ a.Files = append(a.Files, txtar.File{Name: f, Data: fs.m[f]})
+ }
+ return txtar.Format(a)
+}
+
+// splitFiles splits the user's input program src into 1 or more
+// files, splitting it based on boundaries as specified by the "txtar"
+// format. It returns an error if any filenames are bogus or
+// duplicates. The implicit filename for the txtar comment (the lines
+// before any txtar separator line) are named "prog.go". It is an
+// error to have an explicit file named "prog.go" in addition to
+// having the implicit "prog.go" file (non-empty comment section).
+//
+// The filenames are validated to only be relative paths, not too
+// long, not too deep, not have ".." elements, not have backslashes or
+// low ASCII binary characters, and to be in path.Clean canonical
+// form.
+//
+// splitFiles takes ownership of src.
+func splitFiles(src []byte) (*fileSet, error) {
+ fs := new(fileSet)
+ a := txtar.Parse(src)
+ if v := bytes.TrimSpace(a.Comment); len(v) > 0 {
+ fs.noHeader = true
+ fs.AddFile(progName, a.Comment)
+ }
+ const limitNumFiles = 20 // arbitrary
+ numFiles := len(a.Files) + fs.Num()
+ if numFiles > limitNumFiles {
+ return nil, fmt.Errorf("too many files in txtar archive (%v exceeds limit of %v)", numFiles, limitNumFiles)
+ }
+ for _, f := range a.Files {
+ if len(f.Name) > 200 { // arbitrary limit
+ return nil, errors.New("file name too long")
+ }
+ if strings.IndexFunc(f.Name, isBogusFilenameRune) != -1 {
+ return nil, fmt.Errorf("invalid file name %q", f.Name)
+ }
+ if f.Name != path.Clean(f.Name) || path.IsAbs(f.Name) {
+ return nil, fmt.Errorf("invalid file name %q", f.Name)
+ }
+ parts := strings.Split(f.Name, "/")
+ if len(parts) > 10 { // arbitrary limit
+ return nil, fmt.Errorf("file name %q too deep", f.Name)
+ }
+ for _, part := range parts {
+ if part == "." || part == ".." {
+ return nil, fmt.Errorf("invalid file name %q", f.Name)
+ }
+ }
+ if fs.Contains(f.Name) {
+ return nil, fmt.Errorf("duplicate file name %q", f.Name)
+ }
+ fs.AddFile(f.Name, f.Data)
+ }
+ return fs, nil
+}
+
+// isBogusFilenameRune reports whether r should be rejected if it
+// appears in a txtar section's filename.
+func isBogusFilenameRune(r rune) bool { return r == '\\' || r < ' ' }
diff --git a/txtar_test.go b/txtar_test.go
new file mode 100644
index 0000000..ae1ef96
--- /dev/null
+++ b/txtar_test.go
@@ -0,0 +1,153 @@
+// Copyright 2019 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 (
+ "fmt"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func newFileSet(kv ...string) *fileSet {
+ fs := new(fileSet)
+ if kv[0] == "prog.go!implicit" {
+ fs.noHeader = true
+ kv[0] = "prog.go"
+ }
+ for len(kv) > 0 {
+ fs.AddFile(kv[0], []byte(kv[1]))
+ kv = kv[2:]
+ }
+ return fs
+}
+
+func TestSplitFiles(t *testing.T) {
+ for _, tt := range []struct {
+ name string
+ in string
+ want *fileSet
+ wantErr string
+ }{
+ {
+ name: "classic",
+ in: "package main",
+ want: newFileSet("prog.go!implicit", "package main\n"),
+ },
+ {
+ name: "implicit prog.go",
+ in: "package main\n-- two.go --\nsecond",
+ want: newFileSet(
+ "prog.go!implicit", "package main\n",
+ "two.go", "second\n",
+ ),
+ },
+ {
+ name: "basic txtar",
+ in: "-- main.go --\npackage main\n-- foo.go --\npackage main\n",
+ want: newFileSet(
+ "main.go", "package main\n",
+ "foo.go", "package main\n",
+ ),
+ },
+ {
+ name: "reject dotdot 1",
+ in: "-- ../foo --\n",
+ wantErr: `invalid file name "../foo"`,
+ },
+ {
+ name: "reject dotdot 2",
+ in: "-- .. --\n",
+ wantErr: `invalid file name ".."`,
+ },
+ {
+ name: "reject dotdot 3",
+ in: "-- bar/../foo --\n",
+ wantErr: `invalid file name "bar/../foo"`,
+ },
+ {
+ name: "reject long",
+ in: "-- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx --\n",
+ wantErr: `file name too long`,
+ },
+ {
+ name: "reject deep",
+ in: "-- x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x --\n",
+ wantErr: `file name "x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x" too deep`,
+ },
+ {
+ name: "reject abs",
+ in: "-- /etc/passwd --\n",
+ wantErr: `invalid file name "/etc/passwd"`,
+ },
+ {
+ name: "reject backslash",
+ in: "-- foo\\bar --\n",
+ wantErr: `invalid file name "foo\\bar"`,
+ },
+ {
+ name: "reject binary null",
+ in: "-- foo\x00bar --\n",
+ wantErr: `invalid file name "foo\x00bar"`,
+ },
+ {
+ name: "reject binary low",
+ in: "-- foo\x1fbar --\n",
+ wantErr: `invalid file name "foo\x1fbar"`,
+ },
+ {
+ name: "reject dup",
+ in: "-- foo.go --\n-- foo.go --\n",
+ wantErr: `duplicate file name "foo.go"`,
+ },
+ {
+ name: "reject implicit dup",
+ in: "package main\n-- prog.go --\n",
+ wantErr: `duplicate file name "prog.go"`,
+ },
+ {
+ name: "skip leading whitespace comment",
+ in: "\n \n\n \n\n-- f.go --\ncontents",
+ want: newFileSet("f.go", "contents\n"),
+ },
+ {
+ name: "reject many files",
+ in: strings.Repeat("-- x.go --\n", 50),
+ wantErr: `too many files in txtar archive (50 exceeds limit of 20)`,
+ },
+ } {
+ got, err := splitFiles([]byte(tt.in))
+ var gotErr string
+ if err != nil {
+ gotErr = err.Error()
+ }
+ if gotErr != tt.wantErr {
+ if tt.wantErr == "" {
+ t.Errorf("%s: unexpected error: %v", tt.name, err)
+ continue
+ }
+ t.Errorf("%s: error = %#q; want error %#q", tt.name, err, tt.wantErr)
+ continue
+ }
+ if err != nil {
+ continue
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("%s: wrong files\n got:\n%s\nwant:\n%s", tt.name, filesAsString(got), filesAsString(tt.want))
+ }
+ }
+}
+
+func filesAsString(fs *fileSet) string {
+ var sb strings.Builder
+ for i, f := range fs.files {
+ var implicit string
+ if i == 0 && f == progName && fs.noHeader {
+ implicit = " (implicit)"
+ }
+ fmt.Fprintf(&sb, "[file %q%s]: %q\n", f, implicit, fs.m[f])
+ }
+ return sb.String()
+}
diff --git a/vet.go b/vet.go
index 78df95c..32edeca 100644
--- a/vet.go
+++ b/vet.go
@@ -48,8 +48,11 @@
// vet successfully found nothing, and (non-empty, nil) if vet ran and
// found issues.
func vetCheckInDir(dir, goPath string, modules bool) (output string, execErr error) {
- in := filepath.Join(dir, progName)
- cmd := exec.Command("go", "vet", in)
+ cmd := exec.Command("go", "vet")
+ if !modules {
+ cmd.Args = append(cmd.Args, progName)
+ }
+ cmd.Dir = dir
// Linux go binary is not built with CGO_ENABLED=0.
// Prevent vet to compile packages in cgo mode.
// See #26307.
@@ -70,11 +73,13 @@
// Rewrite compiler errors to refer to progName
// instead of '/tmp/sandbox1234/main.go'.
- errs := strings.Replace(string(out), in, progName, -1)
+ errs := strings.Replace(string(out), dir, "", -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)
-
+ // Remove vet's package name banner.
+ if strings.HasPrefix(errs, "#") {
+ if nl := strings.Index(errs, "\n"); nl != -1 {
+ errs = errs[nl+1:]
+ }
+ }
return errs, nil
}