internal/lsp: fix hover for builtin error method (Error)

Looks like this may never have worked. "Error" does not appear in the
builtin package's scope, so we have to look up "error" and then find
the method. A little convoluted, but it works.

Change-Id: I5fe4e96d5c51a1fdc683e44b9a80e0cbdab85422
Reviewed-on: https://go-review.googlesource.com/c/tools/+/259143
Trust: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Peter Weinberger <pjw@google.com>
diff --git a/gopls/internal/regtest/definition_test.go b/gopls/internal/regtest/definition_test.go
index 3f3e3a2..9d2ffd8 100644
--- a/gopls/internal/regtest/definition_test.go
+++ b/gopls/internal/regtest/definition_test.go
@@ -8,6 +8,8 @@
 	"path"
 	"strings"
 	"testing"
+
+	"golang.org/x/tools/internal/lsp/tests"
 )
 
 const internalDefinition = `
@@ -101,3 +103,29 @@
 		}
 	})
 }
+
+// Test the hover on an error's Error function.
+// This can't be done via the marker tests because Error is a builtin.
+func TestHoverOnError(t *testing.T) {
+	const mod = `
+-- go.mod --
+module mod.com
+-- main.go --
+package main
+
+func main() {
+	var err error
+	err.Error()
+}`
+	run(t, mod, func(t *testing.T, env *Env) {
+		env.OpenFile("main.go")
+		content, _ := env.Hover("main.go", env.RegexpSearch("main.go", "Error"))
+		if content == nil {
+			t.Fatalf("nil hover content for Error")
+		}
+		want := "```go\nfunc (error).Error() string\n```"
+		if content.Value != want {
+			t.Fatalf("hover failed:\n%s", tests.Diff(want, content.Value))
+		}
+	})
+}
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index 88eecc1..753d12b 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -204,14 +204,55 @@
 		}
 		result.Declaration.node = decl
 
-		// The builtin package isn't in the dependency graph, so the usual utilities
-		// won't work here.
+		// The builtin package isn't in the dependency graph, so the usual
+		// utilities won't work here.
 		rng := NewMappedRange(snapshot.FileSet(), builtin.ParsedFile.Mapper, decl.Pos(), decl.Pos()+token.Pos(len(result.Name)))
 		result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng)
-
 		return result, nil
 	}
 
+	// (error).Error is a special case of builtin. Lots of checks to confirm
+	// that this is the builtin Error.
+	if obj := result.Declaration.obj; obj.Parent() == nil && obj.Pkg() == nil && obj.Name() == "Error" {
+		if _, ok := obj.Type().(*types.Signature); ok {
+			builtin, err := snapshot.BuiltinPackage(ctx)
+			if err != nil {
+				return nil, err
+			}
+			// Look up "error" and then navigate to its only method.
+			// The Error method does not appear in the builtin package's scope.log.Pri
+			const errorName = "error"
+			builtinObj := builtin.Package.Scope.Lookup(errorName)
+			if builtinObj == nil {
+				return nil, fmt.Errorf("no builtin object for %s", errorName)
+			}
+			decl, ok := builtinObj.Decl.(ast.Node)
+			if !ok {
+				return nil, errors.Errorf("no declaration for %s", errorName)
+			}
+			spec, ok := decl.(*ast.TypeSpec)
+			if !ok {
+				return nil, fmt.Errorf("no type spec for %s", errorName)
+			}
+			iface, ok := spec.Type.(*ast.InterfaceType)
+			if !ok {
+				return nil, fmt.Errorf("%s is not an interface", errorName)
+			}
+			if iface.Methods.NumFields() != 1 {
+				return nil, fmt.Errorf("expected 1 method for %s, got %v", errorName, iface.Methods.NumFields())
+			}
+			method := iface.Methods.List[0]
+			if len(method.Names) != 1 {
+				return nil, fmt.Errorf("expected 1 name for %v, got %v", method, len(method.Names))
+			}
+			name := method.Names[0].Name
+			result.Declaration.node = method
+			rng := NewMappedRange(snapshot.FileSet(), builtin.ParsedFile.Mapper, method.Pos(), method.Pos()+token.Pos(len(name)))
+			result.Declaration.MappedRange = append(result.Declaration.MappedRange, rng)
+			return result, nil
+		}
+	}
+
 	if wasEmbeddedField {
 		// The original position was on the embedded field declaration, so we
 		// try to dig out the type and jump to that instead.