gopls/internal/lsp/regtest: move @implementation to new framework

This change is a simple migration of the old @implementation tests
into the new marker framework. The tests themselves are logically
unchanged.

Change-Id: I431e13814612180a5500dba18b26ce8c66dbbe8b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/479295
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go
index dac7407..75e91d2 100644
--- a/gopls/internal/lsp/lsp_test.go
+++ b/gopls/internal/lsp/lsp_test.go
@@ -25,7 +25,6 @@
 	"golang.org/x/tools/gopls/internal/lsp/tests/compare"
 	"golang.org/x/tools/gopls/internal/span"
 	"golang.org/x/tools/internal/bug"
-	"golang.org/x/tools/internal/diff"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/testenv"
 )
@@ -769,35 +768,6 @@
 	}
 }
 
-func (r *runner) Implementation(t *testing.T, spn span.Span, wantSpans []span.Span) {
-	sm, err := r.data.Mapper(spn.URI())
-	if err != nil {
-		t.Fatal(err)
-	}
-	loc, err := sm.SpanLocation(spn)
-	if err != nil {
-		t.Fatal(err)
-	}
-	gotImpls, err := r.server.Implementation(r.ctx, &protocol.ImplementationParams{
-		TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc),
-	})
-	if err != nil {
-		t.Fatalf("Server.Implementation(%s): %v", spn, err)
-	}
-	gotLocs, err := tests.LocationsToSpans(r.data, gotImpls)
-	if err != nil {
-		t.Fatal(err)
-	}
-	sanitize := func(s string) string {
-		return strings.ReplaceAll(s, r.data.Config.Dir, "gopls/internal/lsp/testdata")
-	}
-	want := sanitize(tests.SortAndFormatSpans(wantSpans))
-	got := sanitize(tests.SortAndFormatSpans(gotLocs))
-	if got != want {
-		t.Errorf("implementations(%s):\n%s", sanitize(fmt.Sprint(spn)), diff.Unified("want", "got", want, got))
-	}
-}
-
 func (r *runner) Highlight(t *testing.T, src span.Span, spans []span.Span) {
 	m, err := r.data.Mapper(src.URI())
 	if err != nil {
diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go
index 8b4c61e..179d40b 100644
--- a/gopls/internal/lsp/regtest/marker.go
+++ b/gopls/internal/lsp/regtest/marker.go
@@ -34,7 +34,6 @@
 	"golang.org/x/tools/gopls/internal/lsp/source"
 	"golang.org/x/tools/gopls/internal/lsp/tests"
 	"golang.org/x/tools/gopls/internal/lsp/tests/compare"
-	"golang.org/x/tools/internal/diff"
 	"golang.org/x/tools/internal/jsonrpc2"
 	"golang.org/x/tools/internal/jsonrpc2/servertest"
 	"golang.org/x/tools/internal/testenv"
@@ -130,6 +129,10 @@
 //     src location, and checks that the result is the dst location, with hover
 //     content matching "hover.md" in the golden data g.
 //
+//   - implementations(src location, want ...location): makes a
+//     textDocument/implementation query at the src location and
+//     checks that the resulting set of locations matches want.
+//
 //   - loc(name, location): specifies the name for a location in the source. These
 //     locations may be referenced by other markers.
 //
@@ -258,7 +261,6 @@
 //   - FunctionExtractions
 //   - MethodExtractions
 //   - Definitions
-//   - Implementations
 //   - Highlights
 //   - Renames
 //   - PrepareRenames
@@ -428,9 +430,7 @@
 		if i < len(fn.converters) {
 			convert = fn.converters[i]
 		} else if !fn.variadic {
-			mark.errorf("got %d arguments to %s, expect %d",
-				len(mark.note.Args), mark.note.Name, len(fn.converters))
-			return
+			goto arity // too many args
 		}
 
 		// Special handling for the blank identifier: treat it as the zero value.
@@ -447,8 +447,16 @@
 		}
 		args = append(args, reflect.ValueOf(out))
 	}
