gopls: add diagnostics for both vulns used and not used

Add separate diagnostics with different severity levels in order
to capture both reachable and unreachable vulnerabilites in the
diagnostics.

Change-Id: Ib7fa422d9ef7f84e85a5da30b6cf766618cb583c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/452837
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Suzy Mueller <suzmue@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go
index 7202911..e338520 100644
--- a/gopls/internal/lsp/mod/diagnostics.go
+++ b/gopls/internal/lsp/mod/diagnostics.go
@@ -215,7 +215,7 @@
 		// Map affecting vulns to 'warning' level diagnostics,
 		// others to 'info' level diagnostics.
 		// Fixes will include only the upgrades for warning level diagnostics.
-		var fixes []source.SuggestedFix
+		var warningFixes, infoFixes []source.SuggestedFix
 		var warning, info []string
 		var relatedInfo []source.RelatedInformation
 		for _, mv := range vulns {
@@ -251,52 +251,71 @@
 				if err != nil {
 					return nil, err // TODO: bug report
 				}
-				fixes = append(fixes, source.SuggestedFixFromCommand(cmd, protocol.QuickFix))
+				sf := source.SuggestedFixFromCommand(cmd, protocol.QuickFix)
+				if !vuln.IsCalled() {
+					infoFixes = append(infoFixes, sf)
+				} else {
+					warningFixes = append(warningFixes, sf)
+				}
 			}
 		}
 
 		if len(warning) == 0 && len(info) == 0 {
 			continue
 		}
-		severity := protocol.SeverityInformation
-		if len(warning) > 0 {
-			severity = protocol.SeverityWarning
+		// Add an upgrade for module@latest.
+		// TODO(suzmue): verify if latest is the same as fixedVersion.
+		latest, err := getUpgradeCodeAction(fh, req, "latest")
+		if err != nil {
+			return nil, err // TODO: bug report
 		}
-		if len(fixes) > 0 {
-			// Add an upgrade for module@latest.
-			// TODO(suzmue): verify if latest is the same as fixedVersion.
-			latest, err := getUpgradeCodeAction(fh, req, "latest")
-			if err != nil {
-				return nil, err // TODO: bug report
-			}
-			fixes = append([]source.SuggestedFix{source.SuggestedFixFromCommand(latest, protocol.QuickFix)}, fixes...)
+		sf := source.SuggestedFixFromCommand(latest, protocol.QuickFix)
+		if len(warningFixes) > 0 {
+			warningFixes = append(warningFixes, sf)
+		}
+		if len(infoFixes) > 0 {
+			infoFixes = append(infoFixes, sf)
 		}
 
 		sort.Strings(warning)
 		sort.Strings(info)
 
-		var b strings.Builder
-		switch len(warning) {
-		case 0:
-			if n := len(info); n == 1 {
+		if len(warning) > 0 {
+			var b strings.Builder
+			switch len(warning) {
+			case 1:
+				fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0])
+			default:
+				fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", "))
+			}
+			vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{
+				URI:            fh.URI(),
+				Range:          rng,
+				Severity:       protocol.SeverityWarning,
+				Source:         source.Vulncheck,
+				Message:        b.String(),
+				SuggestedFixes: warningFixes,
+				Related:        relatedInfo,
+			})
+		}
+		if len(info) > 0 {
+			var b strings.Builder
+			switch len(info) {
+			case 1:
 				fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", req.Mod.Path, info[0])
-			} else if n > 1 {
+			default:
 				fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", req.Mod.Path, strings.Join(info, ", "))
 			}
-		case 1:
-			fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", req.Mod.Path, warning[0])
-		default:
-			fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", req.Mod.Path, strings.Join(warning, ", "))
+			vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{
+				URI:            fh.URI(),
+				Range:          rng,
+				Severity:       protocol.SeverityInformation,
+				Source:         source.Vulncheck,
+				Message:        b.String(),
+				SuggestedFixes: infoFixes,
+				Related:        relatedInfo,
+			})
 		}
-		vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{
-			URI:            fh.URI(),
-			Range:          rng,
-			Severity:       severity,
-			Source:         source.Vulncheck,
-			Message:        b.String(),
-			SuggestedFixes: fixes,
-			Related:        relatedInfo,
-		})
 	}
 
 	return vulnDiagnostics, nil
diff --git a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go
index f2a5be5..e50c38f 100644
--- a/gopls/internal/regtest/misc/vuln_test.go
+++ b/gopls/internal/regtest/misc/vuln_test.go
@@ -404,6 +404,18 @@
 						severity: protocol.SeverityWarning,
 						codeActions: []string{
 							"Upgrade to latest",
+							"Upgrade to v1.0.4",
+						},
+						relatedInfo: []vulnRelatedInfo{
+							{"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln1
+							{"x.go", uint32(lineX.Line), "[GO-2022-01]"}, // avuln.VulnData.Vuln2
+						},
+					},
+					{
+						msg:      "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.",
+						severity: protocol.SeverityInformation,
+						codeActions: []string{
+							"Upgrade to latest",
 							"Upgrade to v1.0.6",
 						},
 						relatedInfo: []vulnRelatedInfo{
@@ -600,15 +612,14 @@
 			t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff)
 			continue
 		}
-
-		// Check that useful info is supplemented as hover.
-		if len(want.hover) > 0 {
-			hover, _ := env.Hover("go.mod", pos)
-			for _, part := range want.hover {
-				if !strings.Contains(hover.Value, part) {
-					t.Errorf("hover contents for %q do not match, want %v, got %v\n", w.msg, strings.Join(want.hover, ","), hover.Value)
-					break
-				}
+	}
+	// Check that useful info is supplemented as hover.
+	if len(want.hover) > 0 {
+		hover, _ := env.Hover("go.mod", pos)
+		for _, part := range want.hover {
+			if !strings.Contains(hover.Value, part) {
+				t.Errorf("hover contents for %q do not match, want %v, got %v\n", requireMod, strings.Join(want.hover, ","), hover.Value)
+				break
 			}
 		}
 	}