From 414ec9c3f0ea84250efb8f18f51c607184b7631e Mon Sep 17 00:00:00 2001 From: Heschi Kreinick Date: Fri, 4 Feb 2022 15:23:09 -0500 Subject: [PATCH] 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 Run-TryBot: Heschi Kreinick Reviewed-by: Robert Findley gopls-CI: kokoro TryBot-Result: Gopher Robot --- gopls/doc/commands.md | 31 ++++ .../internal/regtest/misc/add_import_test.go | 59 -------- gopls/internal/regtest/misc/import_test.go | 133 ++++++++++++++++++ internal/lsp/command.go | 44 ++++++ internal/lsp/command/command_gen.go | 20 +++ internal/lsp/command/interface.go | 26 ++++ internal/lsp/source/api_json.go | 7 + 7 files changed, 261 insertions(+), 59 deletions(-) delete mode 100644 gopls/internal/regtest/misc/add_import_test.go create mode 100644 gopls/internal/regtest/misc/import_test.go diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 97e18527f7..b70b0afec8 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -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` diff --git a/gopls/internal/regtest/misc/add_import_test.go b/gopls/internal/regtest/misc/add_import_test.go deleted file mode 100644 index 8eb96cf00b..0000000000 --- a/gopls/internal/regtest/misc/add_import_test.go +++ /dev/null @@ -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)) - } - }) -} diff --git a/gopls/internal/regtest/misc/import_test.go b/gopls/internal/regtest/misc/import_test.go new file mode 100644 index 0000000000..d5b6bcf43f --- /dev/null +++ b/gopls/internal/regtest/misc/import_test.go @@ -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) + } + } + + }) +} diff --git a/internal/lsp/command.go b/internal/lsp/command.go index 153af443b0..d8f9d2ce4f 100644 --- a/internal/lsp/command.go +++ b/internal/lsp/command.go @@ -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", diff --git a/internal/lsp/command/command_gen.go b/internal/lsp/command/command_gen.go index f872c7fc69..c814bfe58c 100644 --- a/internal/lsp/command/command_gen.go +++ b/internal/lsp/command/command_gen.go @@ -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 { diff --git a/internal/lsp/command/interface.go b/internal/lsp/command/interface.go index d80e675b2a..d5f520dd76 100644 --- a/internal/lsp/command/interface.go +++ b/internal/lsp/command/interface.go @@ -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 { } diff --git a/internal/lsp/source/api_json.go b/internal/lsp/source/api_json.go index cf5f97888e..4e8e9df14a 100755 --- a/internal/lsp/source/api_json.go +++ b/internal/lsp/source/api_json.go @@ -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",