+	if len(args) < len(fn.converters) {
+		goto arity // too few args
+	}
 
 	fn.fn.Call(args)
+	return
+
+arity:
+	mark.errorf("got %d arguments to %s, want %d",
+		len(mark.note.Args), mark.note.Name, len(fn.converters))
 }
 
 // Supported marker functions.
@@ -459,15 +467,16 @@
 // Marker funcs should not mutate the test environment (e.g. via opening files
 // or applying edits in the editor).
 var markerFuncs = map[string]markerFunc{
-	"complete":     makeMarkerFunc(completeMarker),
-	"def":          makeMarkerFunc(defMarker),
-	"diag":         makeMarkerFunc(diagMarker),
-	"hover":        makeMarkerFunc(hoverMarker),
-	"loc":          makeMarkerFunc(locMarker),
-	"rename":       makeMarkerFunc(renameMarker),
-	"renameerr":    makeMarkerFunc(renameErrMarker),
-	"suggestedfix": makeMarkerFunc(suggestedfixMarker),
-	"refs":         makeMarkerFunc(refsMarker),
+	"complete":       makeMarkerFunc(completeMarker),
+	"def":            makeMarkerFunc(defMarker),
+	"diag":           makeMarkerFunc(diagMarker),
+	"hover":          makeMarkerFunc(hoverMarker),
+	"implementation": makeMarkerFunc(implementationMarker),
+	"loc":            makeMarkerFunc(locMarker),
+	"rename":         makeMarkerFunc(renameMarker),
+	"renameerr":      makeMarkerFunc(renameErrMarker),
+	"suggestedfix":   makeMarkerFunc(suggestedfixMarker),
+	"refs":           makeMarkerFunc(refsMarker),
 }
 
 // markerTest holds all the test data extracted from a test txtar archive.
@@ -1354,37 +1363,17 @@
 // refsMarker implements the @refs marker.
 func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) {
 	refs := func(includeDeclaration bool, want []protocol.Location) error {
-		params := &protocol.ReferenceParams{
+		got, err := mark.run.env.Editor.Server.References(mark.run.env.Ctx, &protocol.ReferenceParams{
 			TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src),
 			Context: protocol.ReferenceContext{
 				IncludeDeclaration: includeDeclaration,
 			},
-		}
-
-		got, err := mark.run.env.Editor.Server.References(mark.run.env.Ctx, params)
+		})
 		if err != nil {
 			return err
 		}
 
-		// Compare the sets of locations.
-		toString := func(locs []protocol.Location) string {
-			// TODO(adonovan): use generic JoinValues(locs, fmtLoc).
-			strs := make([]string, len(locs))
-			for i, loc := range locs {
-				strs[i] = mark.run.fmtLoc(loc)
-			}
-			sort.Strings(strs)
-			return strings.Join(strs, "\n")
-		}
-		gotStr := toString(got)
-		wantStr := toString(want)
-		if gotStr != wantStr {
-			return fmt.Errorf("incorrect references (got %d, want %d) at %s:\n%s",
-				len(got), len(want),
-				mark.run.fmtLoc(src),
-				diff.Unified("want", "got", wantStr, gotStr))
-		}
-		return nil
+		return compareLocations(mark, got, want)
 	}
 
 	for _, includeDeclaration := range []bool{false, true} {
@@ -1401,3 +1390,35 @@
 		}
 	}
 }
