--- /dev/null
+// Copyright 2013 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 imports
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "go/build"
+ "io/ioutil"
+ "log"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "sort"
+ "strings"
+ "sync"
+ "testing"
+
+ "golang.org/x/tools/go/packages/packagestest"
+ "golang.org/x/tools/internal/gocommand"
+)
+
+var testDebug = flag.Bool("debug", false, "enable debug output")
+
+var tests = []struct {
+ name string
+ formatOnly bool
+ in, out string
+}{
+ // Adding an import to an existing parenthesized import
+ {
+ name: "factored_imports_add",
+ in: `package foo
+import (
+ "fmt"
+)
+func bar() {
+var b bytes.Buffer
+fmt.Println(b.String())
+}
+`,
+ out: `package foo
+
+import (
+ "bytes"
+ "fmt"
+)
+
+func bar() {
+ var b bytes.Buffer
+ fmt.Println(b.String())
+}
+`,
+ },
+
+ // Adding an import to an existing parenthesized import,
+ // verifying it goes into the first section.
+ {
+ name: "factored_imports_add_first_sec",
+ in: `package foo
+import (
+ "fmt"
+
+ "github.com/golang/snappy"
+)
+func bar() {
+var b bytes.Buffer
+_ = snappy.ErrCorrupt
+fmt.Println(b.String())
+}
+`,
+ out: `package foo
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/golang/snappy"
+)
+
+func bar() {
+ var b bytes.Buffer
+ _ = snappy.ErrCorrupt
+ fmt.Println(b.String())
+}
+`,
+ },
+
+ // Adding an import to an existing parenthesized import,
+ // verifying it goes into the first section. (test 2)
+ {
+ name: "factored_imports_add_first_sec_2",
+ in: `package foo
+import (
+ "fmt"
+
+ "github.com/golang/snappy"
+)
+func bar() {
+_ = math.NaN
+_ = fmt.Sprintf
+_ = snappy.ErrCorrupt
+}
+`,
+ out: `package foo
+
+import (
+ "fmt"
+ "math"
+
+ "github.com/golang/snappy"
+)
+
+func bar() {
+ _ = math.NaN
+ _ = fmt.Sprintf
+ _ = snappy.ErrCorrupt
+}
+`,
+ },
+
+ // Adding a new import line, without parens
+ {
+ name: "add_import_section",
+ in: `package foo
+func bar() {
+var b bytes.Buffer
+}
+`,
+ out: `package foo
+
+import "bytes"
+
+func bar() {
+ var b bytes.Buffer
+}
+`,
+ },
+
+ // Adding two new imports, which should make a parenthesized import decl.
+ {
+ name: "add_import_paren_section",
+ in: `package foo
+func bar() {
+_, _ := bytes.Buffer, zip.NewReader
+}
+`,
+ out: `package foo
+
+import (
+ "archive/zip"
+ "bytes"
+)
+
+func bar() {
+ _, _ := bytes.Buffer, zip.NewReader
+}
+`,
+ },
+
+ // Make sure we don't add things twice
+ {
+ name: "no_double_add",
+ in: `package foo
+func bar() {
+_, _ := bytes.Buffer, bytes.NewReader
+}
+`,
+ out: `package foo
+
+import "bytes"
+
+func bar() {
+ _, _ := bytes.Buffer, bytes.NewReader
+}
+`,
+ },
+
+ // Make sure we don't add packages that don't have the right exports
+ {
+ name: "no_mismatched_add",
+ in: `package foo
+
+func bar() {
+ _ := bytes.NonexistentSymbol
+}
+`,
+ out: `package foo
+
+func bar() {
+ _ := bytes.NonexistentSymbol
+}
+`,
+ },
+
+ // Remove unused imports, 1 of a factored block
+ {
+ name: "remove_unused_1_of_2",
+ in: `package foo
+import (
+"bytes"
+"fmt"
+)
+
+func bar() {
+_, _ := bytes.Buffer, bytes.NewReader
+}
+`,
+ out: `package foo
+
+import (
+ "bytes"
+)
+
+func bar() {
+ _, _ := bytes.Buffer, bytes.NewReader
+}
+`,
+ },
+
+ // Remove unused imports, 2 of 2
+ {
+ name: "remove_unused_2_of_2",
+ in: `package foo
+import (
+"bytes"
+"fmt"
+)
+
+func bar() {
+}
+`,
+ out: `package foo
+
+func bar() {
+}
+`,
+ },
+
+ // Remove unused imports, 1 of 1
+ {
+ name: "remove_unused_1_of_1",
+ in: `package foo
+
+import "fmt"
+
+func bar() {
+}
+`,
+ out: `package foo
+
+func bar() {
+}
+`,
+ },
+
+ // Don't remove empty imports.
+ {
+ name: "dont_remove_empty_imports",
+ in: `package foo
+import (
+_ "image/png"
+_ "image/jpeg"
+)
+`,
+ out: `package foo
+
+import (
+ _ "image/jpeg"
+ _ "image/png"
+)
+`,
+ },
+
+ // Don't remove dot imports.
+ {
+ name: "dont_remove_dot_imports",
+ in: `package foo
+import (
+. "foo"
+. "bar"
+)
+`,
+ out: `package foo
+
+import (
+ . "bar"
+ . "foo"
+)
+`,
+ },
+
+ // Skip refs the parser can resolve.
+ {
+ name: "skip_resolved_refs",
+ in: `package foo
+
+func f() {
+ type t struct{ Println func(string) }
+ fmt := t{Println: func(string) {}}
+ fmt.Println("foo")
+}
+`,
+ out: `package foo
+
+func f() {
+ type t struct{ Println func(string) }
+ fmt := t{Println: func(string) {}}
+ fmt.Println("foo")
+}
+`,
+ },
+
+ // Do not add a package we already have a resolution for.
+ {
+ name: "skip_template",
+ in: `package foo
+
+import "html/template"
+
+func f() { t = template.New("sometemplate") }
+`,
+ out: `package foo
+
+import "html/template"
+
+func f() { t = template.New("sometemplate") }
+`,
+ },
+
+ // Don't touch cgo
+ {
+ name: "cgo",
+ in: `package foo
+
+/*
+#include <foo.h>
+*/
+import "C"
+`,
+ out: `package foo
+
+/*
+#include <foo.h>
+*/
+import "C"
+`,
+ },
+
+ // Put some things in their own section
+ {
+ name: "make_sections",
+ in: `package foo
+
+import (
+"os"
+)
+
+func foo () {
+_, _ = os.Args, fmt.Println
+_, _ = snappy.ErrCorrupt, p.P
+}
+`,
+ out: `package foo
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/golang/snappy"
+ "rsc.io/p"
+)
+
+func foo() {
+ _, _ = os.Args, fmt.Println
+ _, _ = snappy.ErrCorrupt, p.P
+}
+`,
+ },
+ // Merge import blocks, even when no additions are required.
+ {
+ name: "merge_import_blocks_no_fix",
+ in: `package foo
+
+import (
+ "fmt"
+)
+import "os"
+
+import (
+ "rsc.io/p"
+)
+
+var _, _ = os.Args, fmt.Println
+var _, _ = snappy.ErrCorrupt, p.P
+`,
+ out: `package foo
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/golang/snappy"
+ "rsc.io/p"
+)
+
+var _, _ = os.Args, fmt.Println
+var _, _ = snappy.ErrCorrupt, p.P
+`,
+ },
+ // Delete existing empty import block
+ {
+ name: "delete_empty_import_block",
+ in: `package foo
+
+import ()
+`,
+ out: `package foo
+`,
+ },
+
+ // Use existing empty import block
+ {
+ name: "use_empty_import_block",
+ in: `package foo
+
+import ()
+
+func f() {
+ _ = fmt.Println
+}
+`,
+ out: `package foo
+
+import "fmt"
+
+func f() {
+ _ = fmt.Println
+}
+`,
+ },
+
+ // Blank line before adding new section.
+ {
+ name: "blank_line_before_new_group",
+ in: `package foo
+
+import (
+ "fmt"
+ "net"
+)
+
+func f() {
+ _ = net.Dial
+ _ = fmt.Printf
+ _ = snappy.ErrCorrupt
+}
+`,
+ out: `package foo
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/golang/snappy"
+)
+
+func f() {
+ _ = net.Dial
+ _ = fmt.Printf
+ _ = snappy.ErrCorrupt
+}
+`,
+ },
+
+ // Blank line between standard library and third-party stuff.
+ {
+ name: "blank_line_separating_std_and_third_party",
+ in: `package foo
+
+import (
+ "github.com/golang/snappy"
+ "fmt"
+ "net"
+)
+
+func f() {
+ _ = net.Dial
+ _ = fmt.Printf
+ _ = snappy.Foo
+}
+`,
+ out: `package foo
+
+import (
+ "fmt"
+ "net"
+
+ "github.com/golang/snappy"
+)
+
+func f() {
+ _ = net.Dial
+ _ = fmt.Printf
+ _ = snappy.Foo
+}
+`,
+ },
+
+ // golang.org/issue/6884
+ {
+ name: "new_imports_before_comment",
+ in: `package main
+
+// A comment
+func main() {
+ fmt.Println("Hello, world")
+}
+`,
+ out: `package main
+
+import "fmt"
+
+// A comment
+func main() {
+ fmt.Println("Hello, world")
+}
+`,
+ },
+
+ // golang.org/issue/7132
+ {
+ name: "new_section_for_dotless_import",
+ in: `package main
+
+import (
+"fmt"
+
+"gu"
+"manypackages.com/packagea"
+)
+
+var (
+a = packagea.A
+b = gu.A
+c = fmt.Printf
+)
+`,
+ out: `package main
+
+import (
+ "fmt"
+
+ "gu"
+
+ "manypackages.com/packagea"
+)
+
+var (
+ a = packagea.A
+ b = gu.A
+ c = fmt.Printf
+)
+`,
+ },
+
+ {
+ name: "fragment_with_main",
+ in: `func main(){fmt.Println("Hello, world")}`,
+ out: `package main
+
+import "fmt"
+
+func main() { fmt.Println("Hello, world") }
+`,
+ },
+
+ {
+ name: "fragment_without_main",
+ in: `func notmain(){fmt.Println("Hello, world")}`,
+ out: `import "fmt"
+
+func notmain() { fmt.Println("Hello, world") }`,
+ },
+
+ // Remove first import within in a 2nd/3rd/4th/etc. section.
+ // golang.org/issue/7679
+ {
+ name: "remove_first_import_in_section",
+ in: `package main
+
+import (
+ "fmt"
+
+ "manypackages.com/packagea"
+ "manypackages.com/packageb"
+)
+
+func main() {
+ var _ = fmt.Println
+ //var _ = packagea.A
+ var _ = packageb.B
+}
+`,
+ out: `package main
+
+import (
+ "fmt"
+
+ "manypackages.com/packageb"
+)
+
+func main() {
+ var _ = fmt.Println
+ //var _ = packagea.A
+ var _ = packageb.B
+}
+`,
+ },
+
+ // Blank line can be added before all types of import declarations.
+ // golang.org/issue/7866
+ {
+ name: "new_section_for_all_kinds_of_imports",
+ in: `package main
+
+import (
+ "fmt"
+ renamed_packagea "manypackages.com/packagea"
+
+ . "manypackages.com/packageb"
+ "io"
+
+ _ "manypackages.com/packagec"
+ "strings"
+)
+
+var _, _, _, _, _ = fmt.Errorf, io.Copy, strings.Contains, renamed_packagea.A, B
+`,
+ out: `package main
+
+import (
+ "fmt"
+
+ renamed_packagea "manypackages.com/packagea"
+
+ "io"
+
+ . "manypackages.com/packageb"
+
+ "strings"
+
+ _ "manypackages.com/packagec"
+)
+
+var _, _, _, _, _ = fmt.Errorf, io.Copy, strings.Contains, renamed_packagea.A, B
+`,
+ },
+
+ // Non-idempotent comment formatting
+ // golang.org/issue/8035
+ {
+ name: "comments_formatted",
+ in: `package main
+
+import (
+ "fmt" // A
+ "go/ast" // B
+ _ "manypackages.com/packagec" // C
+)
+
+func main() { _, _ = fmt.Print, ast.Walk }
+`,
+ out: `package main
+
+import (
+ "fmt" // A
+ "go/ast" // B
+
+ _ "manypackages.com/packagec" // C
+)
+
+func main() { _, _ = fmt.Print, ast.Walk }
+`,
+ },
+
+ // Failure to delete all duplicate imports
+ // golang.org/issue/8459
+ {
+ name: "remove_duplicates",
+ in: `package main
+
+import (
+ "fmt"
+ "log"
+ "log"
+ "math"
+)
+
+func main() { fmt.Println("pi:", math.Pi) }
+`,
+ out: `package main
+
+import (
+ "fmt"
+ "math"
+)
+
+func main() { fmt.Println("pi:", math.Pi) }
+`,
+ },
+
+ // Too aggressive prefix matching
+ // golang.org/issue/9961
+ {
+ name: "no_extra_groups",
+ in: `package p
+
+import (
+ "zip"
+
+ "rsc.io/p"
+)
+
+var (
+ _ = fmt.Print
+ _ = zip.Store
+ _ p.P
+ _ = regexp.Compile
+)
+`,
+ out: `package p
+
+import (
+ "fmt"
+ "regexp"
+ "zip"
+
+ "rsc.io/p"
+)
+
+var (
+ _ = fmt.Print
+ _ = zip.Store
+ _ p.P
+ _ = regexp.Compile
+)
+`,
+ },
+
+ // Unused named import is mistaken for unnamed import
+ // golang.org/issue/8149
+ {
+ name: "named_import_doesnt_provide_package_name",
+ in: `package main
+
+import foo "fmt"
+
+func main() { fmt.Println() }
+`,
+ out: `package main
+
+import "fmt"
+
+func main() { fmt.Println() }
+`,
+ },
+
+ // Unused named import is mistaken for unnamed import
+ // golang.org/issue/8149
+ {
+ name: "unused_named_import_removed",
+ in: `package main
+
+import (
+ "fmt"
+ x "fmt"
+)
+
+func main() { fmt.Println() }
+`,
+ out: `package main
+
+import (
+ "fmt"
+)
+
+func main() { fmt.Println() }
+`,
+ },
+
+ {
+ name: "ignore_unexported_identifier",
+ in: `package main
+var _ = fmt.unexported`,
+ out: `package main
+
+var _ = fmt.unexported
+`,
+ },
+
+ // FormatOnly
+ {
+ name: "formatonly_works",
+ formatOnly: true,
+ in: `package main
+
+import (
+"fmt"
+"manypackages.com/packagea"
+)
+
+func main() {}
+`,
+ out: `package main
+
+import (
+ "fmt"
+
+ "manypackages.com/packagea"
+)
+
+func main() {}
+`,
+ },
+
+ {
+ name: "preserve_import_group",
+ in: `package p
+
+import (
+ "bytes"
+ "fmt"
+)
+
+var _ = fmt.Sprintf
+`,
+ out: `package p
+
+import (
+ "fmt"
+)
+
+var _ = fmt.Sprintf
+`,
+ },
+ {
+ name: "import_grouping_not_path_dependent_no_groups",
+ in: `package main
+
+import (
+ "time"
+)
+
+func main() {
+ _ = snappy.ErrCorrupt
+ _ = p.P
+ _ = time.Parse
+}
+`,
+ out: `package main
+
+import (
+ "time"
+
+ "github.com/golang/snappy"
+ "rsc.io/p"
+)
+
+func main() {
+ _ = snappy.ErrCorrupt
+ _ = p.P
+ _ = time.Parse
+}
+`,
+ },
+
+ {
+ name: "import_grouping_not_path_dependent_existing_group",
+ in: `package main
+
+import (
+ "time"
+
+ "github.com/golang/snappy"
+)
+
+func main() {
+ _ = snappy.ErrCorrupt
+ _ = p.P
+ _ = time.Parse
+}
+`,
+ out: `package main
+
+import (
+ "time"
+
+ "github.com/golang/snappy"
+ "rsc.io/p"
+)
+
+func main() {
+ _ = snappy.ErrCorrupt
+ _ = p.P
+ _ = time.Parse
+}
+`,
+ },
+
+ // golang.org/issue/12097
+ {
+ name: "package_statement_insertion_preserves_comments",
+ in: `// a
+// b
+// c
+
+func main() {
+ _ = fmt.Println
+}`,
+ out: `package main
+
+import "fmt"
+
+// a
+// b
+// c
+
+func main() {
+ _ = fmt.Println
+}
+`,
+ },
+
+ {
+ name: "import_comment_stays_on_import",
+ in: `package main
+
+import (
+ "math" // fun
+)
+
+func main() {
+ x := math.MaxInt64
+ fmt.Println(strings.Join(",", []string{"hi"}), x)
+}`,
+ out: `package main
+
+import (
+ "fmt"
+ "math" // fun
+ "strings"
+)
+
+func main() {
+ x := math.MaxInt64
+ fmt.Println(strings.Join(",", []string{"hi"}), x)
+}
+`,
+ },
+
+ {
+ name: "no_blank_after_comment",
+ in: `package main
+
+import (
+ _ "io"
+ _ "net/http"
+ _ "net/http/pprof" // install the pprof http handlers
+ _ "strings"
+)
+
+func main() {
+}
+`,
+ out: `package main
+
+import (
+ _ "io"
+ _ "net/http"
+ _ "net/http/pprof" // install the pprof http handlers
+ _ "strings"
+)
+
+func main() {
+}
+`,
+ },
+
+ {
+ name: "no_blank_after_comment_reordered",
+ in: `package main
+
+import (
+ _ "io"
+ _ "net/http/pprof" // install the pprof http handlers
+ _ "net/http"
+ _ "strings"
+)
+
+func main() {
+}
+`,
+ out: `package main
+
+import (
+ _ "io"
+ _ "net/http"
+ _ "net/http/pprof" // install the pprof http handlers
+ _ "strings"
+)
+
+func main() {
+}
+`,
+ },
+
+ {
+ name: "no_blank_after_comment_unnamed",
+ in: `package main
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ _ "net/http/pprof" // install the pprof http handlers
+ "strings"
+
+ "manypackages.com/packagea"
+)
+
+func main() {
+ _ = strings.ToUpper("hello")
+ _ = io.EOF
+ var (
+ _ json.Number
+ _ *http.Request
+ _ packagea.A
+ )
+}
+`,
+ out: `package main
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ _ "net/http/pprof" // install the pprof http handlers
+ "strings"
+
+ "manypackages.com/packagea"
+)
+
+func main() {
+ _ = strings.ToUpper("hello")
+ _ = io.EOF
+ var (
+ _ json.Number
+ _ *http.Request
+ _ packagea.A
+ )
+}
+`,
+ },
+
+ {
+ name: "blank_after_package_statement_with_comment",
+ in: `package p // comment
+
+import "math"
+
+var _ = fmt.Printf
+`,
+ out: `package p // comment
+
+import "fmt"
+
+var _ = fmt.Printf
+`,
+ },
+
+ {
+ name: "blank_after_package_statement_no_comment",
+ in: `package p
+
+import "math"
+
+var _ = fmt.Printf
+`,
+ out: `package p
+
+import "fmt"
+
+var _ = fmt.Printf
+`,
+ },
+
+ {
+ name: "cryptorand_preferred_easy_possible",
+ in: `package p
+
+var _ = rand.Read
+`,
+ out: `package p
+
+import "crypto/rand"
+
+var _ = rand.Read
+`,
+ },
+
+ {
+ name: "cryptorand_preferred_easy_impossible",
+ in: `package p
+
+var _ = rand.NewZipf
+`,
+ out: `package p
+
+import "math/rand"
+
+var _ = rand.NewZipf
+`,
+ },
+
+ {
+ name: "cryptorand_preferred_complex_possible",
+ in: `package p
+
+var _, _ = rand.Read, rand.Prime
+`,
+ out: `package p
+
+import "crypto/rand"
+
+var _, _ = rand.Read, rand.Prime
+`,
+ },
+
+ {
+ name: "cryptorand_preferred_complex_impossible",
+ in: `package p
+
+var _, _ = rand.Read, rand.NewZipf
+`,
+ out: `package p
+
+import "math/rand"
+
+var _, _ = rand.Read, rand.NewZipf
+`,
+ },
+}
+
+func TestSimpleCases(t *testing.T) {
+ const localPrefix = "local.com,github.com/local"
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "golang.org/fake",
+ Files: fm{"x.go": tt.in},
+ },
+ // Skeleton non-stdlib packages for use during testing.
+ // Each includes one arbitrary symbol, e.g. the first declaration in the first file.
+ // Try not to add more without a good reason.
+ // DO NOT USE PACKAGES NOT LISTED HERE -- they will be downloaded!
+ {
+ Name: "rsc.io",
+ Files: fm{"p/x.go": "package p\nfunc P(){}\n"},
+ },
+ {
+ Name: "github.com/golang/snappy",
+ Files: fm{"x.go": "package snappy\nvar ErrCorrupt error\n"},
+ },
+ {
+ Name: "manypackages.com",
+ Files: fm{
+ "packagea/x.go": "package packagea\nfunc A(){}\n",
+ "packageb/x.go": "package packageb\nfunc B(){}\n",
+ "packagec/x.go": "package packagec\nfunc C(){}\n",
+ "packaged/x.go": "package packaged\nfunc D(){}\n",
+ },
+ },
+ {
+ Name: "local.com",
+ Files: fm{"foo/x.go": "package foo\nfunc Foo(){}\n"},
+ },
+ {
+ Name: "github.com/local",
+ Files: fm{"bar/x.go": "package bar\nfunc Bar(){}\n"},
+ },
+ },
+ }.test(t, func(t *goimportTest) {
+ options := &Options{
+ LocalPrefix: localPrefix,
+ TabWidth: 8,
+ TabIndent: true,
+ Comments: true,
+ Fragment: true,
+ FormatOnly: tt.formatOnly,
+ }
+ t.assertProcessEquals("golang.org/fake", "x.go", nil, options, tt.out)
+ })
+
+ })
+ }
+}
+
+func TestAppengine(t *testing.T) {
+ const input = `package p
+
+var _, _, _ = fmt.Printf, appengine.Main, datastore.ErrInvalidEntityType
+`
+
+ const want = `package p
+
+import (
+ "fmt"
+
+ "appengine"
+ "appengine/datastore"
+)
+
+var _, _, _ = fmt.Printf, appengine.Main, datastore.ErrInvalidEntityType
+`
+
+ testConfig{
+ gopathOnly: true, // can't create a module named appengine, so no module tests.
+ modules: []packagestest.Module{
+ {
+ Name: "golang.org/fake",
+ Files: fm{"x.go": input},
+ },
+ {
+ Name: "appengine",
+ Files: fm{
+ "x.go": "package appengine\nfunc Main(){}\n",
+ "datastore/x.go": "package datastore\nvar ErrInvalidEntityType error\n",
+ },
+ },
+ },
+ }.processTest(t, "golang.org/fake", "x.go", nil, nil, want)
+}
+
+func TestReadFromFilesystem(t *testing.T) {
+ tests := []struct {
+ name string
+ in, out string
+ }{
+ {
+ name: "works",
+ in: `package foo
+func bar() {
+fmt.Println("hi")
+}
+`,
+ out: `package foo
+
+import "fmt"
+
+func bar() {
+ fmt.Println("hi")
+}
+`,
+ },
+ {
+ name: "missing_package",
+ in: `
+func bar() {
+fmt.Println("hi")
+}
+`,
+ out: `
+import "fmt"
+
+func bar() {
+ fmt.Println("hi")
+}
+`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ options := &Options{
+ TabWidth: 8,
+ TabIndent: true,
+ Comments: true,
+ Fragment: true,
+ }
+ testConfig{
+ module: packagestest.Module{
+ Name: "golang.org/fake",
+ Files: fm{"x.go": tt.in},
+ },
+ }.processTest(t, "golang.org/fake", "x.go", nil, options, tt.out)
+ })
+ }
+
+}
+
+// Test support for packages in GOPATH that are actually symlinks.
+// Also test that a symlink loop does not block the process.
+func TestImportSymlinks(t *testing.T) {
+ switch runtime.GOOS {
+ case "windows", "plan9":
+ t.Skipf("skipping test on %q as there are no symlinks", runtime.GOOS)
+ }
+
+ const input = `package p
+
+var (
+ _ = fmt.Print
+ _ = mypkg.Foo
+)
+`
+ const want = `package p
+
+import (
+ "fmt"
+
+ "golang.org/fake/x/y/mypkg"
+)
+
+var (
+ _ = fmt.Print
+ _ = mypkg.Foo
+)
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "golang.org/fake",
+ Files: fm{
+ "target/f.go": "package mypkg\nvar Foo = 123\n",
+ "x/y/mypkg": packagestest.Symlink("../../target"), // valid symlink
+ "x/y/apkg": packagestest.Symlink(".."), // symlink loop
+ "myotherpackage/toformat.go": input,
+ },
+ },
+ }.processTest(t, "golang.org/fake", "myotherpackage/toformat.go", nil, nil, want)
+}
+
+func TestImportSymlinksWithIgnore(t *testing.T) {
+ switch runtime.GOOS {
+ case "windows", "plan9":
+ t.Skipf("skipping test on %q as there are no symlinks", runtime.GOOS)
+ }
+
+ const input = `package p
+
+var (
+ _ = fmt.Print
+ _ = mypkg.Foo
+)
+`
+ const want = `package p
+
+import "fmt"
+
+var (
+ _ = fmt.Print
+ _ = mypkg.Foo
+)
+`
+
+ testConfig{
+ gopathOnly: true,
+ module: packagestest.Module{
+ Name: "golang.org/fake",
+ Files: fm{
+ "target/f.go": "package mypkg\nvar Foo = 123\n",
+ "x/y/mypkg": packagestest.Symlink("../../target"), // valid symlink
+ "x/y/apkg": packagestest.Symlink(".."), // symlink loop
+ "myotherpkg/toformat.go": input,
+ "../../.goimportsignore": "golang.org/fake/x/y/mypkg\n",
+ },
+ },
+ }.processTest(t, "golang.org/fake", "myotherpkg/toformat.go", nil, nil, want)
+}
+
+// Test for x/y/v2 convention for package y.
+func TestModuleVersion(t *testing.T) {
+ const input = `package p
+
+import (
+ "fmt"
+
+ "github.com/foo/v2"
+)
+
+var (
+ _ = fmt.Print
+ _ = foo.Foo
+)
+`
+
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "mypkg.com/outpkg",
+ Files: fm{"toformat.go": input},
+ },
+ {
+ Name: "github.com/foo/v2",
+ Files: fm{"x.go": "package foo\n func Foo(){}\n"},
+ },
+ },
+ }.processTest(t, "mypkg.com/outpkg", "toformat.go", nil, nil, input)
+}
+
+// Test for correctly identifying the name of a vendored package when it
+// differs from its directory name. In this test, the import line
+// "mypkg.com/mypkg_v1" would be removed if goimports wasn't able to detect
+// that the package name is "mypkg".
+func TestVendorPackage(t *testing.T) {
+ const input = `package p
+import (
+ "fmt"
+ "mypkg.com/mypkg_v1"
+)
+var _, _ = fmt.Print, mypkg.Foo
+`
+
+ const want = `package p
+
+import (
+ "fmt"
+
+ mypkg "mypkg.com/mypkg_v1"
+)
+
+var _, _ = fmt.Print, mypkg.Foo
+`
+
+ testConfig{
+ gopathOnly: true,
+ module: packagestest.Module{
+ Name: "mypkg.com/outpkg",
+ Files: fm{
+ "vendor/mypkg.com/mypkg_v1/f.go": "package mypkg\nvar Foo = 123\n",
+ "toformat.go": input,
+ },
+ },
+ }.processTest(t, "mypkg.com/outpkg", "toformat.go", nil, nil, want)
+}
+
+func TestInternal(t *testing.T) {
+ const input = `package bar
+
+var _ = race.Acquire
+`
+ const importAdded = `package bar
+
+import "foo.com/internal/race"
+
+var _ = race.Acquire
+`
+
+ // Packages under the same directory should be able to use internal packages.
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "internal/race/x.go": "package race\n func Acquire(){}\n",
+ "bar/x.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "bar/x.go", nil, nil, importAdded)
+
+ // Packages outside the same directory should not.
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "foo.com",
+ Files: fm{"internal/race/x.go": "package race\n func Acquire(){}\n"},
+ },
+ {
+ Name: "bar.com",
+ Files: fm{"x.go": input},
+ },
+ },
+ }.processTest(t, "bar.com", "x.go", nil, nil, input)
+}
+
+func TestProcessVendor(t *testing.T) {
+ const input = `package p
+
+var _ = hpack.HuffmanDecode
+`
+ const want = `package p
+
+import "golang.org/x/net/http2/hpack"
+
+var _ = hpack.HuffmanDecode
+`
+ testConfig{
+ gopathOnly: true,
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "vendor/golang.org/x/net/http2/hpack/huffman.go": "package hpack\nfunc HuffmanDecode() { }\n",
+ "bar/x.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "bar/x.go", nil, nil, want)
+}
+
+func TestFindStdlib(t *testing.T) {
+ tests := []struct {
+ pkg string
+ symbols []string
+ want string
+ }{
+ {"http", []string{"Get"}, "net/http"},
+ {"http", []string{"Get", "Post"}, "net/http"},
+ {"http", []string{"Get", "Foo"}, ""},
+ {"bytes", []string{"Buffer"}, "bytes"},
+ {"ioutil", []string{"Discard"}, "io/ioutil"},
+ }
+ for _, tt := range tests {
+ input := "package p\n"
+ for _, sym := range tt.symbols {
+ input += fmt.Sprintf("var _ = %s.%s\n", tt.pkg, sym)
+ }
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{"x.go": input},
+ },
+ }.test(t, func(t *goimportTest) {
+ buf, err := t.process("foo.com", "x.go", nil, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got := string(buf); !strings.Contains(got, tt.want) {
+ t.Errorf("Process(%q) = %q, wanted it to contain %q", input, buf, tt.want)
+ }
+ })
+ }
+}
+
+// https://golang.org/issue/31814
+func TestStdlibNotPrefixed(t *testing.T) {
+ const input = `package p
+var _ = bytes.Buffer
+`
+ const want = `package p
+
+import "bytes"
+
+var _ = bytes.Buffer
+`
+ // Force a scan of the stdlib.
+ savedStdlib := stdlib
+ defer func() { stdlib = savedStdlib }()
+ stdlib = map[string][]string{}
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "ignored.com",
+ Files: fm{"x.go": "package x"},
+ },
+ }.test(t, func(t *goimportTest) {
+ // Run in GOROOT/src so that the std module shows up in go list -m all.
+ t.env.WorkingDir = filepath.Join(t.goroot, "src")
+ got, err := t.processNonModule(filepath.Join(t.goroot, "src/x.go"), []byte(input), nil)
+ if err != nil {
+ t.Fatalf("Process() = %v", err)
+ }
+ if string(got) != want {
+ t.Errorf("Got:\n%s\nWant:\n%s", got, want)
+ }
+ })
+}
+
+func TestStdlibSelfImports(t *testing.T) {
+ const input = `package ecdsa
+
+var _ = ecdsa.GenerateKey
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "ignored.com",
+ Files: fm{"x.go": "package x"},
+ },
+ }.test(t, func(t *goimportTest) {
+ got, err := t.processNonModule(filepath.Join(t.goroot, "src/crypto/ecdsa/foo.go"), []byte(input), nil)
+ if err != nil {
+ t.Fatalf("Process() = %v", err)
+ }
+ if string(got) != input {
+ t.Errorf("Got:\n%s\nWant:\n%s", got, input)
+ }
+ })
+}
+
+type testConfig struct {
+ gopathOnly bool
+ module packagestest.Module
+ modules []packagestest.Module
+}
+
+// fm is the type for a packagestest.Module's Files, abbreviated for shorter lines.
+type fm map[string]interface{}
+
+func (c testConfig) test(t *testing.T, fn func(*goimportTest)) {
+ t.Helper()
+
+ if c.module.Name != "" {
+ c.modules = []packagestest.Module{c.module}
+ }
+
+ for _, exporter := range packagestest.All {
+ t.Run(exporter.Name(), func(t *testing.T) {
+ t.Helper()
+ if c.gopathOnly && exporter.Name() == "Modules" {
+ t.Skip("test marked GOPATH-only")
+ }
+ exported := packagestest.Export(t, exporter, c.modules)
+ defer exported.Cleanup()
+
+ env := map[string]string{}
+ for _, kv := range exported.Config.Env {
+ split := strings.SplitN(kv, "=", 2)
+ env[split[0]] = split[1]
+ }
+ it := &goimportTest{
+ T: t,
+ env: &ProcessEnv{
+ Env: env,
+ WorkingDir: exported.Config.Dir,
+ GocmdRunner: &gocommand.Runner{},
+ },
+ exported: exported,
+ }
+ if *testDebug {
+ it.env.Logf = log.Printf
+ }
+ // packagestest clears out GOROOT to work around golang/go#32849,
+ // which isn't relevant here. Fill it back in so we can find the standard library.
+ it.env.Env["GOROOT"] = build.Default.GOROOT
+ it.goroot = build.Default.GOROOT
+
+ fn(it)
+ })
+ }
+}
+
+func (c testConfig) processTest(t *testing.T, module, file string, contents []byte, opts *Options, want string) {
+ t.Helper()
+ c.test(t, func(t *goimportTest) {
+ t.Helper()
+ t.assertProcessEquals(module, file, contents, opts, want)
+ })
+}
+
+type goimportTest struct {
+ *testing.T
+ goroot string
+ env *ProcessEnv
+ exported *packagestest.Exported
+}
+
+func (t *goimportTest) process(module, file string, contents []byte, opts *Options) ([]byte, error) {
+ t.Helper()
+ f := t.exported.File(module, file)
+ if f == "" {
+ t.Fatalf("%v not found in exported files (typo in filename?)", file)
+ }
+ return t.processNonModule(f, contents, opts)
+}
+
+func (t *goimportTest) processNonModule(file string, contents []byte, opts *Options) ([]byte, error) {
+ if contents == nil {
+ var err error
+ contents, err = ioutil.ReadFile(file)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if opts == nil {
+ opts = &Options{Comments: true, TabIndent: true, TabWidth: 8}
+ }
+ // ProcessEnv is not safe for concurrent use. Make a copy.
+ opts.Env = t.env.CopyConfig()
+ return Process(file, contents, opts)
+}
+
+func (t *goimportTest) assertProcessEquals(module, file string, contents []byte, opts *Options, want string) {
+ buf, err := t.process(module, file, contents, opts)
+ if err != nil {
+ t.Fatalf("Process() = %v", err)
+ }
+ if string(buf) != want {
+ t.Errorf("Got:\n%s\nWant:\n%s", buf, want)
+ }
+}
+
+// Tests that added imports are renamed when the import path's base doesn't
+// match its package name.
+func TestRenameWhenPackageNameMismatch(t *testing.T) {
+ const input = `package main
+ const Y = bar.X`
+
+ const want = `package main
+
+import bar "foo.com/foo/bar/baz"
+
+const Y = bar.X
+`
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "foo/bar/baz/x.go": "package bar \n const X = 1",
+ "test/t.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "test/t.go", nil, nil, want)
+}
+
+// Tests that an existing import with badly mismatched path/name has its name
+// correctly added. See #28645 and #29041.
+func TestAddNameToMismatchedImport(t *testing.T) {
+ const input = `package main
+
+import (
+"foo.com/a.thing"
+"foo.com/surprise"
+"foo.com/v1"
+"foo.com/other/v2"
+"foo.com/other/v3"
+"foo.com/go-thing"
+"foo.com/go-wrong"
+)
+
+var _ = []interface{}{bar.X, v1.Y, a.A, v2.V2, other.V3, thing.Thing, gow.Wrong}`
+
+ const want = `package main
+
+import (
+ "foo.com/a.thing"
+ "foo.com/go-thing"
+ gow "foo.com/go-wrong"
+ v2 "foo.com/other/v2"
+ "foo.com/other/v3"
+ bar "foo.com/surprise"
+ v1 "foo.com/v1"
+)
+
+var _ = []interface{}{bar.X, v1.Y, a.A, v2.V2, other.V3, thing.Thing, gow.Wrong}
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "a.thing/a.go": "package a \n const A = 1",
+ "surprise/x.go": "package bar \n const X = 1",
+ "v1/x.go": "package v1 \n const Y = 1",
+ "other/v2/y.go": "package v2 \n const V2 = 1",
+ "other/v3/z.go": "package other \n const V3 = 1",
+ "go-thing/b.go": "package thing \n const Thing = 1",
+ "go-wrong/b.go": "package gow \n const Wrong = 1",
+ "test/t.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "test/t.go", nil, nil, want)
+}
+
+// Tests that the LocalPrefix option causes imports
+// to be added into a later group (num=3).
+func TestLocalPrefix(t *testing.T) {
+ tests := []struct {
+ name string
+ modules []packagestest.Module
+ localPrefix string
+ src string
+ want string
+ }{
+ {
+ name: "one_local",
+ modules: []packagestest.Module{
+ {
+ Name: "foo.com",
+ Files: fm{
+ "bar/bar.go": "package bar \n const X = 1",
+ },
+ },
+ },
+ localPrefix: "foo.com/",
+ src: "package main \n const Y = bar.X \n const _ = runtime.GOOS",
+ want: `package main
+
+import (
+ "runtime"
+
+ "foo.com/bar"
+)
+
+const Y = bar.X
+const _ = runtime.GOOS
+`,
+ },
+ {
+ name: "two_local",
+ modules: []packagestest.Module{
+ {
+ Name: "foo.com",
+ Files: fm{
+ "foo/foo.go": "package foo \n const X = 1",
+ "foo/bar/bar.go": "package bar \n const X = 1",
+ },
+ },
+ },
+ localPrefix: "foo.com/foo",
+ src: "package main \n const Y = bar.X \n const Z = foo.X \n const _ = runtime.GOOS",
+ want: `package main
+
+import (
+ "runtime"
+
+ "foo.com/foo"
+ "foo.com/foo/bar"
+)
+
+const Y = bar.X
+const Z = foo.X
+const _ = runtime.GOOS
+`,
+ },
+ {
+ name: "three_prefixes",
+ modules: []packagestest.Module{
+ {
+ Name: "example.org/pkg",
+ Files: fm{"pkg.go": "package pkg \n const A = 1"},
+ },
+ {
+ Name: "foo.com",
+ Files: fm{"bar/bar.go": "package bar \n const B = 1"},
+ },
+ {
+ Name: "code.org/r/p",
+ Files: fm{"expproj/expproj.go": "package expproj \n const C = 1"},
+ },
+ },
+ localPrefix: "example.org/pkg,foo.com/,code.org",
+ src: "package main \n const X = pkg.A \n const Y = bar.B \n const Z = expproj.C \n const _ = runtime.GOOS",
+ want: `package main
+
+import (
+ "runtime"
+
+ "code.org/r/p/expproj"
+ "example.org/pkg"
+ "foo.com/bar"
+)
+
+const X = pkg.A
+const Y = bar.B
+const Z = expproj.C
+const _ = runtime.GOOS
+`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ testConfig{
+ // The module being processed has to be first so it's the primary module.
+ modules: append([]packagestest.Module{{
+ Name: "test.com",
+ Files: fm{"t.go": tt.src},
+ }}, tt.modules...),
+ }.test(t, func(t *goimportTest) {
+ options := &Options{
+ LocalPrefix: tt.localPrefix,
+ TabWidth: 8,
+ TabIndent: true,
+ Comments: true,
+ Fragment: true,
+ }
+ t.assertProcessEquals("test.com", "t.go", nil, options, tt.want)
+ })
+ })
+ }
+}
+
+// Tests that "package documentation" files are ignored.
+func TestIgnoreDocumentationPackage(t *testing.T) {
+ const input = `package x
+
+const Y = foo.X
+`
+ const want = `package x
+
+import "foo.com/foo"
+
+const Y = foo.X
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "foo/foo.go": "package foo\nconst X = 1\n",
+ "foo/doc.go": "package documentation \n // just to confuse things\n",
+ "x/x.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "x/x.go", nil, nil, want)
+}
+
+// Tests importPathToNameGoPathParse and in particular that it stops
+// after finding the first non-documentation package name, not
+// reporting an error on inconsistent package names (since it should
+// never make it that far).
+func TestImportPathToNameGoPathParse(t *testing.T) {
+ testConfig{
+ module: packagestest.Module{
+ Name: "example.net/pkg",
+ Files: fm{
+ "doc.go": "package documentation\n", // ignored
+ "gen.go": "package main\n", // also ignored
+ "pkg.go": "package the_pkg_name_to_find\n and this syntax error is ignored because of parser.PackageClauseOnly",
+ "z.go": "package inconsistent\n", // inconsistent but ignored
+ },
+ },
+ }.test(t, func(t *goimportTest) {
+ if strings.Contains(t.Name(), "GoPackages") {
+ t.Skip("go/packages does not ignore package main")
+ }
+ r, err := t.env.GetResolver()
+ if err != nil {
+ t.Fatal(err)
+ }
+ srcDir := filepath.Dir(t.exported.File("example.net/pkg", "z.go"))
+ names, err := r.loadPackageNames([]string{"example.net/pkg"}, srcDir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ const want = "the_pkg_name_to_find"
+ if got := names["example.net/pkg"]; got != want {
+ t.Errorf("loadPackageNames(..) = %q; want %q", got, want)
+ }
+ })
+}
+
+func TestIgnoreConfiguration(t *testing.T) {
+ const input = `package x
+
+const _ = pkg.X
+`
+ const want = `package x
+
+import "foo.com/otherwise-longer-so-worse-example/foo/pkg"
+
+const _ = pkg.X
+`
+
+ testConfig{
+ gopathOnly: true,
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "../.goimportsignore": "# comment line\n\n foo.com/example", // tests comment, blank line, whitespace trimming
+ "example/pkg/pkg.go": "package pkg\nconst X = 1",
+ "otherwise-longer-so-worse-example/foo/pkg/pkg.go": "package pkg\nconst X = 1",
+ "x/x.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "x/x.go", nil, nil, want)
+}
+
+// Skip "node_modules" directory.
+func TestSkipNodeModules(t *testing.T) {
+ const input = `package x
+
+const _ = pkg.X
+`
+ const want = `package x
+
+import "foo.com/otherwise-longer/not_modules/pkg"
+
+const _ = pkg.X
+`
+
+ testConfig{
+ gopathOnly: true,
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "example/node_modules/pkg/a.go": "package pkg\nconst X = 1",
+ "otherwise-longer/not_modules/pkg/a.go": "package pkg\nconst X = 1",
+ "x/x.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "x/x.go", nil, nil, want)
+}
+
+// Tests that package global variables with the same name and function name as
+// a function in a separate package do not result in an import which masks
+// the global variable
+func TestGlobalImports(t *testing.T) {
+ const usesGlobal = `package pkg
+
+func doSomething() {
+ t := time.Now()
+}
+`
+
+ const declaresGlobal = `package pkg
+
+type Time struct{}
+
+func (t Time) Now() Time {
+ return Time{}
+}
+
+var time Time
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "pkg/uses.go": usesGlobal,
+ "pkg/global.go": declaresGlobal,
+ },
+ },
+ }.processTest(t, "foo.com", "pkg/uses.go", nil, nil, usesGlobal)
+}
+
+// Some people put multiple packages' files in the same directory. Globals
+// declared in other packages should be ignored.
+func TestGlobalImports_DifferentPackage(t *testing.T) {
+ const declaresGlobal = `package main
+var fmt int
+`
+ const input = `package pkg
+var _ = fmt.Printf
+`
+ const want = `package pkg
+
+import "fmt"
+
+var _ = fmt.Printf
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "pkg/main.go": declaresGlobal,
+ "pkg/uses.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "pkg/uses.go", nil, nil, want)
+}
+
+func TestGlobalImports_MultipleMains(t *testing.T) {
+ const declaresGlobal = `package main
+var fmt int
+`
+ const input = `package main
+import "fmt"
+var _, _ = fmt.Printf, bytes.Equal
+`
+ const want = `package main
+
+import (
+ "bytes"
+ "fmt"
+)
+
+var _, _ = fmt.Printf, bytes.Equal
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "pkg/main.go": declaresGlobal,
+ "pkg/uses.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "pkg/uses.go", nil, nil, want)
+}
+
+// Tests that sibling files - other files in the same package - can provide an
+// import that may not be the default one otherwise.
+func TestSiblingImports(t *testing.T) {
+
+ // provide is the sibling file that provides the desired import.
+ const provide = `package siblingimporttest
+
+import "local/log"
+import "my/bytes"
+import renamed "fmt"
+
+func LogSomething() {
+ log.Print("Something")
+ bytes.SomeFunc()
+ renamed.Println("Something")
+}
+`
+
+ // need is the file being tested that needs the import.
+ const need = `package siblingimporttest
+
+var _ = bytes.Buffer{}
+
+func LogSomethingElse() {
+ log.Print("Something else")
+ renamed.Println("Yet another")
+}
+`
+
+ // want is the expected result file
+ const want = `package siblingimporttest
+
+import (
+ "bytes"
+ renamed "fmt"
+ "local/log"
+)
+
+var _ = bytes.Buffer{}
+
+func LogSomethingElse() {
+ log.Print("Something else")
+ renamed.Println("Yet another")
+}
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "p/needs_import.go": need,
+ "p/provides_import.go": provide,
+ },
+ },
+ }.processTest(t, "foo.com", "p/needs_import.go", nil, nil, want)
+}
+
+// Tests #29180: a sibling import of the right package with the wrong name is used.
+func TestSiblingImport_Misnamed(t *testing.T) {
+ const sibling = `package main
+import renamed "fmt"
+var _ = renamed.Printf
+`
+ const input = `package pkg
+var _ = fmt.Printf
+`
+ const want = `package pkg
+
+import "fmt"
+
+var _ = fmt.Printf
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "pkg/main.go": sibling,
+ "pkg/uses.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "pkg/uses.go", nil, nil, want)
+
+}
+
+// Tests that an input file's own package is ignored.
+func TestIgnoreOwnPackage(t *testing.T) {
+ const input = `package pkg
+
+const _ = pkg.X
+`
+ const want = `package pkg
+
+const _ = pkg.X
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "pkg/a.go": "package pkg\nconst X = 1",
+ "pkg/b.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "pkg/b.go", nil, nil, want)
+}
+
+func TestExternalTestImportsPackageUnderTest(t *testing.T) {
+ const provide = `package pkg
+func DoIt(){}
+`
+ const input = `package pkg_test
+
+var _ = pkg.DoIt`
+
+ const want = `package pkg_test
+
+import "foo.com/pkg"
+
+var _ = pkg.DoIt
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "pkg/provide.go": provide,
+ "pkg/x_test.go": input,
+ },
+ },
+ }.processTest(t, "foo.com", "pkg/x_test.go", nil, nil, want)
+}
+
+func TestPkgIsCandidate(t *testing.T) {
+ tests := []struct {
+ name string
+ filename string
+ pkgIdent string
+ pkg *pkg
+ want bool
+ }{
+ {
+ name: "normal_match",
+ filename: "/gopath/src/my/pkg/pkg.go",
+ pkgIdent: "client",
+ pkg: &pkg{
+ dir: "/gopath/src/client",
+ importPathShort: "client",
+ },
+ want: true,
+ },
+ {
+ name: "no_match",
+ filename: "/gopath/src/my/pkg/pkg.go",
+ pkgIdent: "zzz",
+ pkg: &pkg{
+ dir: "/gopath/src/client",
+ importPathShort: "client",
+ },
+ want: false,
+ },
+ {
+ name: "match_too_early",
+ filename: "/gopath/src/my/pkg/pkg.go",
+ pkgIdent: "client",
+ pkg: &pkg{
+ dir: "/gopath/src/client/foo/foo/foo",
+ importPathShort: "client/foo/foo",
+ },
+ want: false,
+ },
+ {
+ name: "substring_match",
+ filename: "/gopath/src/my/pkg/pkg.go",
+ pkgIdent: "client",
+ pkg: &pkg{
+ dir: "/gopath/src/foo/go-client",
+ importPathShort: "foo/go-client",
+ },
+ want: true,
+ },
+ {
+ name: "hidden_internal",
+ filename: "/gopath/src/my/pkg/pkg.go",
+ pkgIdent: "client",
+ pkg: &pkg{
+ dir: "/gopath/src/foo/internal/client",
+ importPathShort: "foo/internal/client",
+ },
+ want: false,
+ },
+ {
+ name: "visible_internal",
+ filename: "/gopath/src/foo/bar.go",
+ pkgIdent: "client",
+ pkg: &pkg{
+ dir: "/gopath/src/foo/internal/client",
+ importPathShort: "foo/internal/client",
+ },
+ want: true,
+ },
+ {
+ name: "invisible_vendor",
+ filename: "/gopath/src/foo/bar.go",
+ pkgIdent: "client",
+ pkg: &pkg{
+ dir: "/gopath/src/other/vendor/client",
+ importPathShort: "client",
+ },
+ want: false,
+ },
+ {
+ name: "visible_vendor",
+ filename: "/gopath/src/foo/bar.go",
+ pkgIdent: "client",
+ pkg: &pkg{
+ dir: "/gopath/src/foo/vendor/client",
+ importPathShort: "client",
+ },
+ want: true,
+ },
+ {
+ name: "match_with_hyphens",
+ filename: "/gopath/src/foo/bar.go",
+ pkgIdent: "socketio",
+ pkg: &pkg{
+ dir: "/gopath/src/foo/socket-io",
+ importPathShort: "foo/socket-io",
+ },
+ want: true,
+ },
+ {
+ name: "match_with_mixed_case",
+ filename: "/gopath/src/foo/bar.go",
+ pkgIdent: "fooprod",
+ pkg: &pkg{
+ dir: "/gopath/src/foo/FooPROD",
+ importPathShort: "foo/FooPROD",
+ },
+ want: true,
+ },
+ {
+ name: "matches_with_hyphen_and_caps",
+ filename: "/gopath/src/foo/bar.go",
+ pkgIdent: "fooprod",
+ pkg: &pkg{
+ dir: "/gopath/src/foo/Foo-PROD",
+ importPathShort: "foo/Foo-PROD",
+ },
+ want: true,
+ },
+ }
+ for i, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ refs := references{tt.pkgIdent: nil}
+ got := pkgIsCandidate(tt.filename, refs, tt.pkg)
+ if got != tt.want {
+ t.Errorf("test %d. pkgIsCandidate(%q, %q, %+v) = %v; want %v",
+ i, tt.filename, tt.pkgIdent, *tt.pkg, got, tt.want)
+ }
+ })
+ }
+}
+
+// Issue 20941: this used to panic on Windows.
+func TestProcessStdin(t *testing.T) {
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ },
+ }.test(t, func(t *goimportTest) {
+ got, err := t.processNonModule("<standard input>", []byte("package main\nfunc main() {\n\tfmt.Println(123)\n}\n"), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !strings.Contains(string(got), `"fmt"`) {
+ t.Errorf("expected fmt import; got: %s", got)
+ }
+ })
+}
+
+// Tests LocalPackagePromotion when there is a local package that matches, it
+// should be the closest match.
+// https://golang.org/issues/17557
+func TestLocalPackagePromotion(t *testing.T) {
+ const input = `package main
+var c = &config.SystemConfig{}
+`
+ const want = `package main
+
+import "mycompany.net/tool/config"
+
+var c = &config.SystemConfig{}
+`
+
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "config.net/config",
+ Files: fm{"config.go": "package config\n type SystemConfig struct {}"}, // Will match but should not be first choice
+ },
+ {
+ Name: "mycompany.net/config",
+ Files: fm{"config.go": "package config\n type SystemConfig struct {}"}, // Will match but should not be first choice
+ },
+ {
+ Name: "mycompany.net/tool",
+ Files: fm{
+ "config/config.go": "package config\n type SystemConfig struct {}", // Local package should be promoted over shorter package
+ "main.go": input,
+ },
+ },
+ },
+ }.processTest(t, "mycompany.net/tool", "main.go", nil, nil, want)
+}
+
+// Tests FindImportInLocalGoFiles looks at the import lines for other Go files in the
+// local directory, since the user is likely to import the same packages in the current
+// Go file. If an import is found that satisfies the need, it should be used over the
+// standard library.
+// https://golang.org/issues/17557
+func TestFindImportInLocalGoFiles(t *testing.T) {
+ const input = `package main
+ var _ = &bytes.Buffer{}`
+
+ const want = `package main
+
+import "bytes.net/bytes"
+
+var _ = &bytes.Buffer{}
+`
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "mycompany.net/tool",
+ Files: fm{
+ "io.go": "package main\n import \"bytes.net/bytes\"\n var _ = &bytes.Buffer{}", // Contains package import that will cause stdlib to be ignored
+ "main.go": input,
+ },
+ },
+ {
+ Name: "bytes.net/bytes",
+ Files: fm{"bytes.go": "package bytes\n type Buffer struct {}"}, // Should be selected over standard library
+ },
+ },
+ }.processTest(t, "mycompany.net/tool", "main.go", nil, nil, want)
+}
+
+func TestInMemoryFile(t *testing.T) {
+ const input = `package main
+ var _ = &bytes.Buffer{}`
+
+ const want = `package main
+
+import "bytes"
+
+var _ = &bytes.Buffer{}
+`
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{"x.go": "package x\n"},
+ },
+ }.processTest(t, "foo.com", "x.go", []byte(input), nil, want)
+}
+
+func TestImportNoGoFiles(t *testing.T) {
+ const input = `package main
+ var _ = &bytes.Buffer{}`
+
+ const want = `package main
+
+import "bytes"
+
+var _ = &bytes.Buffer{}
+`
+ testConfig{
+ module: packagestest.Module{
+ Name: "mycompany.net",
+ },
+ }.test(t, func(t *goimportTest) {
+ buf, err := t.processNonModule("mycompany.net/tool/main.go", []byte(input), nil)
+ if err != nil {
+ t.Fatalf("Process() = %v", err)
+ }
+ if string(buf) != want {
+ t.Errorf("Got:\n%s\nWant:\n%s", buf, want)
+ }
+ })
+
+}
+
+// Ensures a token as large as 500000 bytes can be handled
+// https://golang.org/issues/18201
+func TestProcessLargeToken(t *testing.T) {
+ largeString := strings.Repeat("x", 500000)
+
+ input := `package testimports
+
+import (
+ "bytes"
+)
+
+const s = fmt.Sprintf("%s", "` + largeString + `")
+var _ = bytes.Buffer{}
+
+// end
+`
+
+ want := `package testimports
+
+import (
+ "bytes"
+ "fmt"
+)
+
+const s = fmt.Sprintf("%s", "` + largeString + `")
+
+var _ = bytes.Buffer{}
+
+// end
+`
+
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{"foo.go": input},
+ },
+ }.processTest(t, "foo.com", "foo.go", nil, nil, want)
+}
+
+// Tests that an external test package will import the package under test if it
+// also uses symbols exported only in test files.
+// https://golang.org/issues/29979
+func TestExternalTest(t *testing.T) {
+ const input = `package a_test
+func TestX() {
+ a.X()
+ a.Y()
+}
+`
+ const want = `package a_test
+
+import "foo.com/a"
+
+func TestX() {
+ a.X()
+ a.Y()
+}
+`
+
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "foo.com/a",
+ Files: fm{
+ "a.go": "package a\n func X() {}",
+ "export_test.go": "package a\n func Y() {}",
+ "a_test.go": input,
+ },
+ },
+ },
+ }.processTest(t, "foo.com/a", "a_test.go", nil, nil, want)
+}
+
+// TestGetCandidates tests that get packages finds packages
+// with correct priorities.
+func TestGetCandidates(t *testing.T) {
+ type res struct {
+ relevance float64
+ name, path string
+ }
+ want := []res{
+ {0, "bytes", "bytes"},
+ {0, "http", "net/http"},
+ {0, "rand", "crypto/rand"},
+ {0, "bar", "bar.com/bar"},
+ {0, "foo", "foo.com/foo"},
+ }
+
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "bar.com",
+ Files: fm{"bar/bar.go": "package bar\n"},
+ },
+ {
+ Name: "foo.com",
+ Files: fm{"foo/foo.go": "package foo\n"},
+ },
+ },
+ }.test(t, func(t *goimportTest) {
+ var mu sync.Mutex
+ var got []res
+ add := func(c ImportFix) {
+ mu.Lock()
+ defer mu.Unlock()
+ for _, w := range want {
+ if c.StmtInfo.ImportPath == w.path {
+ got = append(got, res{c.Relevance, c.IdentName, c.StmtInfo.ImportPath})
+ }
+ }
+ }
+ if err := GetAllCandidates(context.Background(), add, "", "x.go", "x", t.env); err != nil {
+ t.Fatalf("GetAllCandidates() = %v", err)
+ }
+ // Sort, then clear out relevance so it doesn't mess up the DeepEqual.
+ sort.Slice(got, func(i, j int) bool {
+ ri, rj := got[i], got[j]
+ if ri.relevance != rj.relevance {
+ return ri.relevance > rj.relevance // Highest first.
+ }
+ return ri.name < rj.name
+ })
+ for i := range got {
+ got[i].relevance = 0
+ }
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("wanted results in order %v, got %v", want, got)
+ }
+ })
+}
+
+func TestGetImportPaths(t *testing.T) {
+ type res struct {
+ relevance float64
+ name, path string
+ }
+ want := []res{
+ {0, "http", "net/http"},
+ {0, "net", "net"},
+ {0, "neta", "neta.com/neta"},
+ }
+
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "neta.com",
+ Files: fm{"neta/neta.go": "package neta\n"},
+ },
+ },
+ }.test(t, func(t *goimportTest) {
+ var mu sync.Mutex
+ var got []res
+ add := func(c ImportFix) {
+ mu.Lock()
+ defer mu.Unlock()
+ for _, w := range want {
+ if c.StmtInfo.ImportPath == w.path {
+ got = append(got, res{c.Relevance, c.IdentName, c.StmtInfo.ImportPath})
+ }
+ }
+ }
+ if err := GetImportPaths(context.Background(), add, "ne", "x.go", "x", t.env); err != nil {
+ t.Fatalf("GetImportPaths() = %v", err)
+ }
+ // Sort, then clear out relevance so it doesn't mess up the DeepEqual.
+ sort.Slice(got, func(i, j int) bool {
+ ri, rj := got[i], got[j]
+ if ri.relevance != rj.relevance {
+ return ri.relevance > rj.relevance // Highest first.
+ }
+ return ri.name < rj.name
+ })
+ for i := range got {
+ got[i].relevance = 0
+ }
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("wanted results in order %v, got %v", want, got)
+ }
+ })
+}
+
+func TestGetPackageCompletions(t *testing.T) {
+ type res struct {
+ relevance float64
+ name, path, symbol string
+ }
+ want := []res{
+ {0, "rand", "math/rand", "Seed"},
+ {0, "rand", "bar.com/rand", "Bar"},
+ }
+
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "bar.com",
+ Files: fm{"rand/bar.go": "package rand\nvar Bar int\n"},
+ },
+ },
+ }.test(t, func(t *goimportTest) {
+ var mu sync.Mutex
+ var got []res
+ add := func(c PackageExport) {
+ mu.Lock()
+ defer mu.Unlock()
+ for _, csym := range c.Exports {
+ for _, w := range want {
+ if c.Fix.StmtInfo.ImportPath == w.path && csym == w.symbol {
+ got = append(got, res{c.Fix.Relevance, c.Fix.IdentName, c.Fix.StmtInfo.ImportPath, csym})
+ }
+ }
+ }
+ }
+ if err := GetPackageExports(context.Background(), add, "rand", "x.go", "x", t.env); err != nil {
+ t.Fatalf("getPackageCompletions() = %v", err)
+ }
+ // Sort, then clear out relevance so it doesn't mess up the DeepEqual.
+ sort.Slice(got, func(i, j int) bool {
+ ri, rj := got[i], got[j]
+ if ri.relevance != rj.relevance {
+ return ri.relevance > rj.relevance // Highest first.
+ }
+ return ri.name < rj.name
+ })
+ for i := range got {
+ got[i].relevance = 0
+ }
+ if !reflect.DeepEqual(want, got) {
+ t.Errorf("wanted results in order %v, got %v", want, got)
+ }
+ })
+}
+
+// Tests #34895: process should not panic on concurrent calls.
+func TestConcurrentProcess(t *testing.T) {
+ testConfig{
+ module: packagestest.Module{
+ Name: "foo.com",
+ Files: fm{
+ "p/first.go": `package foo
+
+func _() {
+ fmt.Println()
+}
+`,
+ "p/second.go": `package foo
+
+import "fmt"
+
+func _() {
+ fmt.Println()
+ imports.Bar() // not imported.
+}
+`,
+ },
+ },
+ }.test(t, func(t *goimportTest) {
+ var (
+ n = 10
+ wg sync.WaitGroup
+ )
+ wg.Add(n)
+ for i := 0; i < n; i++ {
+ go func() {
+ defer wg.Done()
+ _, err := t.process("foo.com", "p/first.go", nil, nil)
+ if err != nil {
+ t.Error(err)
+ }
+ }()
+ }
+ wg.Wait()
+ })
+}
+
+func TestNonlocalDot(t *testing.T) {
+ const input = `package main
+import (
+ "fmt"
+)
+var _, _ = fmt.Sprintf, dot.Dot
+`
+ const want = `package main
+
+import (
+ "fmt"
+ "noninternet/dot.v1/dot"
+)
+
+var _, _ = fmt.Sprintf, dot.Dot
+`
+ testConfig{
+ modules: []packagestest.Module{
+ {
+ Name: "golang.org/fake",
+ Files: fm{"x.go": input},
+ },
+ {
+ Name: "noninternet/dot.v1",
+ Files: fm{
+ "dot/dot.go": "package dot\nfunc Dot(){}\n",
+ },
+ },
+ },
+ gopathOnly: true, // our modules testing setup doesn't allow modules without dots.
+ }.processTest(t, "golang.org/fake", "x.go", nil, nil, want)
+}