Skip to content

Commit 01fd755

Browse files
ethanalee-workgopherbot
authored andcommitted
gopls/internal/server: trigger vulncheck prompt for go.mod changes
- Detect go.mod dependency changes upon file modification. - Create new vulncheck prompt that allows a user to pick their preferred method of vulncheck usage. - Add tests to verify hashing of go.mod dependencies and vulncheck prompt logic. For golang/go#75447 Change-Id: If6f692bc825419dc4d10c997162bb755438f01b4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/722100 Reviewed-by: Hongxiang Jiang <hxjiang@golang.org> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Ethan Lee <ethanalee@google.com>
1 parent 00b22d9 commit 01fd755

File tree

4 files changed

+459
-0
lines changed

4 files changed

+459
-0
lines changed

‎gopls/internal/server/server.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ func New(session *cache.Session, client protocol.ClientCloser, options *settings
5353
progress: progress.NewTracker(client),
5454
options: options,
5555
viewsToDiagnose: make(map[*cache.View]uint64),
56+
// checkingGoMod: make(map[protocol.DocumentURI]struct{}),
5657
}
5758
}
5859

@@ -186,6 +187,9 @@ type server struct {
186187
lastModificationID uint64 // incrementing clock
187188

188189
runGovulncheckInProgress atomic.Bool
190+
191+
// goModCheckInProgress is used to serialize calls to checkGoModDeps
192+
goModCheckInProgress atomic.Bool
189193
}
190194

