gopls/mod: show hint for discordant module versions
Sometimes a go.mod file specifies a version of a module that
is different from what Go ends up using. This CL adds
inlay hints to the go.mod file when that happens.
It can happen in a require or replace directive, when some other
module needs a different version.
Accepting the inlay hint will change the specified version to
the one that is used.
There is a new test.
And here is a real-life example:
Create a directory and initialize a go.work file with
go work init ./delve ./tools
git clone https://github.com/go-delve/delve.git
(cd delve; git checkout 4303ae45a8e2996b30d2318f239677a771aef9c1)
git clone https://go.googlesource.com/tools
(cd tools; git checkout 8111118043894a9c0eab886fc370ca123dcf48f1)
Then the delve go.mod file has a hint in the 'require' section.
Fixes: golang/go#57026
Change-Id: Ifb131ed852efad3e2f29524a529f8b23db0d9ee9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/466976
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Peter Weinberger <pjw@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/lsp/inlay_hint.go b/gopls/internal/lsp/inlay_hint.go
index 6aceecb..7efe98b 100644
--- a/gopls/internal/lsp/inlay_hint.go
+++ b/gopls/internal/lsp/inlay_hint.go
@@ -7,15 +7,22 @@
import (
"context"
+ "golang.org/x/tools/gopls/internal/lsp/mod"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
)
func (s *Server) inlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) {
- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go)
+ snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
defer release()
if !ok {
return nil, err
}
- return source.InlayHint(ctx, snapshot, fh, params.Range)
+ switch snapshot.View().FileKind(fh) {
+ case source.Mod:
+ return mod.InlayHint(ctx, snapshot, fh, params.Range)
+ case source.Go:
+ return source.InlayHint(ctx, snapshot, fh, params.Range)
+ }
+ return nil, nil
}
diff --git a/gopls/internal/lsp/mod/inlayhint.go b/gopls/internal/lsp/mod/inlayhint.go
new file mode 100644
index 0000000..e494cc7
--- /dev/null
+++ b/gopls/internal/lsp/mod/inlayhint.go
@@ -0,0 +1,100 @@
+// Copyright 2023 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 mod
+
+import (
+ "context"
+ "fmt"
+
+ "golang.org/x/mod/modfile"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+)
+
+func InlayHint(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, rng protocol.Range) ([]protocol.InlayHint, error) {
+ // Inlay hints are enabled if the client supports them.
+ pm, err := snapshot.ParseMod(ctx, fh)
+ if err != nil {
+ return nil, err
+ }
+ return unexpectedVersion(ctx, snapshot, pm), nil
+}
+
+// Compare the version of the module used in the snapshot's metadata with the
+// version requested by the module, in both cases, taking replaces into account.
+// Produce an InlayHint when the version is the module is not the one usedd.
+func unexpectedVersion(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule) []protocol.InlayHint {
+ var ans []protocol.InlayHint
+ if pm.File == nil {
+ return nil
+ }
+ replaces := make(map[string]*modfile.Replace)
+ requires := make(map[string]*modfile.Require)
+ for _, x := range pm.File.Replace {
+ replaces[x.Old.Path] = x
+ }
+ for _, x := range pm.File.Require {
+ requires[x.Mod.Path] = x
+ }
+ am, _ := snapshot.AllMetadata(ctx)
+ seen := make(map[string]bool)
+ for _, meta := range am {
+ if meta == nil || meta.Module == nil || seen[meta.Module.Path] {
+ continue
+ }
+ seen[meta.Module.Path] = true
+ metaMod := meta.Module
+ metaVersion := metaMod.Version
+ if metaMod.Replace != nil {
+ metaVersion = metaMod.Replace.Version
+ }
+ // These versions can be blank, as in gopls/go.mod's local replace
+ if oldrepl, ok := replaces[metaMod.Path]; ok && oldrepl.New.Version != metaVersion {
+ ih := genHint(oldrepl.Syntax, oldrepl.New.Version, metaVersion, pm.Mapper)
+ if ih != nil {
+ ans = append(ans, *ih)
+ }
+ } else if oldreq, ok := requires[metaMod.Path]; ok && oldreq.Mod.Version != metaVersion {
+ // maybe it was replaced:
+ if _, ok := replaces[metaMod.Path]; ok {
+ continue
+ }
+ ih := genHint(oldreq.Syntax, oldreq.Mod.Version, metaVersion, pm.Mapper)
+ if ih != nil {
+ ans = append(ans, *ih)
+ }
+ }
+ }
+ return ans
+}
+
+func genHint(mline *modfile.Line, oldVersion, newVersion string, m *protocol.Mapper) *protocol.InlayHint {
+ x := mline.End.Byte // the parser has removed trailing whitespace and comments (see modfile_test.go)
+ x -= len(mline.Token[len(mline.Token)-1])
+ line, err := m.OffsetPosition(x)
+ if err != nil {
+ return nil
+ }
+ part := protocol.InlayHintLabelPart{
+ Value: newVersion,
+ Tooltip: &protocol.OrPTooltipPLabel{
+ Value: fmt.Sprintf("used metadata's version %s rather than go.mod's version %s", newVersion, oldVersion),
+ },
+ }
+ rng, err := m.OffsetRange(x, mline.End.Byte)
+ if err != nil {
+ return nil
+ }
+ te := protocol.TextEdit{
+ Range: rng,
+ NewText: newVersion,
+ }
+ return &protocol.InlayHint{
+ Position: line,
+ Label: []protocol.InlayHintLabelPart{part},
+ Kind: protocol.Parameter,
+ PaddingRight: true,
+ TextEdits: []protocol.TextEdit{te},
+ }
+}
diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go
index e884695..92639d2 100644
--- a/gopls/internal/lsp/source/view.go
+++ b/gopls/internal/lsp/source/view.go
@@ -857,6 +857,7 @@
Govulncheck DiagnosticSource = "govulncheck"
TemplateError DiagnosticSource = "template"
WorkFileError DiagnosticSource = "go.work file"
+ ConsistencyInfo DiagnosticSource = "consistency"
)
func AnalyzerErrorKind(name string) DiagnosticSource {
diff --git a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go
index 483118d..268f109 100644
--- a/gopls/internal/regtest/modfile/modfile_test.go
+++ b/gopls/internal/regtest/modfile/modfile_test.go
@@ -1186,3 +1186,78 @@
env.SaveBuffer("go.work") // doesn't fail
})
}
+
+func TestInconsistentMod(t *testing.T) {
+ const proxy = `
+-- golang.org/x/mod@v0.7.0/go.mod --
+go 1.20
+module golang.org/x/mod
+-- golang.org/x/mod@v0.7.0/a.go --
+package mod
+func AutoQuote(string) string { return ""}
+-- golang.org/x/mod@v0.9.0/go.mod --
+go 1.20
+module golang.org/x/mod
+-- golang.org/x/mod@v0.9.0/a.go --
+package mod
+func AutoQuote(string) string { return ""}
+`
+ const files = `
+-- go.work --
+go 1.20
+use (
+ ./a
+ ./b
+)
+
+-- a/go.mod --
+module a.mod.com
+go 1.20
+require golang.org/x/mod v0.6.0 // yyy
+replace golang.org/x/mod v0.6.0 => golang.org/x/mod v0.7.0
+-- a/main.go --
+package main
+import "golang.org/x/mod"
+import "fmt"
+func main() {fmt.Println(mod.AutoQuote(""))}
+
+-- b/go.mod --
+module b.mod.com
+go 1.20
+require golang.org/x/mod v0.9.0 // xxx
+-- b/main.go --
+package aaa
+import "golang.org/x/mod"
+import "fmt"
+func main() {fmt.Println(mod.AutoQuote(""))}
+var A int
+
+-- b/c/go.mod --
+module c.b.mod.com
+go 1.20
+require b.mod.com v0.4.2
+replace b.mod.com => ../
+-- b/c/main.go --
+package main
+import "b.mod.com/aaa"
+import "fmt"
+func main() {fmt.Println(aaa.A)}
+`
+ testenv.NeedsGo1Point(t, 18)
+ WithOptions(
+ ProxyFiles(proxy),
+ Modes(Default),
+ ).Run(t, files, func(t *testing.T, env *Env) {
+ env.OpenFile("a/go.mod")
+ ahints := env.InlayHints("a/go.mod")
+ if len(ahints) != 1 {
+ t.Errorf("expected exactly one hint, got %d: %#v", len(ahints), ahints)
+ }
+ env.OpenFile("b/c/go.mod")
+ bhints := env.InlayHints("b/c/go.mod")
+ if len(bhints) != 0 {
+ t.Errorf("expected no hints, got %d: %#v", len(bhints), bhints)
+ }
+ })
+
+}