Skip to content

Commit 88e38c1

Browse files
committed
internal/lsp: make sure diagnostics only refer to existing files
We were previously sending diagnostics for nonexistent files, and then adding them to the snapshot in the process. Remove this behavior, and add a regression test. Case insensitive filesystems were too confusing to write a test for, but fortunately, Filippo reported another instance of this bug, so I used that for the regression test. Fixes golang/go#38602 Change-Id: I4ef6b51944f3338e838875a5aafffd066e8392f4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/230315 Run-TryBot: Rebecca Stambler <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Heschi Kreinick <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent 002d754 commit 88e38c1

File tree

10 files changed

+133
-44
lines changed

10 files changed

+133
-44
lines changed

internal/lsp/cache/load.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,11 @@ func (s *snapshot) load(ctx context.Context, scopes ...interface{}) error {
9292
if ctx.Err() != nil {
9393
return ctx.Err()
9494
}
95-
96-
event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs)))
95+
if err != nil {
96+
event.Error(ctx, "go/packages.Load", err, tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs)))
97+
} else {
98+
event.Log(ctx, "go/packages.Load", tag.Snapshot.Of(s.ID()), tag.Directory.Of(cfg.Dir), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs)))
99+
}
97100
if len(pkgs) == 0 {
98101
return err
99102
}

internal/lsp/cache/snapshot.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,17 @@ func (s *snapshot) isWorkspacePackage(id packageID) (packagePath, bool) {
500500
scope, ok := s.workspacePackages[id]
501501
return scope, ok
502502
}
503+
func (s *snapshot) FindFile(uri span.URI) source.FileHandle {
504+
f, err := s.view.getFile(uri)
505+
if err != nil {
506+
return nil
507+
}
508+
509+
s.mu.Lock()
510+
defer s.mu.Unlock()
511+
512+
return s.files[f.URI()]
513+
}
503514

504515
// GetFile returns a File for the given URI. It will always succeed because it
505516
// adds the file to the managed set if needed.

internal/lsp/fake/editor_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func main() {
4848
`
4949

5050
func TestClientEditing(t *testing.T) {
51-
ws, err := NewSandbox("TestClientEditing", exampleProgram, "")
51+
ws, err := NewSandbox("TestClientEditing", exampleProgram, "", false)
5252
if err != nil {
5353
t.Fatal(err)
5454
}

internal/lsp/fake/sandbox.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type Sandbox struct {
3232
// working directory populated by the txtar-encoded content in srctxt, and a
3333
// file-based module proxy populated with the txtar-encoded content in
3434
// proxytxt.
35-
func NewSandbox(name, srctxt, proxytxt string, env ...string) (_ *Sandbox, err error) {
35+
func NewSandbox(name, srctxt, proxytxt string, inGopath bool, env ...string) (_ *Sandbox, err error) {
3636
sb := &Sandbox{
3737
name: name,
3838
env: env,
@@ -48,16 +48,23 @@ func NewSandbox(name, srctxt, proxytxt string, env ...string) (_ *Sandbox, err e
4848
return nil, fmt.Errorf("creating temporary workdir: %v", err)
4949
}
5050
sb.basedir = basedir
51-
sb.gopath = filepath.Join(sb.basedir, "gopath")
52-
workdir := filepath.Join(sb.basedir, "work")
5351
proxydir := filepath.Join(sb.basedir, "proxy")
54-
for _, subdir := range []string{sb.gopath, workdir, proxydir} {
52+
sb.gopath = filepath.Join(sb.basedir, "gopath")
53+
// Set the working directory as $GOPATH/src if inGopath is true.
54+
workdir := filepath.Join(sb.gopath, "src")
55+
dirs := []string{sb.gopath, proxydir}
56+
if !inGopath {
57+
workdir = filepath.Join(sb.basedir, "work")
58+
dirs = append(dirs, workdir)
59+
}
60+
for _, subdir := range dirs {
5561
if err := os.Mkdir(subdir, 0755); err != nil {
5662
return nil, err
5763
}
5864
}
5965
sb.Proxy, err = NewProxy(proxydir, proxytxt)
6066
sb.Workdir, err = NewWorkdir(workdir, srctxt)
67+
6168
return sb, nil
6269
}
6370

internal/lsp/lsprpc/lsprpc_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func TestDebugInfoLifecycle(t *testing.T) {
190190
resetExitFuncs := OverrideExitFuncsForTest()
191191
defer resetExitFuncs()
192192

193-
sb, err := fake.NewSandbox("gopls-lsprpc-test", exampleProgram, "")
193+
sb, err := fake.NewSandbox("gopls-lsprpc-test", exampleProgram, "", false)
194194
if err != nil {
195195
t.Fatal(err)
196196
}

internal/lsp/regtest/diagnostics_test.go

Lines changed: 60 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package regtest
66

77
import (
8+
"fmt"
9+
"os"
810
"testing"
911

1012
"golang.org/x/tools/internal/lsp"
@@ -291,7 +293,7 @@ func main() {}
291293
// package renaming was fully processed. Therefore, in order for this
292294
// test to actually exercise the bug, we must wait until that work has
293295
// completed.
294-
EmptyDiagnostics("a.go"),
296+
NoDiagnostics("a.go"),
295297
CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), 1),
296298
)
297299
})
@@ -405,6 +407,7 @@ var X = 0
405407
}, WithEnv("GOFLAGS=-tags=foo"))
406408
}
407409

410+
// Tests golang/go#38467.
408411
func TestNoSuggestedFixesForGeneratedFiles_Issue38467(t *testing.T) {
409412
const generated = `
410413
-- go.mod --
@@ -441,36 +444,78 @@ func _() {
441444
})
442445
}
443446

