golang.org/x/tools/gopls@v0.15.3/internal/mod/hover.go (about) 1 // Copyright 2020 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 mod 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "sort" 12 "strings" 13 14 "golang.org/x/mod/modfile" 15 "golang.org/x/mod/semver" 16 "golang.org/x/tools/gopls/internal/cache" 17 "golang.org/x/tools/gopls/internal/file" 18 "golang.org/x/tools/gopls/internal/protocol" 19 "golang.org/x/tools/gopls/internal/settings" 20 "golang.org/x/tools/gopls/internal/vulncheck" 21 "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" 22 "golang.org/x/tools/gopls/internal/vulncheck/osv" 23 "golang.org/x/tools/internal/event" 24 ) 25 26 func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) { 27 var found bool 28 for _, uri := range snapshot.View().ModFiles() { 29 if fh.URI() == uri { 30 found = true 31 break 32 } 33 } 34 35 // We only provide hover information for the view's go.mod files. 36 if !found { 37 return nil, nil 38 } 39 40 ctx, done := event.Start(ctx, "mod.Hover") 41 defer done() 42 43 // Get the position of the cursor. 44 pm, err := snapshot.ParseMod(ctx, fh) 45 if err != nil { 46 return nil, fmt.Errorf("getting modfile handle: %w", err) 47 } 48 offset, err := pm.Mapper.PositionOffset(position) 49 if err != nil { 50 return nil, fmt.Errorf("computing cursor position: %w", err) 51 } 52 53 // If the cursor position is on a module statement 54 if hover, ok := hoverOnModuleStatement(ctx, pm, offset, snapshot, fh); ok { 55 return hover, nil 56 } 57 return hoverOnRequireStatement(ctx, pm, offset, snapshot, fh) 58 } 59 60 func hoverOnRequireStatement(ctx context.Context, pm *cache.ParsedModule, offset int, snapshot *cache.Snapshot, fh file.Handle) (*protocol.Hover, error) { 61 // Confirm that the cursor is at the position of a require statement. 62 var req *modfile.Require 63 var startOffset, endOffset int 64 for _, r := range pm.File.Require { 65 dep := []byte(r.Mod.Path) 66 s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte 67 i := bytes.Index(pm.Mapper.Content[s:e], dep) 68 if i == -1 { 69 continue 70 } 71 // Shift the start position to the location of the 72 // dependency within the require statement. 73 startOffset, endOffset = s+i, e 74 if startOffset <= offset && offset <= endOffset { 75 req = r 76 break 77 } 78 } 79 // TODO(hyangah): find position for info about vulnerabilities in Go 80 81 // The cursor position is not on a require statement. 82 if req == nil { 83 return nil, nil 84 } 85 86 // Get the vulnerability info. 87 fromGovulncheck := true 88 vs := snapshot.Vulnerabilities(fh.URI())[fh.URI()] 89 if vs == nil && snapshot.Options().Vulncheck == settings.ModeVulncheckImports { 90 var err error 91 vs, err = snapshot.ModVuln(ctx, fh.URI()) 92 if err != nil { 93 return nil, err 94 } 95 fromGovulncheck = false 96 } 97 affecting, nonaffecting, osvs := lookupVulns(vs, req.Mod.Path, req.Mod.Version) 98 99 // Get the `go mod why` results for the given file. 100 why, err := snapshot.ModWhy(ctx, fh) 101 if err != nil { 102 return nil, err 103 } 104 explanation, ok := why[req.Mod.Path] 105 if !ok { 106 return nil, nil 107 } 108 109 // Get the range to highlight for the hover. 110 // TODO(hyangah): adjust the hover range to include the version number 111 // to match the diagnostics' range. 112 rng, err := pm.Mapper.OffsetRange(startOffset, endOffset) 113 if err != nil { 114 return nil, err 115 } 116 options := snapshot.Options() 117 isPrivate := snapshot.IsGoPrivatePath(req.Mod.Path) 118 header := formatHeader(req.Mod.Path, options) 119 explanation = formatExplanation(explanation, req, options, isPrivate) 120 vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck) 121 122 return &protocol.Hover{ 123 Contents: protocol.MarkupContent{ 124 Kind: options.PreferredContentFormat, 125 Value: header + vulns + explanation, 126 }, 127 Range: rng, 128 }, nil 129 } 130 131 func hoverOnModuleStatement(ctx context.Context, pm *cache.ParsedModule, offset int, snapshot *cache.Snapshot, fh file.Handle) (*protocol.Hover, bool) { 132 module := pm.File.Module 133 if module == nil { 134 return nil, false // no module stmt 135 } 136 if offset < module.Syntax.Start.Byte || offset > module.Syntax.End.Byte { 137 return nil, false // cursor not in module stmt 138 } 139 140 rng, err := pm.Mapper.OffsetRange(module.Syntax.Start.Byte, module.Syntax.End.Byte) 141 if err != nil { 142 return nil, false 143 } 144 fromGovulncheck := true 145 vs := snapshot.Vulnerabilities(fh.URI())[fh.URI()] 146 147 if vs == nil && snapshot.Options().Vulncheck == settings.ModeVulncheckImports { 148 vs, err = snapshot.ModVuln(ctx, fh.URI()) 149 if err != nil { 150 return nil, false 151 } 152 fromGovulncheck = false 153 } 154 modpath := "stdlib" 155 goVersion := snapshot.View().GoVersionString() 156 affecting, nonaffecting, osvs := lookupVulns(vs, modpath, goVersion) 157 options := snapshot.Options() 158 vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck) 159 160 return &protocol.Hover{ 161 Contents: protocol.MarkupContent{ 162 Kind: options.PreferredContentFormat, 163 Value: vulns, 164 }, 165 Range: rng, 166 }, true 167 } 168 169 func formatHeader(modpath string, options *settings.Options) string { 170 var b strings.Builder 171 // Write the heading as an H3. 172 b.WriteString("#### " + modpath) 173 if options.PreferredContentFormat == protocol.Markdown { 174 b.WriteString("\n\n") 175 } else { 176 b.WriteRune('\n') 177 } 178 return b.String() 179 } 180 181 func lookupVulns(vulns *vulncheck.Result, modpath, version string) (affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry) { 182 if vulns == nil || len(vulns.Entries) == 0 { 183 return nil, nil, nil 184 } 185 for _, finding := range vulns.Findings { 186 vuln, typ := foundVuln(finding) 187 if vuln.Module != modpath { 188 continue 189 } 190 // It is possible that the source code was changed since the last 191 // govulncheck run and information in the `vulns` info is stale. 192 // For example, imagine that a user is in the middle of updating 193 // problematic modules detected by the govulncheck run by applying 194 // quick fixes. Stale diagnostics can be confusing and prevent the 195 // user from quickly locating the next module to fix. 196 // Ideally we should rerun the analysis with the updated module 197 // dependencies or any other code changes, but we are not yet 198 // in the position of automatically triggering the analysis 199 // (govulncheck can take a while). We also don't know exactly what 200 // part of source code was changed since `vulns` was computed. 201 // As a heuristic, we assume that a user upgrades the affecting 202 // module to the version with the fix or the latest one, and if the 203 // version in the require statement is equal to or higher than the 204 // fixed version, skip the vulnerability information in the hover. 205 // Eventually, the user has to rerun govulncheck. 206 if finding.FixedVersion != "" && semver.IsValid(version) && semver.Compare(finding.FixedVersion, version) <= 0 { 207 continue 208 } 209 switch typ { 210 case vulnCalled: 211 affecting = append(affecting, finding) 212 case vulnImported: 213 nonaffecting = append(nonaffecting, finding) 214 } 215 } 216 217 // Remove affecting elements from nonaffecting. 218 // An OSV entry can appear in both lists if an OSV entry covers 219 // multiple packages imported but not all vulnerable symbols are used. 220 // The current wording of hover message doesn't clearly 221 // present this case well IMO, so let's skip reporting nonaffecting. 222 if len(affecting) > 0 && len(nonaffecting) > 0 { 223 affectingSet := map[string]bool{} 224 for _, f := range affecting { 225 affectingSet[f.OSV] = true 226 } 227 n := 0 228 for _, v := range nonaffecting { 229 if !affectingSet[v.OSV] { 230 nonaffecting[n] = v 231 n++ 232 } 233 } 234 nonaffecting = nonaffecting[:n] 235 } 236 sort.Slice(nonaffecting, func(i, j int) bool { return nonaffecting[i].OSV < nonaffecting[j].OSV }) 237 sort.Slice(affecting, func(i, j int) bool { return affecting[i].OSV < affecting[j].OSV }) 238 return affecting, nonaffecting, vulns.Entries 239 } 240 241 func fixedVersion(fixed string) string { 242 if fixed == "" { 243 return "No fix is available." 244 } 245 return "Fixed in " + fixed + "." 246 } 247 248 func formatVulnerabilities(affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry, options *settings.Options, fromGovulncheck bool) string { 249 if len(osvs) == 0 || (len(affecting) == 0 && len(nonaffecting) == 0) { 250 return "" 251 } 252 byOSV := func(findings []*govulncheck.Finding) map[string][]*govulncheck.Finding { 253 m := make(map[string][]*govulncheck.Finding) 254 for _, f := range findings { 255 m[f.OSV] = append(m[f.OSV], f) 256 } 257 return m 258 } 259 affectingByOSV := byOSV(affecting) 260 nonaffectingByOSV := byOSV(nonaffecting) 261 262 // TODO(hyangah): can we use go templates to generate hover messages? 263 // Then, we can use a different template for markdown case. 264 useMarkdown := options.PreferredContentFormat == protocol.Markdown 265 266 var b strings.Builder 267 268 if len(affectingByOSV) > 0 { 269 // TODO(hyangah): make the message more eyecatching (icon/codicon/color) 270 if len(affectingByOSV) == 1 { 271 fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerability.\n", len(affectingByOSV)) 272 } else { 273 fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerabilities.\n", len(affectingByOSV)) 274 } 275 } 276 for id, findings := range affectingByOSV { 277 fix := fixedVersion(findings[0].FixedVersion) 278 pkgs := vulnerablePkgsInfo(findings, useMarkdown) 279 osvEntry := osvs[id] 280 281 if useMarkdown { 282 fmt.Fprintf(&b, "- [**%v**](%v) %v%v\n%v\n", id, href(id), osvEntry.Summary, pkgs, fix) 283 } else { 284 fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", id, osvEntry.Summary, href(id), pkgs, fix) 285 } 286 } 287 if len(nonaffecting) > 0 { 288 if fromGovulncheck { 289 fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") 290 } else { 291 fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities. Use `govulncheck` to check if the project uses vulnerable symbols.\n") 292 } 293 } 294 for k, findings := range nonaffectingByOSV { 295 fix := fixedVersion(findings[0].FixedVersion) 296 pkgs := vulnerablePkgsInfo(findings, useMarkdown) 297 osvEntry := osvs[k] 298 299 if useMarkdown { 300 fmt.Fprintf(&b, "- [%v](%v) %v%v\n%v\n", k, href(k), osvEntry.Summary, pkgs, fix) 301 } else { 302 fmt.Fprintf(&b, " - [%v] %v (%v) %v\n%v\n", k, osvEntry.Summary, href(k), pkgs, fix) 303 } 304 } 305 b.WriteString("\n") 306 return b.String() 307 } 308 309 func vulnerablePkgsInfo(findings []*govulncheck.Finding, useMarkdown bool) string { 310 var b strings.Builder 311 seen := map[string]bool{} 312 for _, f := range findings { 313 p := f.Trace[0].Package 314 if !seen[p] { 315 seen[p] = true 316 if useMarkdown { 317 b.WriteString("\n * `") 318 } else { 319 b.WriteString("\n ") 320 } 321 b.WriteString(p) 322 if useMarkdown { 323 b.WriteString("`") 324 } 325 } 326 } 327 return b.String() 328 } 329 330 func formatExplanation(text string, req *modfile.Require, options *settings.Options, isPrivate bool) string { 331 text = strings.TrimSuffix(text, "\n") 332 splt := strings.Split(text, "\n") 333 length := len(splt) 334 335 var b strings.Builder 336 337 // If the explanation is 2 lines, then it is of the form: 338 // # golang.org/x/text/encoding 339 // (main module does not need package golang.org/x/text/encoding) 340 if length == 2 { 341 b.WriteString(splt[1]) 342 return b.String() 343 } 344 345 imp := splt[length-1] // import path 346 reference := imp 347 // See golang/go#36998: don't link to modules matching GOPRIVATE. 348 if !isPrivate && options.PreferredContentFormat == protocol.Markdown { 349 target := imp 350 if strings.ToLower(options.LinkTarget) == "pkg.go.dev" { 351 target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1) 352 } 353 reference = fmt.Sprintf("[%s](%s)", imp, cache.BuildLink(options.LinkTarget, target, "")) 354 } 355 b.WriteString("This module is necessary because " + reference + " is imported in") 356 357 // If the explanation is 3 lines, then it is of the form: 358 // # golang.org/x/tools 359 // modtest 360 // golang.org/x/tools/go/packages 361 if length == 3 { 362 msg := fmt.Sprintf(" `%s`.", splt[1]) 363 b.WriteString(msg) 364 return b.String() 365 } 366 367 // If the explanation is more than 3 lines, then it is of the form: 368 // # golang.org/x/text/language 369 // rsc.io/quote 370 // rsc.io/sampler 371 // golang.org/x/text/language 372 b.WriteString(":\n```text") 373 dash := "" 374 for _, imp := range splt[1 : length-1] { 375 dash += "-" 376 b.WriteString("\n" + dash + " " + imp) 377 } 378 b.WriteString("\n```") 379 return b.String() 380 }