+
+// implementationMarker implements the @implementation marker.
+func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) {
+	got, err := mark.run.env.Editor.Server.Implementation(mark.run.env.Ctx, &protocol.ImplementationParams{
+		TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src),
+	})
+	if err != nil {
+		mark.errorf("implementation at %s failed: %v", src, err)
+		return
+	}
+	if err := compareLocations(mark, got, want); err != nil {
+		mark.errorf("implementation: %v", err)
+	}
+}
+
+// compareLocations returns an error message if got and want are not
+// the same set of locations. The marker is used only for fmtLoc.
+func compareLocations(mark marker, got, want []protocol.Location) error {
+	toStrings := func(locs []protocol.Location) []string {
+		strs := make([]string, len(locs))
+		for i, loc := range locs {
+			strs[i] = mark.run.fmtLoc(loc)
+		}
+		sort.Strings(strs)
+		return strs
+	}
+	if diff := cmp.Diff(toStrings(want), toStrings(got)); diff != "" {
+		return fmt.Errorf("incorrect result locations: (got %d, want %d):\n%s",
+			len(got), len(want), diff)
+	}
+	return nil
+}
diff --git a/gopls/internal/lsp/testdata/implementation/implementation.go b/gopls/internal/lsp/testdata/implementation/implementation.go
deleted file mode 100644
index 4c1a22d..0000000
--- a/gopls/internal/lsp/testdata/implementation/implementation.go
+++ /dev/null
@@ -1,37 +0,0 @@
-package implementation
-
-import "golang.org/lsptests/implementation/other"
-
-type ImpP struct{} //@ImpP,implementations("ImpP", Laugher, OtherLaugher)
-
-func (*ImpP) Laugh() { //@mark(LaughP, "Laugh"),implementations("Laugh", Laugh, OtherLaugh)
-}
-
-type ImpS struct{} //@ImpS,implementations("ImpS", Laugher, OtherLaugher)
-
-func (ImpS) Laugh() { //@mark(LaughS, "Laugh"),implementations("Laugh", Laugh, OtherLaugh)
-}
-
-type Laugher interface { //@Laugher,implementations("Laugher", ImpP, OtherImpP, ImpS, OtherImpS, embedsImpP)
-	Laugh() //@Laugh,implementations("Laugh", LaughP, OtherLaughP, LaughS, OtherLaughS)
-}
-
-type Foo struct { //@implementations("Foo", Joker)
-	other.Foo
-}
-
-type Joker interface { //@Joker
-	Joke() //@Joke,implementations("Joke", ImpJoker)
-}
-
-type cryer int //@implementations("cryer", Cryer)
-
-func (cryer) Cry(other.CryType) {} //@mark(CryImpl, "Cry"),implementations("Cry", Cry)
-
-type Empty interface{} //@implementations("Empty")
-
-var _ interface{ Joke() } //@implementations("Joke", ImpJoker)
-
-type embedsImpP struct { //@embedsImpP
-	ImpP //@implementations("ImpP", Laugher, OtherLaugher)
-}
diff --git a/gopls/internal/lsp/testdata/implementation/implementation_generics.go b/gopls/internal/lsp/testdata/implementation/implementation_generics.go
deleted file mode 100644
index 1f02d16..0000000
--- a/gopls/internal/lsp/testdata/implementation/implementation_generics.go
+++ /dev/null
@@ -1,16 +0,0 @@
-//go:build go1.18
-// +build go1.18
-
-package implementation
-
-// -- generics --
-
-type GenIface[T any] interface { //@mark(GenIface, "GenIface"),implementations("GenIface", GC)
-	F(int, string, T) //@mark(GenIfaceF, "F"),implementations("F", GCF)
-}
-
-type GenConc[U any] int //@mark(GenConc, "GenConc"),implementations("GenConc", GI)
-
-func (GenConc[V]) F(int, string, V) {} //@mark(GenConcF, "F"),implementations("F", GIF)
-
-type GenConcString struct{ GenConc[string] } //@mark(GenConcString, "GenConcString"),implementations(GenConcString, GIString)
diff --git a/gopls/internal/lsp/testdata/implementation/other/other.go b/gopls/internal/lsp/testdata/implementation/other/other.go
deleted file mode 100644
index aff825e..0000000
--- a/gopls/internal/lsp/testdata/implementation/other/other.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package other
-
-type ImpP struct{} //@mark(OtherImpP, "ImpP")
-
-func (*ImpP) Laugh() { //@mark(OtherLaughP, "Laugh")
-}
-
-type ImpS struct{} //@mark(OtherImpS, "ImpS")
-
-func (ImpS) Laugh() { //@mark(OtherLaughS, "Laugh")
-}
-
-type ImpI interface { //@mark(OtherLaugher, "ImpI")
-	Laugh() //@mark(OtherLaugh, "Laugh")
-}
-
-type Foo struct { //@implementations("Foo", Joker)
-}
-
-func (Foo) Joke() { //@mark(ImpJoker, "Joke"),implementations("Joke", Joke)
-}
-
-type CryType int
-
-type Cryer interface { //@Cryer
-	Cry(CryType) //@Cry,implementations("Cry", CryImpl)
-}
diff --git a/gopls/internal/lsp/testdata/implementation/other/other_generics.go b/gopls/internal/lsp/testdata/implementation/other/other_generics.go
deleted file mode 100644
index 4b4c29f..0000000
--- a/gopls/internal/lsp/testdata/implementation/other/other_generics.go
+++ /dev/null
@@ -1,16 +0,0 @@
-//go:build go1.18
-// +build go1.18
-
-package other
-
-// -- generics (limited support) --
-
-type GI[T any] interface { //@mark(GI, "GI"),implementations("GI", GenConc)
-	F(int, string, T) //@mark(GIF, "F"),implementations("F", GenConcF)
-}
-
-type GIString GI[string] //@mark(GIString, "GIString"),implementations("GIString", GenConcString)
-
-type GC[U any] int //@mark(GC, "GC"),implementations("GC", GenIface)
-
-func (GC[V]) F(int, string, V) {} //@mark(GCF, "F"),implementations("F", GenIfaceF)
diff --git a/gopls/internal/lsp/testdata/implementation/other/other_test.go b/gopls/internal/lsp/testdata/implementation/other/other_test.go
deleted file mode 100644
index 846e0d5..0000000
--- a/gopls/internal/lsp/testdata/implementation/other/other_test.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package other
-
-import (
-	"testing"
-)
-
-// This exists so the other.test package comes into existence.
-
-func TestOther(t *testing.T) {
-}
diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden
index bce3cf8..cd9abc8 100644
--- a/gopls/internal/lsp/testdata/summary.txt.golden
+++ b/gopls/internal/lsp/testdata/summary.txt.golden
@@ -26,6 +26,5 @@
 WorkspaceSymbolsCount = 20
 SignaturesCount = 33
 LinksCount = 7
