internal/lsp: report diagnostics on go.work files
Report diagnostics on use lines where the directory doesn't have a
go.mod file, and on syntax errors in go.work.
For golang/go#50930
Change-Id: Idab36b43d86c4842f8eecd5c071ce0587e6f27b3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/389317
Trust: Michael Matloob <matloob@golang.org>
Run-TryBot: Michael Matloob <matloob@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go
index 82bfd12..4ff6f72 100644
--- a/gopls/internal/regtest/workspace/workspace_test.go
+++ b/gopls/internal/regtest/workspace/workspace_test.go
@@ -796,6 +796,39 @@
})
}
+func TestUseGoWorkDiagnosticMissingModule(t *testing.T) {
+ const files = `
+-- go.work --
+go 1.18
+
+use ./foo
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("go.work")
+ env.Await(
+ env.DiagnosticAtRegexpWithMessage("go.work", "use", "directory ./foo does not contain a module"),
+ )
+ t.Log("bar")
+ })
+}
+
+func TestUseGoWorkDiagnosticSyntaxError(t *testing.T) {
+ const files = `
+-- go.work --
+go 1.18
+
+usa ./foo
+replace
+`
+ Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("go.work")
+ env.Await(
+ env.DiagnosticAtRegexpWithMessage("go.work", "usa", "unknown directive: usa"),
+ env.DiagnosticAtRegexpWithMessage("go.work", "replace", "usage: replace"),
+ )
+ })
+}
+
func TestNonWorkspaceFileCreation(t *testing.T) {
testenv.NeedsGo1Point(t, 13)
diff --git a/internal/lsp/cache/mod.go b/internal/lsp/cache/mod.go
index b555b9a..8a2d42a 100644
--- a/internal/lsp/cache/mod.go
+++ b/internal/lsp/cache/mod.go
@@ -73,13 +73,13 @@
if err != nil {
return &parseModData{err: err}
}
- parseErrors = []*source.Diagnostic{{
+ parseErrors = append(parseErrors, &source.Diagnostic{
URI: modFH.URI(),
Range: rng,
Severity: protocol.SeverityError,
Source: source.ParseError,
Message: mfErr.Err.Error(),
- }}
+ })
}
}
return &parseModData{
@@ -131,7 +131,7 @@
contents, err := modFH.Read()
if err != nil {
- return &parseModData{err: err}
+ return &parseWorkData{err: err}
}
m := &protocol.ColumnMapper{
URI: modFH.URI(),
@@ -144,20 +144,20 @@
if parseErr != nil {
mfErrList, ok := parseErr.(modfile.ErrorList)
if !ok {
- return &parseModData{err: fmt.Errorf("unexpected parse error type %v", parseErr)}
+ return &parseWorkData{err: fmt.Errorf("unexpected parse error type %v", parseErr)}
}
for _, mfErr := range mfErrList {
rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos)
if err != nil {
- return &parseModData{err: err}
+ return &parseWorkData{err: err}
}
- parseErrors = []*source.Diagnostic{{
+ parseErrors = append(parseErrors, &source.Diagnostic{
URI: modFH.URI(),
Range: rng,
Severity: protocol.SeverityError,
Source: source.ParseError,
Message: mfErr.Err.Error(),
- }}
+ })
}
}
return &parseWorkData{
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
index e786c02..1554fbe 100644
--- a/internal/lsp/cache/snapshot.go
+++ b/internal/lsp/cache/snapshot.go
@@ -162,6 +162,10 @@
return uris
}
+func (s *snapshot) WorkFile() span.URI {
+ return s.workspace.workFile
+}
+
func (s *snapshot) Templates() map[span.URI]source.VersionedFileHandle {
s.mu.Lock()
defer s.mu.Unlock()
diff --git a/internal/lsp/diagnostics.go b/internal/lsp/diagnostics.go
index e352e4b..3bf8122 100644
--- a/internal/lsp/diagnostics.go
+++ b/internal/lsp/diagnostics.go
@@ -21,6 +21,7 @@
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/template"
+ "golang.org/x/tools/internal/lsp/work"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/xcontext"
errors "golang.org/x/xerrors"
@@ -35,6 +36,7 @@
analysisSource
typeCheckSource
orphanedSource
+ workSource
)
// A diagnosticReport holds results for a single diagnostic source.
@@ -210,6 +212,23 @@
s.storeDiagnostics(snapshot, id.URI, modSource, diags)
}
+ // Diagnose the go.work file, if it exists.
+ workReports, workErr := work.Diagnostics(ctx, snapshot)
+ if ctx.Err() != nil {
+ log.Trace.Log(ctx, "diagnose cancelled")
+ return
+ }
+ if workErr != nil {
+ event.Error(ctx, "warning: diagnose go.work", workErr, tag.Directory.Of(snapshot.View().Folder().Filename()), tag.Snapshot.Of(snapshot.ID()))
+ }
+ for id, diags := range workReports {
+ if id.URI == "" {
+ event.Error(ctx, "missing URI for work file diagnostics", fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename()))
+ continue
+ }
+ s.storeDiagnostics(snapshot, id.URI, workSource, diags)
+ }
+
// Diagnose all of the packages in the workspace.
wsPkgs, err := snapshot.ActivePackages(ctx)
if s.shouldIgnoreError(ctx, snapshot, err) {
diff --git a/internal/lsp/mod/code_lens.go b/internal/lsp/mod/code_lens.go
index f18aaf7..b26bae7 100644
--- a/internal/lsp/mod/code_lens.go
+++ b/internal/lsp/mod/code_lens.go
@@ -14,7 +14,6 @@
"golang.org/x/tools/internal/lsp/command"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
)
// LensFuncs returns the supported lensFuncs for go.mod files.
@@ -129,7 +128,7 @@
return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI())
}
syntax := pm.File.Module.Syntax
- return lineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End)
+ return source.LineToRange(pm.Mapper, fh.URI(), syntax.Start, syntax.End)
}
// firstRequireRange returns the range for the first "require" in the given
@@ -150,19 +149,5 @@
if start.Byte == 0 || firstRequire.Start.Byte < start.Byte {
start, end = firstRequire.Start, firstRequire.End
}
- return lineToRange(pm.Mapper, fh.URI(), start, end)
-}
-
-func lineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) {
- line, col, err := m.Converter.ToPosition(start.Byte)
- if err != nil {
- return protocol.Range{}, err
- }
- s := span.NewPoint(line, col, start.Byte)
- line, col, err = m.Converter.ToPosition(end.Byte)
- if err != nil {
- return protocol.Range{}, err
- }
- e := span.NewPoint(line, col, end.Byte)
- return m.Range(span.New(uri, s, e))
+ return source.LineToRange(pm.Mapper, fh.URI(), start, end)
}
diff --git a/internal/lsp/mod/diagnostics.go b/internal/lsp/mod/diagnostics.go
index 4b4d0cb..9c49d8b 100644
--- a/internal/lsp/mod/diagnostics.go
+++ b/internal/lsp/mod/diagnostics.go
@@ -61,7 +61,7 @@
if !ok || req.Mod.Version == ver {
continue
}
- rng, err := lineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
+ rng, err := source.LineToRange(pm.Mapper, fh.URI(), req.Syntax.Start, req.Syntax.End)
if err != nil {
return nil, err
}
diff --git a/internal/lsp/source/util.go b/internal/lsp/source/util.go
index 962419b..e41c1d4 100644
--- a/internal/lsp/source/util.go
+++ b/internal/lsp/source/util.go
@@ -17,6 +17,7 @@
"strconv"
"strings"
+ "golang.org/x/mod/modfile"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
@@ -563,3 +564,18 @@
size := tok.Pos(tok.Size())
return int(pos) >= tok.Base() && pos <= size
}
+
+// LineToRange creates a Range spanning start and end.
+func LineToRange(m *protocol.ColumnMapper, uri span.URI, start, end modfile.Position) (protocol.Range, error) {
+ line, col, err := m.Converter.ToPosition(start.Byte)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ s := span.NewPoint(line, col, start.Byte)
+ line, col, err = m.Converter.ToPosition(end.Byte)
+ if err != nil {
+ return protocol.Range{}, err
+ }
+ e := span.NewPoint(line, col, end.Byte)
+ return m.Range(span.New(uri, s, e))
+}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 9e9b035..4d7d411 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -130,6 +130,9 @@
// GoModForFile returns the URI of the go.mod file for the given URI.
GoModForFile(uri span.URI) span.URI
+ // WorkFile, if non-empty, is the go.work file for the workspace.
+ WorkFile() span.URI
+
// ParseWork is used to parse go.work files.
ParseWork(ctx context.Context, fh FileHandle) (*ParsedWorkFile, error)
@@ -656,6 +659,7 @@
OptimizationDetailsError DiagnosticSource = "optimizer details"
UpgradeNotification DiagnosticSource = "upgrade available"
TemplateError DiagnosticSource = "template"
+ WorkFileError DiagnosticSource = "go.work file"
)
func AnalyzerErrorKind(name string) DiagnosticSource {
diff --git a/internal/lsp/work/diagnostics.go b/internal/lsp/work/diagnostics.go
new file mode 100644
index 0000000..d752484
--- /dev/null
+++ b/internal/lsp/work/diagnostics.go
@@ -0,0 +1,87 @@
+// Copyright 2022 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 work
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "golang.org/x/tools/internal/event"
+ "golang.org/x/tools/internal/lsp/debug/tag"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
+)
+
+func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[source.VersionedFileIdentity][]*source.Diagnostic, error) {
+ ctx, done := event.Start(ctx, "work.Diagnostics", tag.Snapshot.Of(snapshot.ID()))
+ defer done()
+
+ reports := map[source.VersionedFileIdentity][]*source.Diagnostic{}
+ uri := snapshot.WorkFile()
+ if uri == "" {
+ return nil, nil
+ }
+ fh, err := snapshot.GetVersionedFile(ctx, uri)
+ if err != nil {
+ return nil, err
+ }
+ reports[fh.VersionedFileIdentity()] = []*source.Diagnostic{}
+ diagnostics, err := DiagnosticsForWork(ctx, snapshot, fh)
+ if err != nil {
+ return nil, err
+ }
+ for _, d := range diagnostics {
+ fh, err := snapshot.GetVersionedFile(ctx, d.URI)
+ if err != nil {
+ return nil, err
+ }
+ reports[fh.VersionedFileIdentity()] = append(reports[fh.VersionedFileIdentity()], d)
+ }
+
+ return reports, nil
+}
+
+func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]*source.Diagnostic, error) {
+ pw, err := snapshot.ParseWork(ctx, fh)
+ if err != nil {
+ if pw == nil || len(pw.ParseErrors) == 0 {
+ return nil, err
+ }
+ return pw.ParseErrors, nil
+ }
+
+ // Add diagnostic if a directory does not contain a module.
+ var diagnostics []*source.Diagnostic
+ workdir := filepath.Dir(pw.URI.Filename())
+ for _, use := range pw.File.Use {
+ modroot := filepath.FromSlash(use.Path)
+ if !filepath.IsAbs(modroot) {
+ modroot = filepath.Join(workdir, modroot)
+ }
+
+ rng, err := source.LineToRange(pw.Mapper, fh.URI(), use.Syntax.Start, use.Syntax.End)
+ if err != nil {
+ return nil, err
+ }
+
+ modfh, err := snapshot.GetFile(ctx, span.URIFromPath(filepath.Join(modroot, "go.mod")))
+ if err != nil {
+ return nil, err
+ }
+ if _, err := modfh.Read(); err != nil && os.IsNotExist(err) {
+ diagnostics = append(diagnostics, &source.Diagnostic{
+ URI: fh.URI(),
+ Range: rng,
+ Severity: protocol.SeverityError,
+ Source: source.UnknownError, // Do we need a new source for this?
+ Message: fmt.Sprintf("directory %v does not contain a module", use.Path),
+ })
+ }
+ }
+ return diagnostics, nil
+}