191195
func (s *server) WorkDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error {

‎gopls/internal/server/text_synchronization.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ func (s *server) didModifyFiles(ctx context.Context, modifications []file.Modifi
254254
// to their files.
255255
modifications = s.session.ExpandModificationsToDirectories(ctx, modifications)
256256

257+
for _, m := range modifications {
258+
// TODO: also handle go.work changes as well.
259+
if strings.HasSuffix(m.URI.Path(), "go.mod") {
260+
if m.Action == file.Create || m.Action == file.Change {
261+
s.checkGoModDeps(ctx, m.URI)
262+
}
263+
}
264+
}
265+
257266
viewsToDiagnose, err := s.session.DidModifyFiles(ctx, modifications)
258267
if err != nil {
259268
return err
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package server
6+
7+
import (
8+
"context"
9+
"crypto/sha256"
10+
"encoding/hex"
11+
"errors"
12+
"fmt"
13+
"os"
14+
"time"
15+
16+
"golang.org/x/mod/modfile"
17+
"golang.org/x/tools/gopls/internal/filecache"
18+
"golang.org/x/tools/gopls/internal/protocol"
19+
"golang.org/x/tools/gopls/internal/settings"
20+
"golang.org/x/tools/internal/event"
21+
)
22+
23+
const (
24+
// goModHashKind is the kind for the go.mod hash in the filecache.
25+
goModHashKind = "gomodhash"
26+
)
27+
28+
// computeGoModHash computes the SHA256 hash of the go.mod file's dependencies.
29+
// It only considers the Require, Exclude, and Replace directives and ignores
30+
// other parts of the file.
31+
func computeGoModHash(file *modfile.File) (string, error) {
32+
h := sha256.New()
33+
for _, req := range file.Require {
34+
if _, err := h.Write([]byte(req.Mod.Path + req.Mod.Version)); err != nil {
35+
return "", err
36+
}
37+
}
38+
for _, exc := range file.Exclude {
39+
if _, err := h.Write([]byte(exc.Mod.Path + exc.Mod.Version)); err != nil {
40+
return "", err
41+
}
42+
}
43+
for _, rep := range file.Replace {
44+
if _, err := h.Write([]byte(rep.Old.Path + rep.Old.Version + rep.New.Path + rep.New.Version)); err != nil {
45+
return "", err
46+
}
47+
}
48+
return hex.EncodeToString(h.Sum(nil)), nil
49+
}
50+
51+
// showMessageRequest causes the client to show a prompt that the user can respond to.
52+
func showMessageRequest(ctx context.Context, cli protocol.Client, typ protocol.MessageType, message string, actions ...string) (string, error) {
53+
var actionItems []protocol.MessageActionItem
54+
for _, action := range actions {
55+
actionItems = append(actionItems, protocol.MessageActionItem{Title: action})
56+
}
57+
params := &protocol.ShowMessageRequestParams{
58+
Type: typ,
59+
Message: message,
60+
Actions: actionItems,
61+
}
62+
// Timeout used to wait for the user to respond to a message request.
63+
const timeout = 15 * time.Second
64+
ctx, cancel := context.WithTimeout(ctx, timeout)
65+
defer cancel()
66+
67+
result, err := cli.ShowMessageRequest(ctx, params)
68+
69+
if err != nil {
70+
if errors.Is(err, context.DeadlineExceeded) {
71+
return "", nil
72+
}
73+
return "", err
74+
}
75+
if result == nil {
76+
return "", nil // User dismissed the notification
77+
}
78+
return result.Title, nil
79+
}
80+
81+
func (s *server) checkGoModDeps(ctx context.Context, uri protocol.DocumentURI) {
82+
if s.Options().Vulncheck != settings.ModeVulncheckPrompt {
83+
return
84+
}
85+
if !s.goModCheckInProgress.CompareAndSwap(false, true) {
86+
return
87+
}
88+
89+
go func() {
90+
defer s.goModCheckInProgress.Store(false)
91+
92+
ctx, done := event.Start(ctx, "server.CheckGoModDeps")
93+
defer done()
94+
95+
var (
96+
newHash string
97+
oldHash string
98+
pathHash [32]byte
99+
)
100+
{
101+
newContent, err := os.ReadFile(uri.Path())
102+
if err != nil {
103+
event.Error(ctx, "reading new go.mod content", err)
104+
return
105+
}
106+
newModFile, err := modfile.Parse("go.mod", newContent, nil)
107+
if err != nil {
108+
event.Error(ctx, "parsing new go.mod", err)
109+
return
110+
}
111+
hash, err := computeGoModHash(newModFile)
112+
if err != nil {
113+
event.Error(ctx, "computing new go.mod hash", err)
114+
return
115+
}
116+
newHash = hash
117+
118+
pathHash = sha256.Sum256([]byte(uri.Path()))
119+
oldHashBytes, err := filecache.Get(goModHashKind, pathHash)
120+
if err != nil && err != filecache.ErrNotFound {
121+
event.Error(ctx, "reading old go.mod hash from filecache", err)
122+
return
123+
}
124+
oldHash = string(oldHashBytes)
125+
}
126+
if oldHash != newHash {
127+
fileLink := fmt.Sprintf("[%s](%s)", uri.Path(), string(uri))
128+
govulncheckLink := "[govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck)"
129+
message := fmt.Sprintf("Dependencies have changed in %s, would you like to run %s to check for vulnerabilities?", fileLink, govulncheckLink)
130+
action, err := showMessageRequest(ctx, s.client, protocol.Info, message, "Yes", "No", "Always", "Never")
131+
132+
if err != nil {
133+
event.Error(ctx, "showing go.mod changed notification", err)
134+
return
135+
}
136+
137+
// TODO: Implement persistent storage for "Always" and "Never" preferences.
138+
// TODO: Implement the logic to run govulncheck when action is "Yes" or "Always".
139+
if action == "No" || action == "Never" || action == "" {
140+
return // Skip the check and don't update the hash.
141+
}
142+
143+
if err := filecache.Set(goModHashKind, pathHash, []byte(newHash)); err != nil {
144+
event.Error(ctx, "writing new go.mod hash to filecache", err)
145+
return
146+
}
147+
}
148+
}()
149+
}

0 commit comments

Comments
 (0)