-ImplementationsCount = 16
 SelectionRangesCount = 3
 
diff --git a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden
index 79efbca..d5662f0 100644
--- a/gopls/internal/lsp/testdata/summary_go1.18.txt.golden
+++ b/gopls/internal/lsp/testdata/summary_go1.18.txt.golden
@@ -26,6 +26,5 @@
 WorkspaceSymbolsCount = 20
 SignaturesCount = 33
 LinksCount = 7
-ImplementationsCount = 26
 SelectionRangesCount = 3
 
diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go
index 0ac02c8..531240c 100644
--- a/gopls/internal/lsp/tests/tests.go
+++ b/gopls/internal/lsp/tests/tests.go
@@ -82,7 +82,6 @@
 type FunctionExtractions = map[span.Span]span.Span
 type MethodExtractions = map[span.Span]span.Span
 type Definitions = map[span.Span]Definition
-type Implementations = map[span.Span][]span.Span
 type Highlights = map[span.Span][]span.Span
 type Renames = map[span.Span]string
 type PrepareRenames = map[span.Span]*source.PrepareItem
@@ -116,7 +115,6 @@
 	FunctionExtractions      FunctionExtractions
 	MethodExtractions        MethodExtractions
 	Definitions              Definitions
-	Implementations          Implementations
 	Highlights               Highlights
 	Renames                  Renames
 	InlayHints               InlayHints
