mirror of
https://github.com/golang/go
synced 2024-11-18 22:44:48 -07:00
caa95bb40b
Unimported packages may be suggested as completion items. Since these are not yet imported, they should be ranked lower than other candidates. They also require an additional import statement to be valid, which is provided as an AdditionalTextEdit. Adding this import does not use astutil.AddNamedImport, to avoid editing the current ast and work even if there are errors. Additionally, it can be hard to determine what changes need to be made to the source document from the ast, as astutil.AddNamedImport includes a merging pass. Instead, the completion item simply adds another import declaration. Change-Id: Icbde226d843bd49ee3713cafcbd5299d51530695 Reviewed-on: https://go-review.googlesource.com/c/tools/+/190338 Run-TryBot: Suzy Mueller <suzmue@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Rebecca Stambler <rstambler@golang.org>
307 lines
5.4 KiB
Go
307 lines
5.4 KiB
Go
package source
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/token"
|
|
"log"
|
|
"testing"
|
|
)
|
|
|
|
var fset = token.NewFileSet()
|
|
|
|
func parse(t *testing.T, name, in string) *ast.File {
|
|
file, err := parser.ParseFile(fset, name, in, parser.ParseComments)
|
|
if err != nil {
|
|
t.Fatalf("%s parse: %v", name, err)
|
|
}
|
|
return file
|
|
}
|
|
|
|
func print(t *testing.T, name string, f *ast.File) string {
|
|
var buf bytes.Buffer
|
|
if err := format.Node(&buf, fset, f); err != nil {
|
|
t.Fatalf("%s gofmt: %v", name, err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
type test struct {
|
|
name string
|
|
renamedPkg string
|
|
pkg string
|
|
in string
|
|
want []importInfo
|
|
unchanged bool // Expect added/deleted return value to be false.
|
|
}
|
|
|
|
type importInfo struct {
|
|
name string
|
|
path string
|
|
}
|
|
|
|
var addTests = []test{
|
|
{
|
|
name: "leave os alone",
|
|
pkg: "os",
|
|
in: `package main
|
|
|
|
import (
|
|
"os"
|
|
)
|
|
`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
},
|
|
unchanged: true,
|
|
},
|
|
{
|
|
name: "package statement only",
|
|
pkg: "os",
|
|
in: `package main
|
|
`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "package statement no new line",
|
|
pkg: "os",
|
|
in: `package main`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "package statement comments",
|
|
pkg: "os",
|
|
in: `// This is a comment
|
|
package main // This too`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "package statement multiline comments",
|
|
pkg: "os",
|
|
in: `package main /* This is a multiline comment
|
|
and it extends
|
|
further down*/`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "import c",
|
|
pkg: "os",
|
|
in: `package main
|
|
|
|
import "C"
|
|
`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
importInfo{
|
|
name: "",
|
|
path: "C",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "existing imports",
|
|
pkg: "os",
|
|
in: `package main
|
|
|
|
import "io"
|
|
`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
importInfo{
|
|
name: "",
|
|
path: "io",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "existing imports with comment",
|
|
pkg: "os",
|
|
in: `package main
|
|
|
|
import "io" // A comment
|
|
`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
importInfo{
|
|
name: "",
|
|
path: "io",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "existing imports multiline comment",
|
|
pkg: "os",
|
|
in: `package main
|
|
|
|
import "io" /* A comment
|
|
that
|
|
extends */
|
|
`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "",
|
|
path: "os",
|
|
},
|
|
importInfo{
|
|
name: "",
|
|
path: "io",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "renamed import",
|
|
renamedPkg: "o",
|
|
pkg: "os",
|
|
in: `package main
|
|
`,
|
|
want: []importInfo{
|
|
importInfo{
|
|
name: "o",
|
|
path: "os",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestAddImport(t *testing.T) {
|
|
for _, test := range addTests {
|
|
file := parse(t, test.name, test.in)
|
|
var before bytes.Buffer
|
|
ast.Fprint(&before, fset, file, nil)
|
|
edits, err := AddNamedImport(fset, file, test.renamedPkg, test.pkg)
|
|
if err != nil && !test.unchanged {
|
|
t.Errorf("error adding import: %s", err)
|
|
continue
|
|
}
|
|
|
|
// Apply the edits and parse the file.
|
|
got := applyEdits(test.in, edits)
|
|
gotFile := parse(t, test.name, got)
|
|
|
|
compareImports(t, fmt.Sprintf("first run: %s:\n", test.name), gotFile.Imports, test.want)
|
|
|
|
// AddNamedImport should be idempotent. Verify that by calling it again,
|
|
// expecting no change to the AST, and the returned added value to always be false.
|
|
edits, err = AddNamedImport(fset, gotFile, test.renamedPkg, test.pkg)
|
|
if err != nil && !test.unchanged {
|
|
t.Errorf("error adding import: %s", err)
|
|
continue
|
|
}
|
|
// Apply the edits and parse the file.
|
|
got = applyEdits(got, edits)
|
|
gotFile = parse(t, test.name, got)
|
|
|
|
compareImports(t, test.name, gotFile.Imports, test.want)
|
|
|
|
}
|
|
}
|
|
|
|
func TestDoubleAddNamedImport(t *testing.T) {
|
|
name := "doublenamedimport"
|
|
in := "package main\n"
|
|
file := parse(t, name, in)
|
|
// Add a named import
|
|
edits, err := AddNamedImport(fset, file, "o", "os")
|
|
if err != nil {
|
|
t.Errorf("error adding import: %s", err)
|
|
return
|
|
}
|
|
got := applyEdits(in, edits)
|
|
log.Println(got)
|
|
gotFile := parse(t, name, got)
|
|
|
|
// Add a second named import
|
|
edits, err = AddNamedImport(fset, gotFile, "i", "io")
|
|
if err != nil {
|
|
t.Errorf("error adding import: %s", err)
|
|
return
|
|
}
|
|
got = applyEdits(got, edits)
|
|
gotFile = parse(t, name, got)
|
|
|
|
want := []importInfo{
|
|
importInfo{
|
|
name: "o",
|
|
path: "os",
|
|
},
|
|
importInfo{
|
|
name: "i",
|
|
path: "io",
|
|
},
|
|
}
|
|
compareImports(t, "", gotFile.Imports, want)
|
|
}
|
|
|
|
func compareImports(t *testing.T, prefix string, got []*ast.ImportSpec, want []importInfo) {
|
|
if len(got) != len(want) {
|
|
t.Errorf("%s\ngot %d imports\nwant %d", prefix, len(got), len(want))
|
|
return
|
|
}
|
|
|
|
for _, imp := range got {
|
|
name := importName(imp)
|
|
path := importPath(imp)
|
|
found := false
|
|
for _, want := range want {
|
|
if want.name == name && want.path == path {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("%s\n\ngot unexpected import: name: %q,path: %q", prefix, name, path)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func applyEdits(contents string, edits []TextEdit) string {
|
|
res := contents
|
|
|
|
// Apply the edits from the end of the file forward
|
|
// to preserve the offsets
|
|
for i := len(edits) - 1; i >= 0; i-- {
|
|
edit := edits[i]
|
|
start := edit.Span.Start().Offset()
|
|
end := edit.Span.End().Offset()
|
|
tmp := res[0:start] + edit.NewText
|
|
res = tmp + res[end:]
|
|
}
|
|
return res
|
|
}
|