diff --git a/Dockerfile b/Dockerfile index e6295da4..27adf061 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,12 +41,17 @@ RUN mkdir /gocache 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 09f50f26..c5aa9431 100644 --- a/fmt.go +++ b/fmt.go @@ -20,25 +20,40 @@ type fmtResponse struct { } 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) + json.NewEncoder(w).Encode(fmtResponse{Error: err.Error()}) + return + } + + fixImports := r.FormValue("imports") != "" + for _, f := range fs.files { + if !strings.HasSuffix(f, ".go") { + continue } - } else { - resp.Body = string(out) + 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(resp) + + json.NewEncoder(w).Encode(fmtResponse{Body: string(fs.Format())}) } diff --git a/fmt_test.go b/fmt_test.go new file mode 100644 index 00000000..b8a1b9ab --- /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 f68be743..be07315e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 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 8e9d91d0..8a315b3b 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,12 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ 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 h1:ZUjXAXmrAyrmmCP 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 56e96136..c4a865d4 100644 --- a/sandbox.go +++ b/sandbox.go @@ -311,33 +311,57 @@ func compileAndRun(req *request) (*response, error) { } 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) + files, err := splitFiles([]byte(req.Body)) + if err != nil { + return &response{Errors: err.Error()}, nil } - fset := token.NewFileSet() + var testParam string + 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) + } + } - 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 + useModules := allowModuleDownloads(files) + if !files.Contains("go.mod") && useModules { + files.AddFile("go.mod", []byte("module play\n")) } - var testParam string - if code := getTestProg(src); code != nil { - testParam = "-test.v" - if err := ioutil.WriteFile(in, code, 0400); err != nil { + 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 @@ func compileAndRun(req *request) (*response, error) { 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 @@ func compileAndRun(req *request) (*response, error) { // 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 4ff2f034..9cc19da0 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 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "testing" ) @@ -240,3 +242,35 @@ func TestCommandHandler(t *testing.T) { } } } + +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 437da2b3..18f6ea0a 100644 --- a/tests.go +++ b/tests.go @@ -57,6 +57,9 @@ func (s *server) test() { 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 TestSanity(t *testing.T) { 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 @@ func main() { for i := range iter.N(5) { fmt.Println(i) } } { 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 @@ func main() { { 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") @@ -439,6 +442,47 @@ func main() { for i := range iter.N(5) { fmt.Println(i) } fmt.Printf("hi %v") } +`, + }, + + { + 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 00000000..9b699bf0 --- /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 00000000..ae1ef967 --- /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 78df95c6..32edeca0 100644 --- a/vet.go +++ b/vet.go @@ -48,8 +48,11 @@ func vetCheck(req *request) (*response, error) { // 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 @@ func vetCheckInDir(dir, goPath string, modules bool) (output string, execErr 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) + errs := strings.Replace(string(out), dir, "", -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 }