@@ -164,7 +162,6 @@
 	FunctionExtraction(*testing.T, span.Span, span.Span)
 	MethodExtraction(*testing.T, span.Span, span.Span)
 	Definition(*testing.T, span.Span, Definition)
-	Implementation(*testing.T, span.Span, []span.Span)
 	Highlight(*testing.T, span.Span, []span.Span)
 	InlayHints(*testing.T, span.Span)
 	Rename(*testing.T, span.Span, string)
@@ -313,7 +310,6 @@
 		RankCompletions:          make(RankCompletions),
 		CaseSensitiveCompletions: make(CaseSensitiveCompletions),
 		Definitions:              make(Definitions),
-		Implementations:          make(Implementations),
 		Highlights:               make(Highlights),
 		Renames:                  make(Renames),
 		PrepareRenames:           make(PrepareRenames),
@@ -457,38 +453,37 @@
 
 	// Collect any data that needs to be used by subsequent tests.
 	if err := datum.Exported.Expect(map[string]interface{}{
-		"codelens":        datum.collectCodeLens,
-		"diag":            datum.collectDiagnostics,
-		"item":            datum.collectCompletionItems,
-		"complete":        datum.collectCompletions(CompletionDefault),
-		"unimported":      datum.collectCompletions(CompletionUnimported),
-		"deep":            datum.collectCompletions(CompletionDeep),
-		"fuzzy":           datum.collectCompletions(CompletionFuzzy),
-		"casesensitive":   datum.collectCompletions(CompletionCaseSensitive),
-		"rank":            datum.collectCompletions(CompletionRank),
-		"snippet":         datum.collectCompletionSnippets,
-		"fold":            datum.collectFoldingRanges,
-		"format":          datum.collectFormats,
-		"import":          datum.collectImports,
-		"semantic":        datum.collectSemanticTokens,
-		"godef":           datum.collectDefinitions,
-		"implementations": datum.collectImplementations,
-		"typdef":          datum.collectTypeDefinitions,
-		"hoverdef":        datum.collectHoverDefinitions,
-		"highlight":       datum.collectHighlights,
-		"inlayHint":       datum.collectInlayHints,
-		"rename":          datum.collectRenames,
-		"prepare":         datum.collectPrepareRenames,
-		"symbol":          datum.collectSymbols,
-		"signature":       datum.collectSignatures,
-		"link":            datum.collectLinks,
-		"suggestedfix":    datum.collectSuggestedFixes,
-		"extractfunc":     datum.collectFunctionExtractions,
-		"extractmethod":   datum.collectMethodExtractions,
-		"incomingcalls":   datum.collectIncomingCalls,
-		"outgoingcalls":   datum.collectOutgoingCalls,
-		"addimport":       datum.collectAddImports,
-		"selectionrange":  datum.collectSelectionRanges,
+		"codelens":       datum.collectCodeLens,
+		"diag":           datum.collectDiagnostics,
+		"item":           datum.collectCompletionItems,
+		"complete":       datum.collectCompletions(CompletionDefault),
+		"unimported":     datum.collectCompletions(CompletionUnimported),
+		"deep":           datum.collectCompletions(CompletionDeep),
+		"fuzzy":          datum.collectCompletions(CompletionFuzzy),
+		"casesensitive":  datum.collectCompletions(CompletionCaseSensitive),
+		"rank":           datum.collectCompletions(CompletionRank),
+		"snippet":        datum.collectCompletionSnippets,
+		"fold":           datum.collectFoldingRanges,
+		"format":         datum.collectFormats,
+		"import":         datum.collectImports,
+		"semantic":       datum.collectSemanticTokens,
+		"godef":          datum.collectDefinitions,
+		"typdef":         datum.collectTypeDefinitions,
+		"hoverdef":       datum.collectHoverDefinitions,
+		"highlight":      datum.collectHighlights,
+		"inlayHint":      datum.collectInlayHints,
+		"rename":         datum.collectRenames,
+		"prepare":        datum.collectPrepareRenames,
+		"symbol":         datum.collectSymbols,
+		"signature":      datum.collectSignatures,
+		"link":           datum.collectLinks,
+		"suggestedfix":   datum.collectSuggestedFixes,
+		"extractfunc":    datum.collectFunctionExtractions,
+		"extractmethod":  datum.collectMethodExtractions,
+		"incomingcalls":  datum.collectIncomingCalls,
+		"outgoingcalls":  datum.collectOutgoingCalls,
+		"addimport":      datum.collectAddImports,
+		"selectionrange": datum.collectSelectionRanges,
 	}); err != nil {
 		t.Fatal(err)
 	}