444-
// Expect a module/gopath error if there is an error in the file at startup
447+
// Expect a module/GOPATH error if there is an error in the file at startup.
448+
// Tests golang/go#37279.
445449
func TestShowMessage_Issue37279(t *testing.T) {
446450
const noModule = `
447451
-- a.go --
448-
package foo
449-
450-
func f() {
451-
fmt.Printl()
452-
}
453-
`
452+
package foo
453+
454+
func f() {
455+
fmt.Printl()
456+
}
457+
`
454458
runner.Run(t, noModule, func(t *testing.T, env *Env) {
455459
env.OpenFile("a.go")
456460
env.Await(env.DiagnosticAtRegexp("a.go", "fmt.Printl"), SomeShowMessage(""))
457461
})
458462
}
459463

460-
// Expecxt no module/gopath error if there is no error in the file
464+
// Expect no module/GOPATH error if there is no error in the file.
465+
// Tests golang/go#37279.
461466
func TestNoShowMessage_Issue37279(t *testing.T) {
462467
const noModule = `
463468
-- a.go --
464-
package foo
465-
466-
func f() {
467-
}
468-
`
469+
package foo
470+
471+
func f() {
472+
}
473+
`
469474
runner.Run(t, noModule, func(t *testing.T, env *Env) {
470475
env.OpenFile("a.go")
471-
env.Await(EmptyDiagnostics("a.go"), EmptyShowMessage(""))
476+
env.Await(NoDiagnostics("a.go"), EmptyShowMessage(""))
472477
// introduce an error, expect no Show Message
473478
env.RegexpReplace("a.go", "func", "fun")
474479
env.Await(env.DiagnosticAtRegexp("a.go", "fun"), EmptyShowMessage(""))
475480
})
476481
}
482+
483+
// Tests golang/go#38602.
484+
func TestNonexistentFileDiagnostics_Issue38602(t *testing.T) {
485+
const collision = `
486+
-- x/x.go --
487+
package x
488+
489+
import "x/hello"
490+
491+
func Hello() {
492+
hello.HiThere()
493+
}
494+
-- x/main.go --
495+
package main
496+
497+
func main() {
498+
fmt.Println("")
499+
}
500+
`
501+
runner.Run(t, collision, func(t *testing.T, env *Env) {
502+
env.OpenFile("x/main.go")
503+
env.Await(
504+
env.DiagnosticAtRegexp("x/main.go", "fmt.Println"),
505+
)
506+
env.OrganizeImports("x/main.go")
507+
// span.Parse misparses the error message when multiple packages are
508+
// defined in the same directory, creating a garbage filename.
509+
// Previously, we would send diagnostics for this nonexistent file.
510+
// This test checks that we don't send diagnostics for this file.
511+
dir, err := os.Getwd()
512+
if err != nil {
513+
t.Fatal(err)
514+
}
515+
badFile := fmt.Sprintf("%s/found packages main (main.go) and x (x.go) in %s/src/x", dir, env.Sandbox.GOPATH())
516+
env.Await(
517+
EmptyDiagnostics("x/main.go"),
518+
NoDiagnostics(badFile),
519+
)
520+
}, InGOPATH())
521+
}

internal/lsp/regtest/env.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,11 +461,26 @@ func (e DiagnosticExpectation) Description() string {
461461
return fmt.Sprintf("%s: %s", e.path, e.description)
462462
}
463463

