1
0
mirror of https://github.com/golang/go synced 2024-11-18 14:14:46 -07:00
go/imports/fix_test.go
haya14busa 84a35ef54d go/ast/astutil: new third-party imports shouldn't go in the std group
Before this change, astutil would only do a prefix match of a new import
with all the existing ones, to try to place it in the correct group. If
none was found, the new import would be placed at the beginning of the
first import group.

This works well for new std imports, but it doesn't work well for new
third-party packages that don't share any prefix with any of the
existing imports.

Example:

	import (
		"time"

		"github.com/golang/snappy"
	)

When adding "golang.org/x/sys/unix" with astutil.AddImport, the import
is inserted as follows:

	import (
		"golang.org/x/sys/unix"
		"time"

		"github.com/golang/snappy"
	)

And goimports reorganizes the imports to separate std and third-party
packages:

	import (
		"time"

		"golang.org/x/sys/unix"

		"github.com/golang/snappy"
	)

We usually don't want to introduce a new import group; in most cases,
the desired behavior is separating std from third-party packages.

With this CL, new imports that don't share prefix with any existing ones
will be placed with the first group of third-party imports, if any
exist. If no third-party import group exists, a new one will be added.
In the case of our example above, this will be the new outcome:

	import (
		"time"

		"github.com/golang/snappy"
		"golang.org/x/sys/unix"
	)

Fixes golang/go#19190.

Change-Id: Id4630015c029bd815234a6c8726cb97f4af16f1c
Reviewed-on: https://go-review.googlesource.com/37552
Run-TryBot: Josh Bleecher Snyder <josharian@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Josh Bleecher Snyder <josharian@gmail.com>
2017-08-15 00:39:00 +00:00

1881 lines
36 KiB
Go