@@ -752,16 +747,6 @@
 		}
 	})
 
-	t.Run("Implementation", func(t *testing.T) {
-		t.Helper()
-		for spn, m := range data.Implementations {
-			t.Run(SpanName(spn), func(t *testing.T) {
-				t.Helper()
-				tests.Implementation(t, spn, m)
-			})
-		}
-	})
-
 	t.Run("Highlight", func(t *testing.T) {
 		t.Helper()
 		for pos, locations := range data.Highlights {
@@ -1010,7 +995,6 @@
 	fmt.Fprintf(buf, "WorkspaceSymbolsCount = %v\n", countWorkspaceSymbols(data.WorkspaceSymbols))
 	fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures))
 	fmt.Fprintf(buf, "LinksCount = %v\n", linksCount)
-	fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations))
 	fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges))
 
 	want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) {
@@ -1217,10 +1201,6 @@
 	data.SelectionRanges = append(data.SelectionRanges, spn)
 }
 
-func (data *Data) collectImplementations(src span.Span, targets []span.Span) {
-	data.Implementations[src] = targets
-}
-
 func (data *Data) collectIncomingCalls(src span.Span, calls []span.Span) {
 	for _, call := range calls {
 		rng := data.mustRange(call)
diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go
index fd65ecb..deadfa8 100644
--- a/gopls/internal/lsp/tests/util.go
+++ b/gopls/internal/lsp/tests/util.go
@@ -518,30 +518,3 @@
 		return source.SymbolCaseInsensitive
 	}
 }
