diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go deleted file mode 100644 index 21f219c2688..00000000000 --- a/gopls/internal/lsp/lsp_test.go +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package lsp - -import ( - "context" - "os" - "path/filepath" - "testing" - - "golang.org/x/tools/gopls/internal/bug" - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/debug" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/testenv" -) - -func TestMain(m *testing.M) { - bug.PanicOnBugs = true - testenv.ExitIfSmallMachine() - - os.Exit(m.Run()) -} - -// TestLSP runs the marker tests in files beneath testdata/ using -// implementations of each of the marker operations that make LSP RPCs to a -// gopls server. -func TestLSP(t *testing.T) { - tests.RunTests(t, "testdata", true, testLSP) -} - -func testLSP(t *testing.T, datum *tests.Data) { - ctx := tests.Context(t) - - // Setting a debug instance suppresses logging to stderr, but ensures that we - // still e.g. convert events into runtime/trace/instrumentation. - // - // Previously, we called event.SetExporter(nil), which turns off all - // instrumentation. - ctx = debug.WithInstance(ctx, "", "off") - - session := cache.NewSession(ctx, cache.New(nil)) - options := source.DefaultOptions(tests.DefaultOptions) - options.SetEnvSlice(datum.Config.Env) - folder := &cache.Folder{ - Dir: span.URIFromPath(datum.Config.Dir), - Name: datum.Config.Dir, - Options: options, - } - view, snapshot, release, err := session.NewView(ctx, folder) - if err != nil { - t.Fatal(err) - } - - defer session.RemoveView(view) - - // Only run the -modfile specific tests in module mode with Go 1.14 or above. - datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14 - release() - - // Open all files for performance reasons, because gopls only - // keeps active packages (those with open files) in memory. - // - // In practice clients will only send document-oriented requests for open - // files. - var modifications []source.FileModification - for _, module := range datum.Exported.Modules { - for name := range module.Files { - filename := datum.Exported.File(module.Name, name) - if filepath.Ext(filename) != ".go" { - continue - } - content, err := datum.Exported.FileContents(filename) - if err != nil { - t.Fatal(err) - } - modifications = append(modifications, source.FileModification{ - URI: span.URIFromPath(filename), - Action: source.Open, - Version: -1, - Text: content, - LanguageID: "go", - }) - } - } - for filename, content := range datum.Config.Overlay { - if filepath.Ext(filename) != ".go" { - continue - } - modifications = append(modifications, source.FileModification{ - URI: span.URIFromPath(filename), - Action: source.Open, - Version: -1, - Text: content, - LanguageID: "go", - }) - } - if err := session.ModifyFiles(ctx, modifications); err != nil { - t.Fatal(err) - } - r := &runner{ - data: datum, - ctx: ctx, - editRecv: make(chan map[span.URI][]byte, 1), - } - - r.server = NewServer(session, testClient{runner: r}, options) - tests.Run(t, r, datum) -} - -// runner implements tests.Tests by making LSP RPCs to a gopls server. -type runner struct { - server *Server - data *tests.Data - diagnostics map[span.URI][]*source.Diagnostic - ctx context.Context - editRecv chan map[span.URI][]byte -} - -// testClient stubs any client functions that may be called by LSP functions. -type testClient struct { - protocol.Client - runner *runner -} - -func (c testClient) Close() error { - return nil -} - -// Trivially implement PublishDiagnostics so that we can call -// server.publishReports below to de-dup sent diagnostics. -func (c testClient) PublishDiagnostics(context.Context, *protocol.PublishDiagnosticsParams) error { - return nil -} - -func (c testClient) ShowMessage(context.Context, *protocol.ShowMessageParams) error { - return nil -} - -func (c testClient) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { - res, err := applyTextDocumentEdits(c.runner, params.Edit.DocumentChanges) - if err != nil { - return nil, err - } - c.runner.editRecv <- res - return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil -} - -func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) { - mapper, err := r.data.Mapper(spn.URI()) - if err != nil { - t.Fatal(err) - } - loc, err := mapper.SpanLocation(spn) - if err != nil { - t.Fatalf("failed for %v: %v", spn, err) - } - - params := &protocol.CallHierarchyPrepareParams{ - TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } - - items, err := r.server.PrepareCallHierarchy(r.ctx, params) - if err != nil { - t.Fatal(err) - } - if len(items) == 0 { - t.Fatalf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range) - } - - callLocation := protocol.Location{ - URI: items[0].URI, - Range: items[0].Range, - } - if callLocation != loc { - t.Fatalf("expected server.PrepareCallHierarchy to return identifier at %v but got %v\n", loc, callLocation) - } - - incomingCalls, err := r.server.IncomingCalls(r.ctx, &protocol.CallHierarchyIncomingCallsParams{Item: items[0]}) - if err != nil { - t.Error(err) - } - var incomingCallItems []protocol.CallHierarchyItem - for _, item := range incomingCalls { - incomingCallItems = append(incomingCallItems, item.From) - } - msg := tests.DiffCallHierarchyItems(incomingCallItems, expectedCalls.IncomingCalls) - if msg != "" { - t.Errorf("incoming calls: %s", msg) - } - - outgoingCalls, err := r.server.OutgoingCalls(r.ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: items[0]}) - if err != nil { - t.Error(err) - } - var outgoingCallItems []protocol.CallHierarchyItem - for _, item := range outgoingCalls { - outgoingCallItems = append(outgoingCallItems, item.To) - } - msg = tests.DiffCallHierarchyItems(outgoingCallItems, expectedCalls.OutgoingCalls) - if msg != "" { - t.Errorf("outgoing calls: %s", msg) - } -} - -func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[span.URI][]byte, error) { - res := make(map[span.URI][]byte) - for _, docEdits := range edits { - if docEdits.TextDocumentEdit != nil { - uri := docEdits.TextDocumentEdit.TextDocument.URI.SpanURI() - var m *protocol.Mapper - // If we have already edited this file, we use the edited version (rather than the - // file in its original state) so that we preserve our initial changes. - if content, ok := res[uri]; ok { - m = protocol.NewMapper(uri, content) - } else { - var err error - if m, err = r.data.Mapper(uri); err != nil { - return nil, err - } - } - patched, _, err := source.ApplyProtocolEdits(m, docEdits.TextDocumentEdit.Edits) - if err != nil { - return nil, err - } - res[uri] = patched - } - } - return res, nil -} diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index 215d5713ab5..f0a1f80296b 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -223,6 +223,10 @@ var update = flag.Bool("update", false, "if set, update test data during marker // textDocument/implementation query at the src location and // checks that the resulting set of locations matches want. // +// - incomingcalls(src location, want ...location): makes a +// callHierarchy/incomingCalls query at the src location, and checks that +// the set of call.From locations matches want. +// // - item(label, details, kind): defines a completion item with the provided // fields. This information is not positional, and therefore @item markers // may occur anywhere in the source. Used in conjunction with @complete, @@ -234,6 +238,10 @@ var update = flag.Bool("update", false, "if set, update test data during marker // - loc(name, location): specifies the name for a location in the source. These // locations may be referenced by other markers. // +// - outgoingcalls(src location, want ...location): makes a +// callHierarchy/outgoingCalls query at the src location, and checks that +// the set of call.To locations matches want. +// // - preparerename(src, spn, placeholder): asserts that a textDocument/prepareRename // request at the src location expands to the spn location, with given // placeholder. If placeholder is "", this is treated as a negative @@ -564,6 +572,11 @@ type marker struct { note *expect.Note } +// ctx returns the mark context. +func (m marker) ctx() context.Context { + return m.run.env.Ctx +} + // server returns the LSP server for the marker test run. func (m marker) server() protocol.Server { return m.run.env.Editor.Server @@ -730,7 +743,9 @@ var actionMarkerFuncs = map[string]func(marker){ "highlight": actionMarkerFunc(highlightMarker), "hover": actionMarkerFunc(hoverMarker), "implementation": actionMarkerFunc(implementationMarker), + "incomingcalls": actionMarkerFunc(incomingCallsMarker), "inlayhints": actionMarkerFunc(inlayhintsMarker), + "outgoingcalls": actionMarkerFunc(outgoingCallsMarker), "preparerename": actionMarkerFunc(prepareRenameMarker), "rank": actionMarkerFunc(rankMarker), "rankl": actionMarkerFunc(ranklMarker), @@ -1743,7 +1758,7 @@ func foldingRangeMarker(mark marker, g *Golden) { // formatMarker implements the @format marker. func formatMarker(mark marker, golden *Golden) { - edits, err := mark.server().Formatting(mark.run.env.Ctx, &protocol.DocumentFormattingParams{ + edits, err := mark.server().Formatting(mark.ctx(), &protocol.DocumentFormattingParams{ TextDocument: mark.document(), }) var got []byte @@ -1868,8 +1883,7 @@ func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr } func selectionRangeMarker(mark marker, loc protocol.Location, g *Golden) { - ctx := mark.run.env.Ctx - ranges, err := mark.run.env.Editor.Server.SelectionRange(ctx, &protocol.SelectionRangeParams{ + ranges, err := mark.run.env.Editor.Server.SelectionRange(mark.ctx(), &protocol.SelectionRangeParams{ TextDocument: mark.document(), Positions: []protocol.Position{loc.Range.Start}, }) @@ -2251,7 +2265,7 @@ func codeActionChanges(env *Env, uri protocol.DocumentURI, rng protocol.Range, a // refsMarker implements the @refs marker. func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { refs := func(includeDeclaration bool, want []protocol.Location) error { - got, err := mark.server().References(mark.run.env.Ctx, &protocol.ReferenceParams{ + got, err := mark.server().References(mark.ctx(), &protocol.ReferenceParams{ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), Context: protocol.ReferenceContext{ IncludeDeclaration: includeDeclaration, @@ -2281,7 +2295,7 @@ func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { // implementationMarker implements the @implementation marker. func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) { - got, err := mark.server().Implementation(mark.run.env.Ctx, &protocol.ImplementationParams{ + got, err := mark.server().Implementation(mark.ctx(), &protocol.ImplementationParams{ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), }) if err != nil { @@ -2293,6 +2307,82 @@ func implementationMarker(mark marker, src protocol.Location, want ...protocol.L } } +func itemLocation(item protocol.CallHierarchyItem) protocol.Location { + return protocol.Location{ + URI: item.URI, + Range: item.Range, + } +} + +func incomingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { + getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) { + calls, err := mark.server().IncomingCalls(mark.ctx(), &protocol.CallHierarchyIncomingCallsParams{Item: item}) + if err != nil { + return nil, err + } + var locs []protocol.Location + for _, call := range calls { + locs = append(locs, itemLocation(call.From)) + } + return locs, nil + } + callHierarchy(mark, src, getCalls, want) +} + +func outgoingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { + getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) { + calls, err := mark.server().OutgoingCalls(mark.ctx(), &protocol.CallHierarchyOutgoingCallsParams{Item: item}) + if err != nil { + return nil, err + } + var locs []protocol.Location + for _, call := range calls { + locs = append(locs, itemLocation(call.To)) + } + return locs, nil + } + callHierarchy(mark, src, getCalls, want) +} + +type callHierarchyFunc = func(protocol.CallHierarchyItem) ([]protocol.Location, error) + +func callHierarchy(mark marker, src protocol.Location, getCalls callHierarchyFunc, want []protocol.Location) { + items, err := mark.server().PrepareCallHierarchy(mark.ctx(), &protocol.CallHierarchyPrepareParams{ + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), + }) + if err != nil { + mark.errorf("PrepareCallHierarchy failed: %v", err) + return + } + if nitems := len(items); nitems != 1 { + mark.errorf("PrepareCallHierarchy returned %d items, want exactly 1", nitems) + return + } + if loc := itemLocation(items[0]); loc != src { + mark.errorf("PrepareCallHierarchy found call %v, want %v", loc, src) + return + } + calls, err := getCalls(items[0]) + if err != nil { + mark.errorf("call hierarchy failed: %v", err) + return + } + if calls == nil { + calls = []protocol.Location{} + } + // TODO(rfindley): why aren't call hierarchy results stable? + sortLocs := func(locs []protocol.Location) { + sort.Slice(locs, func(i, j int) bool { + return protocol.CompareLocation(locs[i], locs[j]) < 0 + }) + } + sortLocs(want) + sortLocs(calls) + if d := cmp.Diff(want, calls); d != "" { + mark.errorf("call hierarchy: unexpected results (-want +got):\n%s", d) + } +} + func inlayhintsMarker(mark marker, g *Golden) { hints := mark.run.env.InlayHints(mark.path()) @@ -2326,7 +2416,7 @@ func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder st params := &protocol.PrepareRenameParams{ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), } - got, err := mark.run.env.Editor.Server.PrepareRename(mark.run.env.Ctx, params) + got, err := mark.run.env.Editor.Server.PrepareRename(mark.ctx(), params) if err != nil { mark.run.env.T.Fatal(err) } @@ -2345,7 +2435,7 @@ func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder st // symbolMarker implements the @symbol marker. func symbolMarker(mark marker, golden *Golden) { // Retrieve information about all symbols in this file. - symbols, err := mark.server().DocumentSymbol(mark.run.env.Ctx, &protocol.DocumentSymbolParams{ + symbols, err := mark.server().DocumentSymbol(mark.ctx(), &protocol.DocumentSymbolParams{ TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()}, }) if err != nil { @@ -2434,7 +2524,7 @@ func workspaceSymbolMarker(mark marker, query string, golden *Golden) { Query: query, } - gotSymbols, err := mark.server().Symbol(mark.run.env.Ctx, params) + gotSymbols, err := mark.server().Symbol(mark.ctx(), params) if err != nil { mark.errorf("Symbol(%q) failed: %v", query, err) return diff --git a/gopls/internal/lsp/reset_golden.sh b/gopls/internal/lsp/reset_golden.sh deleted file mode 100755 index ff7f4d08208..00000000000 --- a/gopls/internal/lsp/reset_golden.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# -# Copyright 2022 The Go Authors. All rights reserved. -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file. -# -# Updates the *.golden files ... to match the tests' current behavior. - -set -eu - -GO117BIN="go1.17.9" - -command -v $GO117BIN >/dev/null 2>&1 || { - go install golang.org/dl/$GO117BIN@latest - $GO117BIN download -} - -find ./internal/lsp/testdata -name *.golden ! -name summary*.txt.golden -delete -# Here we intentionally do not run the ./internal/lsp/source tests with -# -golden. Eventually these tests will be deleted, and in the meantime they are -# redundant with the ./internal/lsp tests. -# -# Note: go1.17.9 tests must be run *before* go tests, as by convention the -# golden output should match the output of gopls built with the most recent -# version of Go. If output differs at 1.17, tests must be tolerant of the 1.17 -# output. -$GO117BIN test ./internal/lsp -golden -go test ./internal/lsp -golden -$GO117BIN test ./test -golden -go test ./test -golden diff --git a/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go b/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go deleted file mode 100644 index 252e8054f40..00000000000 --- a/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package callhierarchy - -import "golang.org/lsptests/callhierarchy/outgoing" - -func a() { //@mark(hierarchyA, "a") - D() -} - -func b() { //@mark(hierarchyB, "b") - D() -} - -// C is an exported function -func C() { //@mark(hierarchyC, "C") - D() - D() -} - -// To test hierarchy across function literals -var x = func() { //@mark(hierarchyLiteral, "func"),mark(hierarchyLiteralOut, "x") - D() -} - -// D is exported to test incoming/outgoing calls across packages -func D() { //@mark(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB, hierarchyFoo, hierarchyH, hierarchyI, hierarchyJ, hierarchyK) - e() - x() - F() - outgoing.B() - foo := func() {} //@mark(hierarchyFoo, "foo"),incomingcalls(hierarchyFoo, hierarchyD),outgoingcalls(hierarchyFoo) - foo() - - func() { - g() - }() - - var i Interface = impl{} - i.H() - i.I() - - s := Struct{} - s.J() - s.K() -} - -func e() {} //@mark(hierarchyE, "e") - -// F is an exported function -func F() {} //@mark(hierarchyF, "F") - -func g() {} //@mark(hierarchyG, "g") - -type Interface interface { - H() //@mark(hierarchyH, "H") - I() //@mark(hierarchyI, "I") -} - -type impl struct{} - -func (i impl) H() {} -func (i impl) I() {} - -type Struct struct { - J func() //@mark(hierarchyJ, "J") - K func() //@mark(hierarchyK, "K") -} diff --git a/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go b/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go deleted file mode 100644 index c629aa87929..00000000000 --- a/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package incoming - -import "golang.org/lsptests/callhierarchy" - -// A is exported to test incoming calls across packages -func A() { //@mark(incomingA, "A") - callhierarchy.D() -} diff --git a/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go b/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go deleted file mode 100644 index 74362d419c3..00000000000 --- a/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package outgoing - -// B is exported to test outgoing calls across packages -func B() { //@mark(outgoingB, "B") -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden deleted file mode 100644 index b4e7101c0ef..00000000000 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ /dev/null @@ -1,3 +0,0 @@ --- summary -- -CallHierarchyCount = 2 - diff --git a/gopls/internal/lsp/tests/README.md b/gopls/internal/lsp/tests/README.md deleted file mode 100644 index 07df28815c1..00000000000 --- a/gopls/internal/lsp/tests/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Testing - -LSP has "marker tests" defined in `internal/lsp/testdata`, as well as -traditional tests. - -## Marker tests - -Marker tests have a standard input file, like -`internal/lsp/testdata/foo/bar.go`, and some may have a corresponding golden -file, like `internal/lsp/testdata/foo/bar.go.golden`. The former is the "input" -and the latter is the expected output. - -Each input file contains annotations like -`//@suggestedfix("}", "refactor.rewrite", "Fill anonymous struct")`. These annotations are interpreted by -test runners to perform certain actions. The expected output after those actions -is encoded in the golden file. - -When tests are run, each annotation results in a new subtest, which is encoded -in the golden file with a heading like, - -```bash --- suggestedfix_bar_11_21 -- -// expected contents go here --- suggestedfix_bar_13_20 -- -// expected contents go here -``` - -The format of these headings vary: they are defined by the -[`Golden`](https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/tests#Data.Golden) -function for each annotation. In the case above, the format is: annotation -name, file name, annotation line location, annotation character location. - -So, if `internal/lsp/testdata/foo/bar.go` has three `suggestedfix` annotations, -the golden file should have three headers with `suggestedfix_bar_xx_yy` -headings. - -To see a list of all available annotations, see the exported "expectations" in -[tests.go](https://github.com/golang/tools/blob/299f270db45902e93469b1152fafed034bb3f033/internal/lsp/tests/tests.go#L418-L447). - -To run marker tests, - -```bash -cd /path/to/tools - -# The marker tests are located in "internal/lsp", "internal/lsp/cmd, and -# "internal/lsp/source". -go test ./internal/lsp/... -``` - -There are quite a lot of marker tests, so to run one individually, pass the test -path and heading into a -run argument: - -```bash -cd /path/to/tools -go test ./internal/lsp/... -v -run TestLSP/Modules/SuggestedFix/bar_11_21 -``` - -## Resetting marker tests - -Sometimes, a change is made to lsp that requires a change to multiple golden -files. When this happens, you can run, - -```bash -cd /path/to/tools -./internal/lsp/reset_golden.sh -``` diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go deleted file mode 100644 index bb1695456d0..00000000000 --- a/gopls/internal/lsp/tests/tests.go +++ /dev/null @@ -1,512 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package tests exports functionality to be used across a variety of gopls tests. -package tests - -import ( - "bytes" - "context" - "flag" - "fmt" - "go/ast" - "go/token" - "io" - "os" - "path/filepath" - "sort" - "strings" - "sync" - "testing" - "time" - - "golang.org/x/tools/go/packages" - "golang.org/x/tools/go/packages/packagestest" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/typeparams" - "golang.org/x/tools/txtar" -) - -const ( - overlayFileSuffix = ".overlay" - goldenFileSuffix = ".golden" - inFileSuffix = ".in" - summaryFile = "summary.txt" - - // The module path containing the testdata packages. - // - // Warning: the length of this module path matters, as we have bumped up - // against command-line limitations on windows (golang/go#54800). - testModule = "golang.org/lsptests" -) - -var UpdateGolden = flag.Bool("golden", false, "Update golden files") - -// These type names apparently avoid the need to repeat the -// type in the field name and the make() expression. -type CallHierarchy = map[span.Span]*CallHierarchyResult - -type Data struct { - Config packages.Config - Exported *packagestest.Exported - CallHierarchy CallHierarchy - - fragments map[string]string - dir string - golden map[string]*Golden - mode string - - ModfileFlagAvailable bool - - mappersMu sync.Mutex - mappers map[span.URI]*protocol.Mapper -} - -// The Tests interface abstracts the LSP-based implementation of the marker -// test operators appearing in files beneath ../testdata/. -// -// TODO(adonovan): reduce duplication; see https://github.com/golang/go/issues/54845. -// There is only one implementation (*runner in ../lsp_test.go), so -// we can abolish the interface now. -type Tests interface { - CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) -} - -type Completion struct { - CompletionItems []token.Pos -} - -type CompletionSnippet struct { - CompletionItem token.Pos - PlainSnippet string - PlaceholderSnippet string -} - -type CallHierarchyResult struct { - IncomingCalls, OutgoingCalls []protocol.CallHierarchyItem -} - -type Link struct { - Src span.Span - Target string - NotePosition token.Position -} - -type SuggestedFix struct { - ActionKind, Title string -} - -type Golden struct { - Filename string - Archive *txtar.Archive - Modified bool -} - -func Context(t testing.TB) context.Context { - return context.Background() -} - -func DefaultOptions(o *source.Options) { - o.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{ - source.Go: { - protocol.SourceOrganizeImports: true, - protocol.QuickFix: true, - protocol.RefactorRewrite: true, - protocol.RefactorInline: true, - protocol.RefactorExtract: true, - protocol.SourceFixAll: true, - }, - source.Mod: { - protocol.SourceOrganizeImports: true, - }, - source.Sum: {}, - source.Work: {}, - source.Tmpl: {}, - } - o.InsertTextFormat = protocol.SnippetTextFormat - o.CompletionBudget = time.Minute - o.HierarchicalDocumentSymbolSupport = true - o.SemanticTokens = true - o.InternalOptions.NewDiff = "new" - - // Enable all inlay hints. - if o.Hints == nil { - o.Hints = make(map[string]bool) - } - for name := range source.AllInlayHints { - o.Hints[name] = true - } -} - -func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*testing.T, *Data)) { - t.Helper() - modes := []string{"Modules", "GOPATH"} - if includeMultiModule { - modes = append(modes, "MultiModule") - } - for _, mode := range modes { - t.Run(mode, func(t *testing.T) { - datum := load(t, mode, dataDir) - t.Helper() - f(t, datum) - }) - } -} - -func load(t testing.TB, mode string, dir string) *Data { - datum := &Data{ - CallHierarchy: make(CallHierarchy), - - dir: dir, - fragments: map[string]string{}, - golden: map[string]*Golden{}, - mode: mode, - mappers: map[span.URI]*protocol.Mapper{}, - } - - if !*UpdateGolden { - summary := filepath.Join(filepath.FromSlash(dir), summaryFile+goldenFileSuffix) - if _, err := os.Stat(summary); os.IsNotExist(err) { - t.Fatalf("could not find golden file summary.txt in %#v", dir) - } - archive, err := txtar.ParseFile(summary) - if err != nil { - t.Fatalf("could not read golden file %v/%v: %v", dir, summary, err) - } - datum.golden[summaryFile] = &Golden{ - Filename: summary, - Archive: archive, - } - } - - files := packagestest.MustCopyFileTree(dir) - // Prune test cases that exercise generics. - if !typeparams.Enabled { - for name := range files { - if strings.Contains(name, "_generics") { - delete(files, name) - } - } - } - overlays := map[string][]byte{} - for fragment, operation := range files { - if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment { - delete(files, fragment) - goldFile := filepath.Join(dir, fragment) - archive, err := txtar.ParseFile(goldFile) - if err != nil { - t.Fatalf("could not read golden file %v: %v", fragment, err) - } - datum.golden[trimmed] = &Golden{ - Filename: goldFile, - Archive: archive, - } - } else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment { - delete(files, fragment) - files[trimmed] = operation - } else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 { - delete(files, fragment) - partial := fragment[:index] + fragment[index+len(overlayFileSuffix):] - contents, err := os.ReadFile(filepath.Join(dir, fragment)) - if err != nil { - t.Fatal(err) - } - overlays[partial] = contents - } - } - - modules := []packagestest.Module{ - { - Name: testModule, - Files: files, - Overlay: overlays, - }, - } - switch mode { - case "Modules": - datum.Exported = packagestest.Export(t, packagestest.Modules, modules) - case "GOPATH": - datum.Exported = packagestest.Export(t, packagestest.GOPATH, modules) - case "MultiModule": - files := map[string]interface{}{} - for k, v := range modules[0].Files { - files[filepath.Join("testmodule", k)] = v - } - modules[0].Files = files - - overlays := map[string][]byte{} - for k, v := range modules[0].Overlay { - overlays[filepath.Join("testmodule", k)] = v - } - modules[0].Overlay = overlays - - golden := map[string]*Golden{} - for k, v := range datum.golden { - if k == summaryFile { - golden[k] = v - } else { - golden[filepath.Join("testmodule", k)] = v - } - } - datum.golden = golden - - datum.Exported = packagestest.Export(t, packagestest.Modules, modules) - default: - panic("unknown mode " + mode) - } - - for _, m := range modules { - for fragment := range m.Files { - filename := datum.Exported.File(m.Name, fragment) - datum.fragments[filename] = fragment - } - } - - // Turn off go/packages debug logging. - datum.Exported.Config.Logf = nil - datum.Config.Logf = nil - - // Merge the exported.Config with the view.Config. - datum.Config = *datum.Exported.Config - datum.Config.Fset = token.NewFileSet() - datum.Config.Context = Context(nil) - datum.Config.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) { - panic("ParseFile should not be called") - } - - // Do a first pass to collect special markers for completion and workspace symbols. - if err := datum.Exported.Expect(map[string]interface{}{ - "item": func(name string, r packagestest.Range, _ []string) { - datum.Exported.Mark(name, r) - }, - "symbol": func(name string, r packagestest.Range, _ []string) { - datum.Exported.Mark(name, r) - }, - }); err != nil { - t.Fatal(err) - } - - // Collect any data that needs to be used by subsequent tests. - if err := datum.Exported.Expect(map[string]interface{}{ - "incomingcalls": datum.collectIncomingCalls, - "outgoingcalls": datum.collectOutgoingCalls, - }); err != nil { - t.Fatal(err) - } - - if mode == "MultiModule" { - if err := moveFile(filepath.Join(datum.Config.Dir, "go.mod"), filepath.Join(datum.Config.Dir, "testmodule/go.mod")); err != nil { - t.Fatal(err) - } - } - - return datum -} - -// moveFile moves the file at oldpath to newpath, by renaming if possible -// or copying otherwise. -func moveFile(oldpath, newpath string) (err error) { - renameErr := os.Rename(oldpath, newpath) - if renameErr == nil { - return nil - } - - src, err := os.Open(oldpath) - if err != nil { - return err - } - defer func() { - src.Close() - if err == nil { - err = os.Remove(oldpath) - } - }() - - perm := os.ModePerm - fi, err := src.Stat() - if err == nil { - perm = fi.Mode().Perm() - } - - dst, err := os.OpenFile(newpath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) - if err != nil { - return err - } - - _, err = io.Copy(dst, src) - if closeErr := dst.Close(); err == nil { - err = closeErr - } - return err -} - -func Run(t *testing.T, tests Tests, data *Data) { - t.Helper() - checkData(t, data) - - t.Run("CallHierarchy", func(t *testing.T) { - t.Helper() - for spn, callHierarchyResult := range data.CallHierarchy { - t.Run(SpanName(spn), func(t *testing.T) { - t.Helper() - tests.CallHierarchy(t, spn, callHierarchyResult) - }) - } - }) - - if *UpdateGolden { - for _, golden := range data.golden { - if !golden.Modified { - continue - } - sort.Slice(golden.Archive.Files, func(i, j int) bool { - return golden.Archive.Files[i].Name < golden.Archive.Files[j].Name - }) - if err := os.WriteFile(golden.Filename, txtar.Format(golden.Archive), 0666); err != nil { - t.Fatal(err) - } - } - } -} - -func checkData(t *testing.T, data *Data) { - buf := &bytes.Buffer{} - - fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy)) - - want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { - return buf.Bytes(), nil - })) - got := buf.String() - if want != got { - // These counters change when assertions are added or removed. - // They act as an independent safety net to ensure that the - // tests didn't spuriously pass because they did no work. - t.Errorf("test summary does not match:\n%s\n(Run with -golden to update golden file; also, there may be one per Go version.)", compare.Text(want, got)) - } -} - -func (data *Data) Mapper(uri span.URI) (*protocol.Mapper, error) { - data.mappersMu.Lock() - defer data.mappersMu.Unlock() - - if _, ok := data.mappers[uri]; !ok { - content, err := data.Exported.FileContents(uri.Filename()) - if err != nil { - return nil, err - } - data.mappers[uri] = protocol.NewMapper(uri, content) - } - return data.mappers[uri], nil -} - -func (data *Data) Golden(t *testing.T, tag, target string, update func() ([]byte, error)) []byte { - t.Helper() - fragment, found := data.fragments[target] - if !found { - if filepath.IsAbs(target) { - t.Fatalf("invalid golden file fragment %v", target) - } - fragment = target - } - golden := data.golden[fragment] - if golden == nil { - if !*UpdateGolden { - t.Fatalf("could not find golden file %v: %v", fragment, tag) - } - golden = &Golden{ - Filename: filepath.Join(data.dir, fragment+goldenFileSuffix), - Archive: &txtar.Archive{}, - Modified: true, - } - data.golden[fragment] = golden - } - var file *txtar.File - for i := range golden.Archive.Files { - f := &golden.Archive.Files[i] - if f.Name == tag { - file = f - break - } - } - if *UpdateGolden { - if file == nil { - golden.Archive.Files = append(golden.Archive.Files, txtar.File{ - Name: tag, - }) - file = &golden.Archive.Files[len(golden.Archive.Files)-1] - } - contents, err := update() - if err != nil { - t.Fatalf("could not update golden file %v: %v", fragment, err) - } - file.Data = append(contents, '\n') // add trailing \n for txtar - golden.Modified = true - - } - if file == nil { - t.Fatalf("could not find golden contents %v: %v", fragment, tag) - } - if len(file.Data) == 0 { - return file.Data - } - return file.Data[:len(file.Data)-1] // drop the trailing \n -} - -func (data *Data) collectIncomingCalls(src span.Span, calls []span.Span) { - for _, call := range calls { - rng := data.mustRange(call) - // we're only comparing protocol.range - if data.CallHierarchy[src] != nil { - data.CallHierarchy[src].IncomingCalls = append(data.CallHierarchy[src].IncomingCalls, - protocol.CallHierarchyItem{ - URI: protocol.DocumentURI(call.URI()), - Range: rng, - }) - } else { - data.CallHierarchy[src] = &CallHierarchyResult{ - IncomingCalls: []protocol.CallHierarchyItem{ - {URI: protocol.DocumentURI(call.URI()), Range: rng}, - }, - } - } - } -} - -func (data *Data) collectOutgoingCalls(src span.Span, calls []span.Span) { - if data.CallHierarchy[src] == nil { - data.CallHierarchy[src] = &CallHierarchyResult{} - } - for _, call := range calls { - // we're only comparing protocol.range - data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls, - protocol.CallHierarchyItem{ - URI: protocol.DocumentURI(call.URI()), - Range: data.mustRange(call), - }) - } -} - -// mustRange converts spn into a protocol.Range, panicking on any error. -func (data *Data) mustRange(spn span.Span) protocol.Range { - m, err := data.Mapper(spn.URI()) - rng, err := m.SpanRange(spn) - if err != nil { - panic(fmt.Sprintf("converting span %s to range: %v", spn, err)) - } - return rng -} - -func uriName(uri span.URI) string { - return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go")) -} - -// TODO(golang/go#54845): improve the formatting here to match standard -// line:column position formatting. -func SpanName(spn span.Span) string { - return fmt.Sprintf("%v_%v_%v", uriName(spn.URI()), spn.Start().Line(), spn.Start().Column()) -} diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go deleted file mode 100644 index ea0920d2e62..00000000000 --- a/gopls/internal/lsp/tests/util.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package tests - -import ( - "fmt" - - "golang.org/x/tools/gopls/internal/lsp/protocol" -) - -// DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies -func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string { - expected := make(map[protocol.Location]bool) - for _, call := range expectedCalls { - expected[protocol.Location{URI: call.URI, Range: call.Range}] = true - } - - got := make(map[protocol.Location]bool) - for _, call := range gotCalls { - got[protocol.Location{URI: call.URI, Range: call.Range}] = true - } - if len(got) != len(expected) { - return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got)) - } - for spn := range got { - if !expected[spn] { - return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got) - } - } - return "" -} diff --git a/gopls/internal/regtest/marker/testdata/callhierarchy/callhierarchy.txt b/gopls/internal/regtest/marker/testdata/callhierarchy/callhierarchy.txt new file mode 100644 index 00000000000..2621f6709fc --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/callhierarchy/callhierarchy.txt @@ -0,0 +1,94 @@ +This test checks call hierarchy queries. + +-ignore_extra_diags due to the initialization cycle. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/callhierarchy + +-- incoming/incoming.go -- +package incoming + +import "golang.org/lsptests/callhierarchy" + +// A is exported to test incoming calls across packages +func A() { //@loc(incomingA, "A") + callhierarchy.D() +} + +-- outgoing/outgoing.go -- +package outgoing + +// B is exported to test outgoing calls across packages +func B() { //@loc(outgoingB, "B") +} + +-- hierarchy.go -- +package callhierarchy + +import "golang.org/lsptests/callhierarchy/outgoing" + +func a() { //@loc(hierarchyA, "a") + D() +} + +func b() { //@loc(hierarchyB, "b") + D() +} + +// C is an exported function +func C() { //@loc(hierarchyC, "C") + D() + D() +} + +// To test hierarchy across function literals +var x = func() { //@loc(hierarchyLiteral, "func"),loc(hierarchyLiteralOut, "x") + D() +} + +// D is exported to test incoming/outgoing calls across packages +func D() { //@loc(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB, hierarchyFoo, hierarchyH, hierarchyI, hierarchyJ, hierarchyK) + e() + x() + F() + outgoing.B() + foo := func() {} //@loc(hierarchyFoo, "foo"),incomingcalls(hierarchyFoo, hierarchyD),outgoingcalls(hierarchyFoo) + foo() + + func() { + g() + }() + + var i Interface = impl{} + i.H() + i.I() + + s := Struct{} + s.J() + s.K() +} + +func e() {} //@loc(hierarchyE, "e") + +// F is an exported function +func F() {} //@loc(hierarchyF, "F") + +func g() {} //@loc(hierarchyG, "g") + +type Interface interface { + H() //@loc(hierarchyH, "H") + I() //@loc(hierarchyI, "I") +} + +type impl struct{} + +func (i impl) H() {} +func (i impl) I() {} + +type Struct struct { + J func() //@loc(hierarchyJ, "J") + K func() //@loc(hierarchyK, "K") +}