// 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 (
"bytes"
"flag"
"go/build"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
)
var only = flag.String("only", "", "If non-empty, the fix test to run")
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"
"appengine"
)
func bar() {
var b bytes.Buffer
_ = appengine.IsDevServer
fmt.Println(b.String())
}
`,
out: `package foo
import (
"bytes"
"fmt"
"appengine"
)
func bar() {
var b bytes.Buffer
_ = appengine.IsDevServer
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"
"appengine"
)
func bar() {
_ = math.NaN
_ = fmt.Sprintf
_ = appengine.IsDevServer
}
`,
out: `package foo
import (
"fmt"
"math"
"appengine"
)
func bar() {
_ = math.NaN
_ = fmt.Sprintf
_ = appengine.IsDevServer
}
`,
},
// 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
}
`,
},
// 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
_, _ = appengine.FooSomething, user.Current
}
`,
out: `package foo
import (
"fmt"
"os"
"appengine"
"appengine/user"
)
func foo() {
_, _ = os.Args, fmt.Println
_, _ = appengine.FooSomething, user.Current
}
`,
},
// 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.Foo
}
`,
out: `package foo
import (
"fmt"
"net"
"code.google.com/p/snappy-go/snappy"
)
func f() {
_ = net.Dial
_ = fmt.Printf
_ = snappy.Foo
}
`,
},
// Blank line between standard library and third-party stuff.
{
name: "blank_line_separating_std_and_third_party",
in: `package foo
import (
"code.google.com/p/snappy-go/snappy"
"fmt"
"net"
)
func f() {
_ = net.Dial
_ = fmt.Printf
_ = snappy.Foo
}
`,
out: `package foo
import (
"fmt"
"net"
"code.google.com/p/snappy-go/snappy"
)
func f() {
_ = net.Dial
_ = fmt.Printf
_ = snappy.Foo
}
`,
},
// golang.org/issue/6884
{
name: "issue 6884",
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: "issue 7132",
in: `package main
import (
"fmt"
"gu"
"github.com/foo/bar"
)
var (
a = bar.a
b = gu.a
c = fmt.Printf
)
`,
out: `package main
import (
"fmt"
"gu"
"github.com/foo/bar"
)
var (
a = bar.a
b = gu.a
c = fmt.Printf
)
`,
},
{
name: "renamed package",
in: `package main
var _ = str.HasPrefix
`,
out: `package main
import str "strings"
var _ = str.HasPrefix
`,
},
{
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: "issue 7679",
in: `package main
import (
"fmt"
"github.com/foo/bar"
"github.com/foo/qux"
)
func main() {
var _ = fmt.Println
//var _ = bar.A
var _ = qux.B
}
`,
out: `package main
import (
"fmt"
"github.com/foo/qux"
)
func main() {
var _ = fmt.Println
//var _ = bar.A
var _ = qux.B
}
`,
},
// Blank line can be added before all types of import declarations.
// golang.org/issue/7866
{
name: "issue 7866",
in: `package main
import (
"fmt"
renamed_bar "github.com/foo/bar"
. "github.com/foo/baz"
"io"
_ "github.com/foo/qux"
"strings"
)
func main() {
_, _, _, _, _ = fmt.Errorf, io.Copy, strings.Contains, renamed_bar.A, B
}
`,
out: `package main
import (
"fmt"
renamed_bar "github.com/foo/bar"
"io"
. "github.com/foo/baz"
"strings"
_ "github.com/foo/qux"
)
func main() {
_, _, _, _, _ = fmt.Errorf, io.Copy, strings.Contains, renamed_bar.A, B
}
`,
},
// Non-idempotent comment formatting
// golang.org/issue/8035
{
name: "issue 8035",
in: `package main
import (
"fmt" // A
"go/ast" // B
_ "launchpad.net/gocheck" // C
)
func main() { _, _ = fmt.Print, ast.Walk }
`,
out: `package main
import (
"fmt" // A
"go/ast" // B
_ "launchpad.net/gocheck" // C
)
func main() { _, _ = fmt.Print, ast.Walk }
`,
},
// Failure to delete all duplicate imports
// golang.org/issue/8459
{
name: "issue 8459",
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: "issue 9961",
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: "issue 8149",
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: "issue 8149",
in: `package main
import (
"fmt"
x "fmt"
)
func main() { fmt.Println() }
`,
out: `package main
import (
"fmt"
)
func main() { fmt.Println() }
`,
},
// FormatOnly
{
name: "format only",
formatOnly: true,
in: `package main
import (
"fmt"
"golang.org/x/foo"
)
func main() {}
`,
out: `package main
import (
"fmt"
"golang.org/x/foo"
)
func main() {}
`,
},
{
name: "do not make grouped imports non-grouped",
in: `package p
import (
"bytes"
"fmt"
)
var _ = fmt.Sprintf
`,
out: `package p
import (
"fmt"
)
var _ = fmt.Sprintf
`,
},
{
name: "issue #19190 1",
in: `package main
import (
"time"
)
func main() {
_ = snappy.Encode
_ = p.P
_ = time.Parse
}
`,
out: `package main
import (
"time"
"code.google.com/p/snappy-go/snappy"
"rsc.io/p"
)
func main() {
_ = snappy.Encode
_ = p.P
_ = time.Parse
}
`,
},
{
name: "issue #19190 2",
in: `package main
import (
"time"
"code.google.com/p/snappy-go/snappy"
)
func main() {
_ = snappy.Encode
_ = p.P
_ = time.Parse
}
`,
out: `package main
import (
"time"
"code.google.com/p/snappy-go/snappy"
"rsc.io/p"
)
func main() {
_ = snappy.Encode
_ = p.P
_ = time.Parse
}
`,
},
}
func TestFixImports(t *testing.T) {
simplePkgs := map[string]string{
"appengine": "appengine",
"bytes": "bytes",
"fmt": "fmt",
"math": "math",
"os": "os",
"p": "rsc.io/p",
"regexp": "regexp",
"snappy": "code.google.com/p/snappy-go/snappy",
"str": "strings",
"user": "appengine/user",
"zip": "archive/zip",
}
old := findImport
defer func() {
findImport = old
}()
findImport = func(pkgName string, symbols map[string]bool, filename string) (string, bool, error) {
return simplePkgs[pkgName], pkgName == "str", nil
}
options := &Options{
TabWidth: 8,
TabIndent: true,
Comments: true,
Fragment: true,
}
for _, tt := range tests {
options.FormatOnly = tt.formatOnly
if *only != "" && tt.name != *only {
continue
}
buf, err := Process(tt.name+".go", []byte(tt.in), options)
if err != nil {
t.Errorf("error on %q: %v", tt.name, err)
continue
}
if got := string(buf); got != tt.out {
t.Errorf("results diff on %q\nGOT:\n%s\nWANT:\n%s\n", tt.name, got, 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)
}
newGoPath, err := ioutil.TempDir("", "symlinktest")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(newGoPath)
// Create:
// $GOPATH/target/
// $GOPATH/target/f.go // package mypkg\nvar Foo = 123\n
// $GOPATH/src/x/
// $GOPATH/src/x/mypkg => $GOPATH/target // symlink
// $GOPATH/src/x/apkg => $GOPATH/src/x // symlink loop
// Test:
// $GOPATH/src/myotherpkg/toformat.go referencing mypkg.Foo
targetPath := newGoPath + "/target"
if err := os.MkdirAll(targetPath, 0755); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(targetPath+"/f.go", []byte("package mypkg\nvar Foo = 123\n"), 0666); err != nil {
t.Fatal(err)
}
symlinkPath := newGoPath + "/src/x/mypkg"
if err := os.MkdirAll(filepath.Dir(symlinkPath), 0755); err != nil {
t.Fatal(err)
}
if err := os.Symlink(targetPath, symlinkPath); err != nil {
t.Fatal(err)
}
// Add a symlink loop.
if err := os.Symlink(newGoPath+"/src/x", newGoPath+"/src/x/apkg"); err != nil {
t.Fatal(err)
}
withEmptyGoPath(func() {
build.Default.GOPATH = newGoPath
input := `package p
var (
_ = fmt.Print
_ = mypkg.Foo
)
`
output := `package p
import (
"fmt"
"x/mypkg"
)
var (
_ = fmt.Print
_ = mypkg.Foo
)
`
buf, err := Process(newGoPath+"/src/myotherpkg/toformat.go", []byte(input), &Options{})
if err != nil {
t.Fatal(err)
}
if got := string(buf); got != output {
t.Fatalf("results differ\nGOT:\n%s\nWANT:\n%s\n", got, output)
}
})
// Add a .goimportsignore and ensure it is respected.
if err := ioutil.WriteFile(newGoPath+"/src/.goimportsignore", []byte("x/mypkg\n"), 0666); err != nil {
t.Fatal(err)
}
withEmptyGoPath(func() {
build.Default.GOPATH = newGoPath
input := `package p
var (
_ = fmt.Print
_ = mypkg.Foo
)
`
output := `package p
import "fmt"
var (
_ = fmt.Print
_ = mypkg.Foo
)
`
buf, err := Process(newGoPath+"/src/myotherpkg/toformat.go", []byte(input), &Options{})
if err != nil {
t.Fatal(err)
}
if got := string(buf); got != output {
t.Fatalf("ignored results differ\nGOT:\n%s\nWANT:\n%s\n", got, output)
}
})
}
// 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 TestFixImportsVendorPackage(t *testing.T) {
// Skip this test on go versions with no vendor support.
if _, err := os.Stat(filepath.Join(runtime.GOROOT(), "src/vendor")); err != nil {
t.Skip(err)
}
testConfig{
gopathFiles: map[string]string{
"mypkg.com/outpkg/vendor/mypkg.com/mypkg.v1/f.go": "package mypkg\nvar Foo = 123\n",
},
}.test(t, func(t *goimportTest) {
input := `package p
import (
"fmt"
"mypkg.com/mypkg.v1"
)
var (
_ = fmt.Print
_ = mypkg.Foo
)
`
buf, err := Process(filepath.Join(t.gopath, "src/mypkg.com/outpkg/toformat.go"), []byte(input), &Options{})
if err != nil {
t.Fatal(err)
}
if got := string(buf); got != input {
t.Fatalf("results differ\nGOT:\n%s\nWANT:\n%s\n", got, input)
}
})
}
func TestFindImportGoPath(t *testing.T) {
goroot, err := ioutil.TempDir("", "goimports-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(goroot)
origStdlib := stdlib
defer func() {
stdlib = origStdlib
}()
stdlib = nil
withEmptyGoPath(func() {
// Test against imaginary bits/bytes package in std lib
bytesDir := filepath.Join(goroot, "src", "pkg", "bits", "bytes")
for _, tag := range build.Default.ReleaseTags {
// Go 1.4 rearranged the GOROOT tree to remove the "pkg" path component.
if tag == "go1.4" {
bytesDir = filepath.Join(goroot, "src", "bits", "bytes")
}
}
if err := os.MkdirAll(bytesDir, 0755); err != nil {
t.Fatal(err)
}
bytesSrcPath := filepath.Join(bytesDir, "bytes.go")
bytesPkgPath := "bits/bytes"
bytesSrc := []byte(`package bytes
type Buffer2 struct {}
`)
if err := ioutil.WriteFile(bytesSrcPath, bytesSrc, 0775); err != nil {
t.Fatal(err)
}
build.Default.GOROOT = goroot
got, rename, err := findImportGoPath("bytes", map[string]bool{"Buffer2": true}, "x.go")
if err != nil {
t.Fatal(err)
}
if got != bytesPkgPath || rename {
t.Errorf(`findImportGoPath("bytes", Buffer2 ...)=%q, %t, want "%s", false`, got, rename, bytesPkgPath)
}
got, rename, err = findImportGoPath("bytes", map[string]bool{"Missing": true}, "x.go")
if err != nil {
t.Fatal(err)
}
if got != "" || rename {
t.Errorf(`findImportGoPath("bytes", Missing ...)=%q, %t, want "", false`, got, rename)
}
})
}
func init() {
inTests = true
}
func withEmptyGoPath(fn func()) {
testMu.Lock()
dirScanMu.Lock()
populateIgnoreOnce = sync.Once{}
scanGoRootOnce = sync.Once{}
scanGoPathOnce = sync.Once{}
dirScan = nil
ignoredDirs = nil
scanGoRootDone = make(chan struct{})
dirScanMu.Unlock()
oldGOPATH := build.Default.GOPATH
oldGOROOT := build.Default.GOROOT
build.Default.GOPATH = ""
testHookScanDir = func(string) {}
testMu.Unlock()
defer func() {
testMu.Lock()
testHookScanDir = func(string) {}
build.Default.GOPATH = oldGOPATH
build.Default.GOROOT = oldGOROOT
testMu.Unlock()
}()
fn()
}
func TestFindImportInternal(t *testing.T) {
withEmptyGoPath(func() {
// Check for src/internal/race, not just src/internal,
// so that we can run this test also against go1.5
// (which doesn't contain that file).
_, err := os.Stat(filepath.Join(runtime.GOROOT(), "src/internal/race"))
if err != nil {
t.Skip(err)
}
got, rename, err := findImportGoPath("race", map[string]bool{"Acquire": true}, filepath.Join(runtime.GOROOT(), "src/math/x.go"))
if err != nil {
t.Fatal(err)
}
if got != "internal/race" || rename {
t.Errorf(`findImportGoPath("race", Acquire ...) = %q, %t; want "internal/race", false`, got, rename)
}
// should not be able to use internal from outside that tree
got, rename, err = findImportGoPath("race", map[string]bool{"Acquire": true}, filepath.Join(runtime.GOROOT(), "x.go"))
if err != nil {
t.Fatal(err)
}
if got != "" || rename {
t.Errorf(`findImportGoPath("race", Acquire ...)=%q, %t, want "", false`, got, rename)
}
})
}
// rand.Read should prefer crypto/rand.Read, not math/rand.Read.
func TestFindImportRandRead(t *testing.T) {
withEmptyGoPath(func() {
file := filepath.Join(runtime.GOROOT(), "src/foo/x.go") // dummy
tests := []struct {
syms []string
want string
}{
{
syms: []string{"Read"},
want: "crypto/rand",
},
{
syms: []string{"Read", "NewZipf"},
want: "math/rand",
},
{
syms: []string{"NewZipf"},
want: "math/rand",
},
{
syms: []string{"Read", "Prime"},
want: "crypto/rand",
},
}
for _, tt := range tests {
m := map[string]bool{}
for _, sym := range tt.syms {
m[sym] = true
}
got, _, err := findImportGoPath("rand", m, file)
if err != nil {
t.Errorf("for %q: %v", tt.syms, err)
continue
}
if got != tt.want {
t.Errorf("for %q, findImportGoPath = %q; want %q", tt.syms, got, tt.want)
}
}
})
}
func TestFindImportVendor(t *testing.T) {
testConfig{
gorootFiles: map[string]string{
"vendor/golang.org/x/net/http2/hpack/huffman.go": "package hpack\nfunc HuffmanDecode() { }\n",
},
}.test(t, func(t *goimportTest) {
got, rename, err := findImportGoPath("hpack", map[string]bool{"HuffmanDecode": true}, filepath.Join(t.goroot, "src/math/x.go"))
if err != nil {
t.Fatal(err)
}
want := "golang.org/x/net/http2/hpack"
if got != want || rename {
t.Errorf(`findImportGoPath("hpack", HuffmanDecode ...) = %q, %t; want %q, false`, got, rename, want)
}
})
}
func TestProcessVendor(t *testing.T) {
withEmptyGoPath(func() {
_, err := os.Stat(filepath.Join(runtime.GOROOT(), "src/vendor"))
if err != nil {
t.Skip(err)
}
target := filepath.Join(runtime.GOROOT(), "src/math/x.go")
out, err := Process(target, []byte("package http\nimport \"bytes\"\nfunc f() { strings.NewReader(); hpack.HuffmanDecode() }\n"), nil)
if err != nil {
t.Fatal(err)
}
want := "golang_org/x/net/http2/hpack"
if _, err := os.Stat(filepath.Join(runtime.GOROOT(), "src/vendor", want)); os.IsNotExist(err) {
want = "golang.org/x/net/http2/hpack"
}
if !bytes.Contains(out, []byte(want)) {
t.Fatalf("Process(%q) did not add expected hpack import %q; got:\n%s", target, want, out)
}
})
}
func TestFindImportStdlib(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 {
got, rename, ok := findImportStdlib(tt.pkg, strSet(tt.symbols))
if (got != "") != ok {
t.Error("findImportStdlib return value inconsistent")
}
if got != tt.want || rename {
t.Errorf("findImportStdlib(%q, %q) = %q, %t; want %q, false", tt.pkg, tt.symbols, got, rename, tt.want)
}
}
}
type testConfig struct {
// goroot and gopath optionally specifies the path on disk
// to use for the GOROOT and GOPATH. If empty, a temp directory
// is made if needed.
goroot, gopath string
// gorootFiles optionally specifies the complete contents of GOROOT to use,
// If nil, the normal current $GOROOT is used.
gorootFiles map[string]string // paths relative to $GOROOT/src to contents
// gopathFiles is like gorootFiles, but for $GOPATH.
// If nil, there is no GOPATH, though.
gopathFiles map[string]string // paths relative to $GOPATH/src to contents
}
func mustTempDir(t *testing.T, prefix string) string {
dir, err := ioutil.TempDir("", prefix)
if err != nil {
t.Fatal(err)
}
return dir
}
func mapToDir(destDir string, files map[string]string) error {
for path, contents := range files {
file := filepath.Join(destDir, "src", path)
if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
return err
}
var err error
if strings.HasPrefix(contents, "LINK:") {
err = os.Symlink(strings.TrimPrefix(contents, "LINK:"), file)
} else {
err = ioutil.WriteFile(file, []byte(contents), 0644)
}
if err != nil {
return err
}
}
return nil
}
func (c testConfig) test(t *testing.T, fn func(*goimportTest)) {
goroot := c.goroot
gopath := c.gopath
if c.gorootFiles != nil && goroot == "" {
goroot = mustTempDir(t, "goroot-")
defer os.RemoveAll(goroot)
}
if err := mapToDir(goroot, c.gorootFiles); err != nil {
t.Fatal(err)
}
if c.gopathFiles != nil && gopath == "" {
gopath = mustTempDir(t, "gopath-")
defer os.RemoveAll(gopath)
}
if err := mapToDir(gopath, c.gopathFiles); err != nil {
t.Fatal(err)
}
withEmptyGoPath(func() {
if goroot != "" {
build.Default.GOROOT = goroot
}
build.Default.GOPATH = gopath
it := &goimportTest{
T: t,
goroot: build.Default.GOROOT,
gopath: gopath,
ctx: &build.Default,
}
fn(it)
})
}
type goimportTest struct {
*testing.T
ctx *build.Context
goroot string
gopath string
}
// Tests that added imports are renamed when the import path's base doesn't
// match its package name. For example, we want to generate:
//
// import cloudbilling "google.golang.org/api/cloudbilling/v1"
func TestRenameWhenPackageNameMismatch(t *testing.T) {
testConfig{
gopathFiles: map[string]string{
"foo/bar/v1/x.go": "package bar \n const X = 1",
},
}.test(t, func(t *goimportTest) {
buf, err := Process(t.gopath+"/src/test/t.go", []byte("package main \n const Y = bar.X"), &Options{})
if err != nil {
t.Fatal(err)
}
const want = `package main
import bar "foo/bar/v1"
const Y = bar.X
`
if string(buf) != want {
t.Errorf("Got:\n%s\nWant:\n%s", buf, want)
}
})
}
// Tests that the LocalPrefix option causes imports
// to be added into a later group (num=3).
func TestLocalPrefix(t *testing.T) {
defer func(s string) { LocalPrefix = s }(LocalPrefix)
LocalPrefix = "foo/"
testConfig{
gopathFiles: map[string]string{
"foo/bar/bar.go": "package bar \n const X = 1",
},
}.test(t, func(t *goimportTest) {
buf, err := Process(t.gopath+"/src/test/t.go", []byte("package main \n const Y = bar.X \n const _ = runtime.GOOS"), &Options{})
if err != nil {
t.Fatal(err)
}
const want = `package main
import (
"runtime"
"foo/bar"
)
const Y = bar.X
const _ = runtime.GOOS
`
if string(buf) != want {
t.Errorf("Got:\n%s\nWant:\n%s", buf, want)
}
})
}
// Tests that running goimport on files in GOROOT (for people hacking
// on Go itself) don't cause the GOPATH to be scanned (which might be
// much bigger).
func TestOptimizationWhenInGoroot(t *testing.T) {
testConfig{
gopathFiles: map[string]string{
"foo/foo.go": "package foo\nconst X = 1\n",
},
}.test(t, func(t *goimportTest) {
testHookScanDir = func(dir string) {
if dir != filepath.Join(build.Default.GOROOT, "src") {
t.Errorf("unexpected dir scan of %s", dir)
}
}
const in = "package foo\n\nconst Y = bar.X\n"
buf, err := Process(t.goroot+"/src/foo/foo.go", []byte(in), nil)
if err != nil {
t.Fatal(err)
}
if string(buf) != in {
t.Errorf("got:\n%q\nwant unchanged:\n%q\n", in, buf)
}
})
}
// Tests that "package documentation" files are ignored.
func TestIgnoreDocumentationPackage(t *testing.T) {
testConfig{
gopathFiles: map[string]string{
"foo/foo.go": "package foo\nconst X = 1\n",
"foo/doc.go": "package documentation \n // just to confuse things\n",
},
}.test(t, func(t *goimportTest) {
const in = "package x\n\nconst Y = foo.X\n"
const want = "package x\n\nimport \"foo\"\n\nconst Y = foo.X\n"
buf, err := Process(t.gopath+"/src/x/x.go", []byte(in), nil)
if err != nil {
t.Fatal(err)
}
if string(buf) != want {
t.Errorf("wrong output.\ngot:\n%q\nwant:\n%q\n", in, 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{
gopathFiles: map[string]string{
"example.net/pkg/doc.go": "package documentation\n", // ignored
"example.net/pkg/gen.go": "package main\n", // also ignored
"example.net/pkg/pkg.go": "package the_pkg_name_to_find\n and this syntax error is ignored because of parser.PackageClauseOnly",
"example.net/pkg/z.go": "package inconsistent\n", // inconsistent but ignored
},
}.test(t, func(t *goimportTest) {
got, err := importPathToNameGoPathParse("example.net/pkg", filepath.Join(t.gopath, "src", "other.net"))
if err != nil {
t.Fatal(err)
}
const want = "the_pkg_name_to_find"
if got != want {
t.Errorf("importPathToNameGoPathParse(..) = %q; want %q", got, want)
}
})
}
func TestIgnoreConfiguration(t *testing.T) {
testConfig{
gopathFiles: map[string]string{
".goimportsignore": "# comment line\n\n example.net", // tests comment, blank line, whitespace trimming
"example.net/pkg/pkg.go": "package pkg\nconst X = 1",
"otherwise-longer-so-worse.example.net/foo/pkg/pkg.go": "package pkg\nconst X = 1",
},
}.test(t, func(t *goimportTest) {
const in = "package x\n\nconst _ = pkg.X\n"
const want = "package x\n\nimport \"otherwise-longer-so-worse.example.net/foo/pkg\"\n\nconst _ = pkg.X\n"
buf, err := Process(t.gopath+"/src/x/x.go", []byte(in), nil)
if err != nil {
t.Fatal(err)
}
if string(buf) != want {
t.Errorf("wrong output.\ngot:\n%q\nwant:\n%q\n", buf, want)
}
})
}
// Skip "node_modules" directory.
func TestSkipNodeModules(t *testing.T) {
testConfig{
gopathFiles: map[string]string{
"example.net/node_modules/pkg/a.go": "package pkg\nconst X = 1",
"otherwise-longer.net/not_modules/pkg/a.go": "package pkg\nconst X = 1",
},
}.test(t, func(t *goimportTest) {
const in = "package x\n\nconst _ = pkg.X\n"
const want = "package x\n\nimport \"otherwise-longer.net/not_modules/pkg\"\n\nconst _ = pkg.X\n"
buf, err := Process(t.gopath+"/src/x/x.go", []byte(in), nil)
if err != nil {
t.Fatal(err)
}
if string(buf) != want {
t.Errorf("wrong output.\ngot:\n%q\nwant:\n%q\n", buf, want)
}
})
}
// golang.org/issue/16458 -- if GOROOT is a prefix of GOPATH, GOPATH is ignored.
func TestGoRootPrefixOfGoPath(t *testing.T) {
dir := mustTempDir(t, "importstest")
defer os.RemoveAll(dir)
testConfig{
goroot: filepath.Join(dir, "go"),
gopath: filepath.Join(dir, "gopath"),
gopathFiles: map[string]string{
"example.com/foo/pkg.go": "package foo\nconst X = 1",
},
}.test(t, func(t *goimportTest) {
const in = "package x\n\nconst _ = foo.X\n"
const want = "package x\n\nimport \"example.com/foo\"\n\nconst _ = foo.X\n"
buf, err := Process(t.gopath+"/src/x/x.go", []byte(in), nil)
if err != nil {
t.Fatal(err)
}
if string(buf) != want {
t.Errorf("wrong output.\ngot:\n%q\nwant:\n%q\n", buf, want)
}
})
}
const testGlobalImportsUsesGlobal = `package globalimporttest
func doSomething() {
t := time.Now()
}
`
const testGlobalImportsGlobalDecl = `package globalimporttest
type Time struct{}
func (t Time) Now() Time {
return Time{}
}
var time Time
`
// 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 pkg = "globalimporttest"
const usesGlobalFile = pkg + "/uses_global.go"
testConfig{
gopathFiles: map[string]string{
usesGlobalFile: testGlobalImportsUsesGlobal,
pkg + "/global.go": testGlobalImportsGlobalDecl,
},
}.test(t, func(t *goimportTest) {
buf, err := Process(
t.gopath+"/src/"+usesGlobalFile, []byte(testGlobalImportsUsesGlobal), nil)
if err != nil {
t.Fatal(err)
}
if string(buf) != testGlobalImportsUsesGlobal {
t.Errorf("wrong output.\ngot:\n%q\nwant:\n%q\n", buf, testGlobalImportsUsesGlobal)
}
})
}
// 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"
func LogSomething() {
log.Print("Something")
}
`
// need is the file being tested that needs the import.
const need = `package siblingimporttest
func LogSomethingElse() {
log.Print("Something else")
}
`
// want is the expected result file
const want = `package siblingimporttest
import "local/log"
func LogSomethingElse() {
log.Print("Something else")
}
`
const pkg = "siblingimporttest"
const siblingFile = pkg + "/needs_import.go"
testConfig{
gopathFiles: map[string]string{
siblingFile: need,
pkg + "/provides_import.go": provide,
},
}.test(t, func(t *goimportTest) {
buf, err := Process(
t.gopath+"/src/"+siblingFile, []byte(need), nil)
if err != nil {
t.Fatal(err)
}
if string(buf) != want {
t.Errorf("wrong output.\ngot:\n%q\nwant:\n%q\n", buf, want)
}
})
}
func strSet(ss []string) map[string]bool {
m := make(map[string]bool)
for _, s := range ss {
m[s] = true
}
return m
}
func TestPkgIsCandidate(t *testing.T) {
tests := [...]struct {
filename string
pkgIdent string
pkg *pkg
want bool
}{
// normal match
0: {
filename: "/gopath/src/my/pkg/pkg.go",
pkgIdent: "client",
pkg: &pkg{
dir: "/gopath/src/client",
importPath: "client",
importPathShort: "client",
},
want: true,
},
// not a match
1: {
filename: "/gopath/src/my/pkg/pkg.go",
pkgIdent: "zzz",
pkg: &pkg{
dir: "/gopath/src/client",
importPath: "client",
importPathShort: "client",
},
want: false,
},
// would be a match, but "client" appears too deep.
2: {
filename: "/gopath/src/my/pkg/pkg.go",
pkgIdent: "client",
pkg: &pkg{
dir: "/gopath/src/client/foo/foo/foo",
importPath: "client/foo/foo",
importPathShort: "client/foo/foo",
},
want: false,
},
// not an exact match, but substring is good enough.
3: {
filename: "/gopath/src/my/pkg/pkg.go",
pkgIdent: "client",
pkg: &pkg{
dir: "/gopath/src/foo/go-client",
importPath: "foo/go-client",
importPathShort: "foo/go-client",
},
want: true,
},
// "internal" package, and not visible
4: {
filename: "/gopath/src/my/pkg/pkg.go",
pkgIdent: "client",
pkg: &pkg{
dir: "/gopath/src/foo/internal/client",
importPath: "foo/internal/client",
importPathShort: "foo/internal/client",
},
want: false,
},
// "internal" package but visible
5: {
filename: "/gopath/src/foo/bar.go",
pkgIdent: "client",
pkg: &pkg{
dir: "/gopath/src/foo/internal/client",
importPath: "foo/internal/client",
importPathShort: "foo/internal/client",
},
want: true,
},
// "vendor" package not visible
6: {
filename: "/gopath/src/foo/bar.go",
pkgIdent: "client",
pkg: &pkg{
dir: "/gopath/src/other/vendor/client",
importPath: "other/vendor/client",
importPathShort: "client",
},
want: false,
},
// "vendor" package, visible
7: {
filename: "/gopath/src/foo/bar.go",
pkgIdent: "client",
pkg: &pkg{
dir: "/gopath/src/foo/vendor/client",
importPath: "other/foo/client",
importPathShort: "client",
},
want: true,
},
// Ignore hyphens.
8: {
filename: "/gopath/src/foo/bar.go",
pkgIdent: "socketio",
pkg: &pkg{
dir: "/gopath/src/foo/socket-io",
importPath: "foo/socket-io",
importPathShort: "foo/socket-io",
},
want: true,
},
// Ignore case.
9: {
filename: "/gopath/src/foo/bar.go",
pkgIdent: "fooprod",
pkg: &pkg{
dir: "/gopath/src/foo/FooPROD",
importPath: "foo/FooPROD",
importPathShort: "foo/FooPROD",
},
want: true,
},
// Ignoring both hyphens and case together.
10: {
filename: "/gopath/src/foo/bar.go",
pkgIdent: "fooprod",
pkg: &pkg{
dir: "/gopath/src/foo/Foo-PROD",
importPath: "foo/Foo-PROD",
importPathShort: "foo/Foo-PROD",
},
want: true,
},
}
for i, tt := range tests {
got := pkgIsCandidate(tt.filename, tt.pkgIdent, 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)
}
}
}
func TestShouldTraverse(t *testing.T) {
switch runtime.GOOS {
case "windows", "plan9":
t.Skipf("skipping symlink-requiring test on %s", runtime.GOOS)
}
dir, err := ioutil.TempDir("", "goimports-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
// Note: mapToDir prepends "src" to each element, since
// mapToDir was made for creating GOPATHs.
if err := mapToDir(dir, map[string]string{
"foo/foo2/file.txt": "",
"foo/foo2/link-to-src": "LINK:" + dir + "/src",
"foo/foo2/link-to-src-foo": "LINK:" + dir + "/src/foo",
"foo/foo2/link-to-dot": "LINK:.",
"bar/bar2/file.txt": "",
"bar/bar2/link-to-src-foo": "LINK:" + dir + "/src/foo",
"a/b/c": "LINK:" + dir + "/src/a/d",
"a/d/e": "LINK:" + dir + "/src/a/b",
}); err != nil {
t.Fatal(err)
}
tests := []struct {
dir string
file string
want bool
}{
{
dir: dir + "/src/foo/foo2",
file: "link-to-src-foo",
want: false, // loop
},
{
dir: dir + "/src/foo/foo2",
file: "link-to-src",
want: false, // loop
},
{
dir: dir + "/src/foo/foo2",
file: "link-to-dot",
want: false, // loop
},
{
dir: dir + "/src/bar/bar2",
file: "link-to-src-foo",
want: true, // not a loop
},
{
dir: dir + "/src/a/b/c",
file: "e",
want: false, // loop: "e" is the same as "b".
},
}
for i, tt := range tests {
fi, err := os.Stat(filepath.Join(tt.dir, tt.file))
if err != nil {
t.Errorf("%d. Stat = %v", i, err)
continue
}
got := shouldTraverse(tt.dir, fi)
if got != tt.want {
t.Errorf("%d. shouldTraverse(%q, %q) = %v; want %v", i, tt.dir, tt.file, got, tt.want)
}
}
}
// Issue 20941: this used to panic on Windows.
func TestProcessStdin(t *testing.T) {
got, err := Process("<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)
}
}