-
-// LocationsToSpans converts protocol location into span form for testing.
-func LocationsToSpans(data *Data, locs []protocol.Location) ([]span.Span, error) {
-	spans := make([]span.Span, len(locs))
-	for i, loc := range locs {
-		m, err := data.Mapper(loc.URI.SpanURI())
-		if err != nil {
-			return nil, err
-		}
-		spn, err := m.LocationSpan(loc)
-		if err != nil {
-			return nil, fmt.Errorf("failed for %v: %w", loc, err)
-		}
-		spans[i] = spn
-	}
-	return spans, nil
-}
-
-// SortAndFormatSpans sorts and formats a list of spans for use in an assertion.
-func SortAndFormatSpans(spans []span.Span) string {
-	span.SortSpans(spans)
-	var buf strings.Builder
-	for _, spn := range spans {
-		fmt.Fprintf(&buf, "%v\n", spn)
-	}
-	return buf.String()
-}
diff --git a/gopls/internal/regtest/marker/testdata/implementation/basic.txt b/gopls/internal/regtest/marker/testdata/implementation/basic.txt
new file mode 100644
index 0000000..3f63a5d
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/implementation/basic.txt
@@ -0,0 +1,73 @@
+Basic test of implementation query.
+
+-- go.mod --
+module example.com
+go 1.12
+
+-- implementation/implementation.go --
+package implementation
+
+import "example.com/other"
+
+type ImpP struct{} //@loc(ImpP, "ImpP"),implementation("ImpP", Laugher, OtherLaugher)
+
+func (*ImpP) Laugh() { //@loc(LaughP, "Laugh"),implementation("Laugh", Laugh, OtherLaugh)
+}
+
+type ImpS struct{} //@loc(ImpS, "ImpS"),implementation("ImpS", Laugher, OtherLaugher)
+
+func (ImpS) Laugh() { //@loc(LaughS, "Laugh"),implementation("Laugh", Laugh, OtherLaugh)
+}
+
+type Laugher interface { //@loc(Laugher, "Laugher"),implementation("Laugher", ImpP, OtherImpP, ImpS, OtherImpS, embedsImpP)
+	Laugh() //@loc(Laugh, "Laugh"),implementation("Laugh", LaughP, OtherLaughP, LaughS, OtherLaughS)
+}
+
+type Foo struct { //@implementation("Foo", Joker)
+	other.Foo
+}
+
+type Joker interface { //@loc(Joker, "Joker")
+	Joke() //@loc(Joke, "Joke"),implementation("Joke", ImpJoker)
+}
+
+type cryer int //@implementation("cryer", Cryer)
+
+func (cryer) Cry(other.CryType) {} //@loc(CryImpl, "Cry"),implementation("Cry", Cry)
+
+type Empty interface{} //@implementation("Empty")
+
+var _ interface{ Joke() } //@implementation("Joke", ImpJoker)
+
+type embedsImpP struct { //@loc(embedsImpP, "embedsImpP")
+	ImpP //@implementation("ImpP", Laugher, OtherLaugher)
+}
+
+-- other/other.go --
+package other
+
+type ImpP struct{} //@loc(OtherImpP, "ImpP")
+
+func (*ImpP) Laugh() { //@loc(OtherLaughP, "Laugh")
+}
+
+type ImpS struct{} //@loc(OtherImpS, "ImpS")
+
+func (ImpS) Laugh() { //@loc(OtherLaughS, "Laugh")
+}
+
+type ImpI interface { //@loc(OtherLaugher, "ImpI")
+	Laugh() //@loc(OtherLaugh, "Laugh")
+}
+
+type Foo struct { //@implementation("Foo", Joker)
+}
+
+func (Foo) Joke() { //@loc(ImpJoker, "Joke"),implementation("Joke", Joke)
+}
+
+type CryType int
+
+type Cryer interface { //@loc(Cryer, "Cryer")
+	Cry(CryType) //@loc(Cry, "Cry"),implementation("Cry", CryImpl)
+}
diff --git a/gopls/internal/regtest/marker/testdata/implementation/generics.txt b/gopls/internal/regtest/marker/testdata/implementation/generics.txt
new file mode 100644
index 0000000..2a9fcb8
--- /dev/null
+++ b/gopls/internal/regtest/marker/testdata/implementation/generics.txt
@@ -0,0 +1,34 @@
+Test of 'implementation' query on generic types.
+
+-- flags --
+-min_go=go1.18
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- implementation/implementation.go --
+package implementation
+
+type GenIface[T any] interface { //@loc(GenIface, "GenIface"),implementation("GenIface", GC)
+	F(int, string, T) //@loc(GenIfaceF, "F"),implementation("F", GCF)
+}
+
+type GenConc[U any] int //@loc(GenConc, "GenConc"),implementation("GenConc", GI)
+
+func (GenConc[V]) F(int, string, V) {} //@loc(GenConcF, "F"),implementation("F", GIF)
+
+type GenConcString struct{ GenConc[string] } //@loc(GenConcString, "GenConcString"),implementation(GenConcString, GIString)
+
+-- other/other.go --
+package other
+
+type GI[T any] interface { //@loc(GI, "GI"),implementation("GI", GenConc)
+	F(int, string, T) //@loc(GIF, "F"),implementation("F", GenConcF)
+}
+
+type GIString GI[string] //@loc(GIString, "GIString"),implementation("GIString", GenConcString)
+
+type GC[U any] int //@loc(GC, "GC"),implementation("GC", GenIface)
+
+func (GC[V]) F(int, string, V) {} //@loc(GCF, "F"),implementation("F", GenIfaceF)