internal/lsp: add ListImports

The VS Code extension uses information about imports to figure out
whether a given function is a Testify test. As of writing, it asks:
- Does the file import Testify?
- Does the package it's in do so?
To answer these questions, add ListImports, which tells you about the
packages imported by the current file, including their import name,
plus the import paths of all imports in the entire package.

I suspect the latter may be wrong in the presence of GOPATH vendoring,
but that should be a relatively rare situation at this point so I didn't
bother testing.

Fixes golang/go#40514.

Change-Id: I4c69e1db80dce6e594bdb595a81aade1ddec4d29
Reviewed-on: https://go-review.googlesource.com/c/tools/+/383354
Trust: Heschi Kreinick <heschi@google.com>
Run-TryBot: Heschi Kreinick <heschi@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Heschi Kreinick 2022-02-04 15:23:09 -05:00
parent d55d8929fe
commit 414ec9c3f0
7 changed files with 261 additions and 59 deletions

View File

@ -142,6 +142,37 @@ Args:
}
```
### **List imports of a file and its package**
Identifier: `gopls.list_imports`
Retrieve a list of imports in the given Go file, and the package it
belongs to.
Args:
```
{
// The file URI.
"URI": string,
}
```
Result:
```
{
// Imports is a list of imports in the requested file.
"Imports": []{
"Path": string,
"Name": string,
},
// PackageImports is a list of all imports in the requested file's package.
"PackageImports": []{
"Path": string,
},
}
```
### **List known packages**
Identifier: `gopls.list_known_packages`

View File

@ -1,59 +0,0 @@
// Copyright 2021 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 misc
import (
"testing"
"golang.org/x/tools/internal/lsp/command"
"golang.org/x/tools/internal/lsp/protocol"
. "golang.org/x/tools/internal/lsp/regtest"
"golang.org/x/tools/internal/lsp/tests"
)
func TestAddImport(t *testing.T) {
const before = `package main
import "fmt"
func main() {
fmt.Println("hello world")
}
`
const want = `package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println("hello world")
}
`
Run(t, "", func(t *testing.T, env *Env) {
env.CreateBuffer("main.go", before)
cmd, err := command.NewAddImportCommand("Add Import", command.AddImportArgs{
URI: protocol.URIFromSpanURI(env.Sandbox.Workdir.URI("main.go").SpanURI()),
ImportPath: "bytes",
})
if err != nil {
t.Fatal(err)
}
_, err = env.Editor.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{
Command: "gopls.add_import",
Arguments: cmd.Arguments,
})
if err != nil {
t.Fatal(err)
}
got := env.Editor.BufferText("main.go")
if got != want {
t.Fatalf("gopls.add_import failed\n%s", tests.Diff(t, want, got))
}
})
}

View File

@ -0,0 +1,133 @@
// Copyright 2021 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 misc
import (
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/internal/lsp/command"
"golang.org/x/tools/internal/lsp/protocol"
. "golang.org/x/tools/internal/lsp/regtest"
"golang.org/x/tools/internal/lsp/tests"
)
func TestAddImport(t *testing.T) {
const before = `package main
import "fmt"
func main() {
fmt.Println("hello world")
}
`
const want = `package main
import (
"bytes"
"fmt"
)
func main() {
fmt.Println("hello world")
}
`
Run(t, "", func(t *testing.T, env *Env) {
env.CreateBuffer("main.go", before)
cmd, err := command.NewAddImportCommand("Add Import", command.AddImportArgs{
URI: env.Sandbox.Workdir.URI("main.go"),
ImportPath: "bytes",
})
if err != nil {
t.Fatal(err)
}
env.ExecuteCommand(&protocol.ExecuteCommandParams{
Command: "gopls.add_import",
Arguments: cmd.Arguments,
}, nil)
got := env.Editor.BufferText("main.go")
if got != want {
t.Fatalf("gopls.add_import failed\n%s", tests.Diff(t, want, got))
}
})
}
func TestListImports(t *testing.T) {
const files = `
-- go.mod --
module mod.com
go 1.12
-- foo.go --
package foo
const C = 1
-- import_strings_test.go --
package foo
import (
x "strings"
"testing"
)
func TestFoo(t *testing.T) {}
-- import_testing_test.go --
package foo
import "testing"
func TestFoo2(t *testing.T) {}
`
tests := []struct {
filename string
want command.ListImportsResult
}{
{
filename: "import_strings_test.go",
want: command.ListImportsResult{
Imports: []command.FileImport{
{Name: "x", Path: "strings"},
{Path: "testing"},
},
PackageImports: []command.PackageImport{
{Path: "strings"},
{Path: "testing"},
},
},
},
{
filename: "import_testing_test.go",
want: command.ListImportsResult{
Imports: []command.FileImport{
{Path: "testing"},
},
PackageImports: []command.PackageImport{
{Path: "strings"},
{Path: "testing"},
},
},
},
}
Run(t, files, func(t *testing.T, env *Env) {
for _, tt := range tests {
cmd, err := command.NewListImportsCommand("List Imports", command.URIArg{
URI: env.Sandbox.Workdir.URI(tt.filename),
})
if err != nil {
t.Fatal(err)
}
var result command.ListImportsResult
env.ExecuteCommand(&protocol.ExecuteCommandParams{
Command: command.ListImports.ID(),
Arguments: cmd.Arguments,
}, &result)
if diff := cmp.Diff(tt.want, result); diff != "" {
t.Errorf("unexpected list imports result for %q (-want +got):\n%s", tt.filename, diff)
}
}
})
}

View File

@ -13,9 +13,11 @@ import (
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"golang.org/x/mod/modfile"
"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/gocommand"
"golang.org/x/tools/internal/lsp/command"
@ -681,6 +683,48 @@ func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URI
})
return result, err
}
func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) (command.ListImportsResult, error) {
var result command.ListImportsResult
err := c.run(ctx, commandConfig{
forURI: args.URI,
}, func(ctx context.Context, deps commandDeps) error {
pkg, err := deps.snapshot.PackageForFile(ctx, args.URI.SpanURI(), source.TypecheckWorkspace, source.NarrowestPackage)
if err != nil {
return err
}
pgf, err := pkg.File(args.URI.SpanURI())
if err != nil {
return err
}
for _, group := range astutil.Imports(deps.snapshot.FileSet(), pgf.File) {
for _, imp := range group {
if imp.Path == nil {
continue
}
var name string
if imp.Name != nil {
name = imp.Name.Name
}
result.Imports = append(result.Imports, command.FileImport{
Path: source.ImportPath(imp),
Name: name,
})
}
}
for _, imp := range pkg.Imports() {
result.PackageImports = append(result.PackageImports, command.PackageImport{
Path: imp.PkgPath(), // This might be the vendored path under GOPATH vendoring, in which case it's a bug.
})
}
sort.Slice(result.PackageImports, func(i, j int) bool {
return result.PackageImports[i].Path < result.PackageImports[j].Path
})
return nil
})
return result, err
}
func (c *commandHandler) AddImport(ctx context.Context, args command.AddImportArgs) error {
return c.run(ctx, commandConfig{
progress: "Adding import",

View File

@ -27,6 +27,7 @@ const (
Generate Command = "generate"
GenerateGoplsMod Command = "generate_gopls_mod"
GoGetPackage Command = "go_get_package"
ListImports Command = "list_imports"
ListKnownPackages Command = "list_known_packages"
RegenerateCgo Command = "regenerate_cgo"
RemoveDependency Command = "remove_dependency"
@ -49,6 +50,7 @@ var Commands = []Command{
Generate,
GenerateGoplsMod,
GoGetPackage,
ListImports,
ListKnownPackages,
RegenerateCgo,
RemoveDependency,
@ -112,6 +114,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte
return nil, err
}
return nil, s.GoGetPackage(ctx, a0)
case "gopls.list_imports":
var a0 URIArg
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
return nil, err
}
return s.ListImports(ctx, a0)
case "gopls.list_known_packages":
var a0 URIArg
if err := UnmarshalArgs(params.Arguments, &a0); err != nil {
@ -280,6 +288,18 @@ func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) (protocol.Command
}, nil
}
func NewListImportsCommand(title string, a0 URIArg) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {
return protocol.Command{}, err
}
return protocol.Command{
Title: title,
Command: "gopls.list_imports",
Arguments: args,
}, nil
}
func NewListKnownPackagesCommand(title string, a0 URIArg) (protocol.Command, error) {
args, err := MarshalArgs(a0)
if err != nil {

View File

@ -120,6 +120,12 @@ type Interface interface {
// Retrieve a list of packages that are importable from the given URI.
ListKnownPackages(context.Context, URIArg) (ListKnownPackagesResult, error)
// ListImports: List imports of a file and its package
//
// Retrieve a list of imports in the given Go file, and the package it
// belongs to.
ListImports(context.Context, URIArg) (ListImportsResult, error)
// AddImport: Add an import
//
// Ask the server to add an import path to a given Go file. The method will
@ -224,6 +230,26 @@ type ListKnownPackagesResult struct {
Packages []string
}
type ListImportsResult struct {
// Imports is a list of imports in the requested file.
Imports []FileImport
// PackageImports is a list of all imports in the requested file's package.
PackageImports []PackageImport
}
type FileImport struct {
// Path is the import path of the import.
Path string
// Name is the name of the import, e.g. `foo` in `import foo "strings"`.
Name string
}
type PackageImport struct {
// Path is the import path of the import.
Path string
}
type WorkspaceMetadataArgs struct {
}

View File

@ -622,6 +622,13 @@ var GeneratedAPIJSON = &APIJSON{
Doc: "Runs `go get` to fetch a package.",
ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The package to go get.\n\t\"Pkg\": string,\n\t\"AddRequire\": bool,\n}",
},
{
Command: "gopls.list_imports",
Title: "List imports of a file and its package",
Doc: "Retrieve a list of imports in the given Go file, and the package it\nbelongs to.",
ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}",
ResultDoc: "{\n\t// Imports is a list of imports in the requested file.\n\t\"Imports\": []{\n\t\t\"Path\": string,\n\t\t\"Name\": string,\n\t},\n\t// PackageImports is a list of all imports in the requested file's package.\n\t\"PackageImports\": []{\n\t\t\"Path\": string,\n\t},\n}",
},
{
Command: "gopls.list_known_packages",
Title: "List known packages",