gopls/internal/golang: provide available version info in stdlib hover

Version is only available for the types of Var, Func, Const & Type.

For golang/go#67159

Change-Id: I77f95ccb6027914440ec7a2ea5338318c0f88e60
Reviewed-on: https://go-review.googlesource.com/c/tools/+/594875
Reviewed-by: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go
index 1291f64..4818f81 100644
--- a/gopls/internal/golang/hover.go
+++ b/gopls/internal/golang/hover.go
@@ -40,6 +40,7 @@
 	"golang.org/x/tools/gopls/internal/util/typesutil"
 	"golang.org/x/tools/internal/aliases"
 	"golang.org/x/tools/internal/event"
+	"golang.org/x/tools/internal/stdlib"
 	"golang.org/x/tools/internal/tokeninternal"
 	"golang.org/x/tools/internal/typeparams"
 	"golang.org/x/tools/internal/typesinternal"
@@ -77,6 +78,10 @@
 	// For example, the "Node" part of "pkg.go.dev/go/ast#Node".
 	LinkAnchor string `json:"linkAnchor"`
 
+	// stdVersion is the Go release version at which this symbol became available.
+	// It is nil for non-std library.
+	stdVersion *stdlib.Version
+
 	// New fields go below, and are unexported. The existing
 	// exported fields are underspecified and have already
 	// constrained our movements too much. A detailed JSON
@@ -595,6 +600,11 @@
 		linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1)
 	}
 
+	var version *stdlib.Version
+	if symbol := StdSymbolOf(obj); symbol != nil {
+		version = &symbol.Version
+	}
+
 	return *hoverRange, &hoverJSON{
 		Synopsis:          doc.Synopsis(docText),
 		FullDocumentation: docText,
@@ -606,6 +616,7 @@
 		typeDecl:          typeDecl,
 		methods:           methods,
 		promotedFields:    fields,
+		stdVersion:        version,
 	}, nil
 }
 
@@ -1166,11 +1177,15 @@
 			formatDoc(h, options),
 			maybeMarkdown(h.promotedFields),
 			maybeMarkdown(h.methods),
+			fmt.Sprintf("Added in %v", h.stdVersion),
 			formatLink(h, options, pkgURL),
 		}
 		if h.typeDecl != "" {
 			parts[0] = "" // type: suppress redundant Signature
 		}
+		if h.stdVersion == nil || *h.stdVersion == stdlib.Version(0) {
+			parts[5] = "" // suppress stdlib version if not applicable or initial version 1.0
+		}
 		parts = slices.Remove(parts, "")
 
 		var b strings.Builder
@@ -1191,6 +1206,29 @@
 	}
 }
 
+// StdSymbolOf returns the std lib symbol information of the given obj.
+// It returns nil if the input obj is not an exported standard library symbol.
+func StdSymbolOf(obj types.Object) *stdlib.Symbol {
+	if !obj.Exported() {
+		return nil
+	}
+
+	if isPackageLevel(obj) {
+		// TODO(hxjiang): This is binary searchable.
+		for _, s := range stdlib.PackageSymbols[obj.Pkg().Path()] {
+			if s.Kind == stdlib.Method || s.Kind == stdlib.Field {
+				continue
+			}
+			if s.Name == obj.Name() {
+				return &s
+			}
+		}
+	}
+
+	// TODO(hxjiang): handle exported fields and methods of package level types.
+	return nil
+}
+
 // If pkgURL is non-nil, it should be used to generate doc links.
 func formatLink(h *hoverJSON, options *settings.Options, pkgURL func(path PackagePath, fragment string) protocol.URI) string {
 	if options.LinksInHover == false || h.LinkPath == "" {
diff --git a/gopls/internal/test/integration/misc/hover_test.go b/gopls/internal/test/integration/misc/hover_test.go
index 3526a93..21233d0 100644
--- a/gopls/internal/test/integration/misc/hover_test.go
+++ b/gopls/internal/test/integration/misc/hover_test.go
@@ -660,3 +660,52 @@
 		}
 	})
 }
+
+func TestHoverStdlibWithAvailableVersion(t *testing.T) {
+	const src = `
+-- stdlib.go --
+package stdlib
+
+import "fmt"
+import "context"
+import "crypto"
+
+func _() {
+	var ctx context.Context
+	ctx = context.Background()
+	if ctx.Err(); e == context.Canceled {
+		fmt.Println("Canceled")
+		fmt.Printf("%v", crypto.SHA512_224)
+	}
+	_ := fmt.Appendf(make([]byte, 100), "world, %d", 23)
+}
+`
+
+	testcases := []struct {
+		symbolRE      string // regexp matching symbol to hover over
+		shouldContain bool
+		targetString  string
+	}{
+		{"Println", false, "go1.0"},   // package-level func
+		{"Appendf", true, "go1.19"},   // package-level func
+		{"Background", true, "go1.7"}, // package-level func
+		{"Canceled", true, "go1.7"},   // package-level var
+		{"Context", true, "go1.7"},    // package-level type
+		{"SHA512_224", true, "go1.5"}, // package-level const
+		// TODO(hxjiang): add test for symbol type Method.
+		// TODO(hxjiang): add test for symbol type Field.
+	}
+
+	Run(t, src, func(t *testing.T, env *Env) {
+		env.OpenFile("stdlib.go")
+		for _, tc := range testcases {
+			content, _ := env.Hover(env.RegexpSearch("stdlib.go", tc.symbolRE))
+			if tc.shouldContain && !strings.Contains(content.Value, tc.targetString) {
+				t.Errorf("Hover(%q) should contain string %s", tc.symbolRE, tc.targetString)
+			}
+			if !tc.shouldContain && strings.Contains(content.Value, tc.targetString) {
+				t.Errorf("Hover(%q) should not contain string %s", tc.symbolRE, tc.targetString)
+			}
+		}
+	})
+}