464-
// EmptyDiagnostics asserts that diagnostics are empty for the
464+
// EmptyDiagnostics asserts that empty diagnostics are sent for the
465465
// workspace-relative path name.
466466
func EmptyDiagnostics(name string) Expectation {
467467
check := func(s State) (Verdict, interface{}) {
468-
if diags, ok := s.diagnostics[name]; !ok || len(diags.Diagnostics) == 0 {
468+
if diags := s.diagnostics[name]; diags != nil && len(diags.Diagnostics) == 0 {
469+
return Met, nil
470+
}
471+
return Unmet, nil
472+
}
473+
return SimpleExpectation{
474+
check: check,
475+
description: "empty diagnostics",
476+
}
477+
}
478+
479+
// NoDiagnostics asserts that no diagnostics are sent for the
480+
// workspace-relative path name.
481+
func NoDiagnostics(name string) Expectation {
482+
check := func(s State) (Verdict, interface{}) {
483+
if _, ok := s.diagnostics[name]; !ok {
469484
return Met, nil
470485
}
471486
return Unmet, nil

internal/lsp/regtest/runner.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ type runConfig struct {
7171
timeout time.Duration
7272
env []string
7373
skipCleanup bool
74+
gopath bool
7475
}
7576

7677
func (r *Runner) defaultConfig() *runConfig {
@@ -120,6 +121,14 @@ func WithEnv(env ...string) RunOption {
120121
})
121122
}
122123

124+
// InGOPATH configures the workspace working directory to be GOPATH, rather
125+
// than a separate working directory for use with modules.
126+
func InGOPATH() RunOption {
127+
return optionSetter(func(opts *runConfig) {
128+
opts.gopath = true
129+
})
130+
}
131+
123132
// SkipCleanup is used only for debugging: is skips cleaning up the tests state
124133
// after completion.
125134
func SkipCleanup() RunOption {
@@ -159,7 +168,7 @@ func (r *Runner) Run(t *testing.T, filedata string, test func(t *testing.T, e *E
159168
defer cancel()
160169
ctx = debug.WithInstance(ctx, "", "")
161170

162-
sandbox, err := fake.NewSandbox("regtest", filedata, config.proxyTxt, config.env...)
171+
sandbox, err := fake.NewSandbox("regtest", filedata, config.proxyTxt, config.gopath, config.env...)
163172
if err != nil {
164173
t.Fatal(err)
165174
}

internal/lsp/source/diagnostics.go

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,7 @@ func Diagnostics(ctx context.Context, snapshot Snapshot, ph PackageHandle, missi
8484
// Prepare the reports we will send for the files in this package.
8585
reports := make(map[FileIdentity][]*Diagnostic)
8686
for _, fh := range pkg.CompiledGoFiles() {
87-
if err := clearReports(snapshot, reports, fh.File().Identity().URI); err != nil {
88-
return nil, warn, err
89-
}
87+
clearReports(snapshot, reports, fh.File().Identity().URI)
9088
if len(missing) > 0 {
9189
if err := missingModulesDiagnostics(ctx, snapshot, reports, missing, fh.File().Identity().URI); err != nil {
9290
return nil, warn, err
@@ -107,9 +105,7 @@ func Diagnostics(ctx context.Context, snapshot Snapshot, ph PackageHandle, missi
107105
}
108106
}
109107
}
110-
if err := clearReports(snapshot, reports, e.URI); err != nil {
111-
return nil, warn, err
112-
}
108+
clearReports(snapshot, reports, e.URI)
113109
}
114110
// Run diagnostics for the package that this URI belongs to.
115111
hadDiagnostics, hadTypeErrors, err := diagnostics(ctx, snapshot, reports, pkg, len(ph.MissingDependencies()) > 0)
@@ -307,30 +303,29 @@ func analyses(ctx context.Context, snapshot Snapshot, reports map[FileIdentity][
307303
return nil
308304
}
309305

310-
func clearReports(snapshot Snapshot, reports map[FileIdentity][]*Diagnostic, uri span.URI) error {
306+
func clearReports(snapshot Snapshot, reports map[FileIdentity][]*Diagnostic, uri span.URI) {
311307
if snapshot.View().Ignore(uri) {
312-
return nil
308+
return
313309
}
314-
fh, err := snapshot.GetFile(uri)
315-
if err != nil {
316-
return err
310+
fh := snapshot.FindFile(uri)
311+
if fh == nil {
312+
return
317313
}
318314
reports[fh.Identity()] = []*Diagnostic{}
319-
return nil
320315
}
321316

322317
func addReports(snapshot Snapshot, reports map[FileIdentity][]*Diagnostic, uri span.URI, diagnostics ...*Diagnostic) error {
323318
if snapshot.View().Ignore(uri) {
324319
return nil
325320
}
326-
fh, err := snapshot.GetFile(uri)
327-
if err != nil {
328-
return err
321+
fh := snapshot.FindFile(uri)
322+
if fh == nil {
323+
return nil
329324
}
330325
identity := fh.Identity()
331326
existingDiagnostics, ok := reports[identity]
332327
if !ok {
333-
return errors.Errorf("diagnostics for unexpected file %s", uri)
328+
return fmt.Errorf("diagnostics for unexpected file %s", uri)
334329
}
335330
if len(diagnostics) == 1 {
336331
d1 := diagnostics[0]

internal/lsp/source/view.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ type Snapshot interface {
3131
// Config returns the configuration for the view.
3232
Config(ctx context.Context) *packages.Config
3333

34-
// GetFile returns the file object for a given URI, initializing it
35-
// if it is not already part of the view.
34+
// FindFile returns the FileHandle for the given URI, if it is already
35+
// in the given snapshot.
36+
FindFile(uri span.URI) FileHandle
37+
38+
// GetFile returns the FileHandle for a given URI, initializing it
39+
// if it is not already part of the snapshot.
3640
GetFile(uri span.URI) (FileHandle, error)
3741

3842
// IsOpen returns whether the editor currently has a file open.

0 commit comments

Comments
 (0)