Skip to content

Commit 6aeba54

Browse files
stamblerreclintjedwards
authored andcommitted
internal/lsp: show errors when the user is in the wrong directory
If we encounter `go list` errors when loading a user's package, we should try to see if they've encountered any of our common error cases. They are: 1) a user has GO111MODULE=off, but is outside of their GOPATH, and 2) a user is in module mode but doesn't have a go.mod file. Fortunately, go/packages does a great job handling edge cases so gopls will work well for most of them. The main issue will be unresolved imports. These show up in DepErrors in `go list`, so go/packages doesn't propagate them through to the list of errors. This will require changes to go/packages. Updates golang/go#31668 Change-Id: Ibd5253b33b38caffeaad54a403c74c0b861fcc14 Reviewed-on: https://go-review.googlesource.com/c/tools/+/194018 Run-TryBot: Rebecca Stambler <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Cottrell <[email protected]>
1 parent 7956c30 commit 6aeba54

File tree

8 files changed

+119
-19
lines changed

8 files changed

+119
-19
lines changed

internal/lsp/cmd/definition.go

+2-5
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
7979
Position: loc.Range.Start,
8080
}
8181
p := protocol.DefinitionParams{
82-
tdpp,
83-
protocol.WorkDoneProgressParams{},
84-
protocol.PartialResultParams{},
82+
TextDocumentPositionParams: tdpp,
8583
}
8684
locs, err := conn.Definition(ctx, &p)
8785
if err != nil {
@@ -92,8 +90,7 @@ func (d *definition) Run(ctx context.Context, args ...string) error {
9290
return errors.Errorf("%v: not an identifier", from)
9391
}
9492
q := protocol.HoverParams{
95-
tdpp,
96-
protocol.WorkDoneProgressParams{},
93+
TextDocumentPositionParams: tdpp,
9794
}
9895
hover, err := conn.Hover(ctx, &q)
9996
if err != nil {

internal/lsp/diagnostics.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,16 @@ func (s *Server) diagnostics(view source.View, uri span.URI) error {
3333
if !ok {
3434
return errors.Errorf("%s is not a Go file", f.URI())
3535
}
36-
reports, err := source.Diagnostics(ctx, view, gof, view.Options().DisabledAnalyses)
36+
reports, warningMsg, err := source.Diagnostics(ctx, view, gof, view.Options().DisabledAnalyses)
3737
if err != nil {
3838
return err
3939
}
40+
if warningMsg != "" {
41+
s.client.ShowMessage(ctx, &protocol.ShowMessageParams{
42+
Type: protocol.Info,
43+
Message: warningMsg,
44+
})
45+
}
4046

4147
s.undeliveredMu.Lock()
4248
defer s.undeliveredMu.Unlock()

internal/lsp/general.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,9 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitia) (
4646
Name: path.Base(params.RootURI),
4747
}}
4848
} else {
49-
// no folders and no root, single file mode
50-
//TODO(iancottrell): not sure how to do single file mode yet
51-
//issue: golang.org/issue/31168
52-
return nil, errors.Errorf("single file mode not supported yet")
49+
// No folders and no root--we are in single file mode.
50+
// TODO: https://golang.org/issue/34160.
51+
return nil, errors.Errorf("gopls does not yet support editing a single file. Please open a directory.")
5352
}
5453
}
5554

internal/lsp/lsp_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) {
9494
if !ok {
9595
t.Fatalf("%s is not a Go file: %v", uri, err)
9696
}
97-
results, err := source.Diagnostics(r.ctx, v, gof, nil)
97+
results, _, err := source.Diagnostics(r.ctx, v, gof, nil)
9898
if err != nil {
9999
t.Fatal(err)
100100
}
@@ -558,7 +558,7 @@ func (r *runner) SuggestedFix(t *testing.T, data tests.SuggestedFixes) {
558558
if err != nil {
559559
t.Fatal(err)
560560
}
561-
results, err := source.Diagnostics(r.ctx, v, f, nil)
561+
results, _, err := source.Diagnostics(r.ctx, v, f, nil)
562562
if err != nil {
563563
t.Fatal(err)
564564
}

internal/lsp/source/diagnostics.go

+16-6
Original file line numberDiff line numberDiff line change
@@ -64,26 +64,36 @@ const (
6464
SeverityError
6565
)
6666

67-
func Diagnostics(ctx context.Context, view View, f GoFile, disabledAnalyses map[string]struct{}) (map[span.URI][]Diagnostic, error) {
67+
func Diagnostics(ctx context.Context, view View, f GoFile, disabledAnalyses map[string]struct{}) (map[span.URI][]Diagnostic, string, error) {
6868
ctx, done := trace.StartSpan(ctx, "source.Diagnostics", telemetry.File.Of(f.URI()))
6969
defer done()
7070

7171
cphs, err := f.CheckPackageHandles(ctx)
7272
if err != nil {
73-
return nil, err
73+
return nil, "", err
7474
}
7575
cph := NarrowestCheckPackageHandle(cphs)
7676
pkg, err := cph.Check(ctx)
7777
if err != nil {
7878
log.Error(ctx, "no package for file", err)
79-
return singleDiagnostic(f.URI(), "%s is not part of a package", f.URI()), nil
79+
return singleDiagnostic(f.URI(), "%s is not part of a package", f.URI()), "", nil
8080
}
81+
8182
// Prepare the reports we will send for the files in this package.
8283
reports := make(map[span.URI][]Diagnostic)
8384
for _, fh := range pkg.Files() {
8485
clearReports(view, reports, fh.File().Identity().URI)
8586
}
8687

88+
// If we have `go list` errors, we may want to offer a warning message to the user.
89+
var warningMsg string
90+
if hasListErrors(pkg.GetErrors()) {
91+
warningMsg, err = checkCommonErrors(ctx, view, f.URI())
92+
if err != nil {
93+
log.Error(ctx, "error checking common errors", err, telemetry.File.Of(f.URI))
94+
}
95+
}
96+
8797
// Prepare any additional reports for the errors in this package.
8898
for _, err := range pkg.GetErrors() {
8999
if err.Kind != packages.ListError {
@@ -104,19 +114,19 @@ func Diagnostics(ctx context.Context, view View, f GoFile, disabledAnalyses map[
104114
for _, f := range revDeps {
105115
cphs, err := f.CheckPackageHandles(ctx)
106116
if err != nil {
107-
return nil, err
117+
return nil, "", err
108118
}
109119
cph := WidestCheckPackageHandle(cphs)
110120
pkg, err := cph.Check(ctx)
111121
if err != nil {
112-
return nil, err
122+
return nil, warningMsg, err
113123
}
114124
for _, fh := range pkg.Files() {
115125
clearReports(view, reports, fh.File().Identity().URI)
116126
}
117127
diagnostics(ctx, view, pkg, reports)
118128
}
119-
return reports, nil
129+
return reports, warningMsg, nil
120130
}
121131

122132
type diagnosticSet struct {

internal/lsp/source/errors.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package source
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strings"
11+
12+
"golang.org/x/tools/internal/span"
13+
)
14+
15+
func checkCommonErrors(ctx context.Context, view View, uri span.URI) (string, error) {
16+
// Some cases we should be able to detect:
17+
//
18+
// 1. The user is in GOPATH mode and is working outside their GOPATH
19+
// 2. The user is in module mode and has opened a subdirectory of their module
20+
//
21+
gopath := os.Getenv("GOPATH")
22+
cfg := view.Config(ctx)
23+
24+
// Invoke `go env GOMOD` inside of the directory of the file.
25+
fdir := filepath.Dir(uri.Filename())
26+
b, err := invokeGo(ctx, fdir, cfg.Env, "env", "GOMOD")
27+
if err != nil {
28+
return "", err
29+
}
30+
modFile := strings.TrimSpace(b.String())
31+
if modFile == filepath.FromSlash("/dev/null") {
32+
modFile = ""
33+
}
34+
35+
// Not inside of a module.
36+
inAModule := modFile != ""
37+
inGopath := strings.HasPrefix(uri.Filename(), filepath.Join(gopath, "src"))
38+
moduleMode := os.Getenv("GO111MODULE")
39+
40+
var msg string
41+
// The user is in a module.
42+
if inAModule {
43+
// The workspace root is open to a directory different from the module root.
44+
if modRoot := filepath.Dir(modFile); cfg.Dir != filepath.Dir(modFile) {
45+
msg = fmt.Sprintf("Your workspace root is %s, but your module root is %s. Please add %s as a workspace folder.", cfg.Dir, modRoot, modRoot)
46+
}
47+
} else if inGopath {
48+
if moduleMode == "on" {
49+
msg = "You are in module mode, but you are not inside of a module. Please create a module."
50+
}
51+
} else {
52+
msg = "You are neither in a module nor in your GOPATH. Please see X for information on how to set up your Go project."
53+
}
54+
return msg, nil
55+
}
56+
57+
// invokeGo returns the stdout of a go command invocation.
58+
// Borrowed from golang.org/x/tools/go/packages/golist.go.
59+
func invokeGo(ctx context.Context, dir string, env []string, args ...string) (*bytes.Buffer, error) {
60+
stdout := new(bytes.Buffer)
61+
stderr := new(bytes.Buffer)
62+
cmd := exec.CommandContext(ctx, "go", args...)
63+
// On darwin the cwd gets resolved to the real path, which breaks anything that
64+
// expects the working directory to keep the original path, including the
65+
// go command when dealing with modules.
66+
// The Go stdlib has a special feature where if the cwd and the PWD are the
67+
// same node then it trusts the PWD, so by setting it in the env for the child
68+
// process we fix up all the paths returned by the go command.
69+
cmd.Env = append(append([]string{}, env...), "PWD="+dir)
70+
cmd.Dir = dir
71+
cmd.Stdout = stdout
72+
cmd.Stderr = stderr
73+
74+
if err := cmd.Run(); err != nil {
75+
// Check for 'go' executable not being found.
76+
if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound {
77+
return nil, fmt.Errorf("'gopls requires 'go', but %s", exec.ErrNotFound)
78+
}
79+
if _, ok := err.(*exec.ExitError); !ok {
80+
// Catastrophic error:
81+
// - context cancellation
82+
return nil, fmt.Errorf("couldn't exec 'go %v': %s %T", args, err, err)
83+
}
84+
}
85+
return stdout, nil
86+
}

internal/lsp/source/format.go

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"bytes"
1010
"context"
1111
"go/format"
12+
"log"
1213

1314
"golang.org/x/tools/go/packages"
1415
"golang.org/x/tools/internal/imports"
@@ -268,6 +269,7 @@ func hasParseErrors(pkg Package, uri span.URI) bool {
268269
func hasListErrors(errors []packages.Error) bool {
269270
for _, err := range errors {
270271
if err.Kind == packages.ListError {
272+
log.Printf("LIST ERROR: %v", err)
271273
return true
272274
}
273275
}

internal/lsp/source/source_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) {
6868
if err != nil {
6969
t.Fatal(err)
7070
}
71-
results, err := source.Diagnostics(r.ctx, r.view, f.(source.GoFile), nil)
71+
results, _, err := source.Diagnostics(r.ctx, r.view, f.(source.GoFile), nil)
7272
if err != nil {
7373
t.Fatal(err)
7474
}

0 commit comments

Comments
 (0)