From 4bcf6a3b5685a89572ed19def97a3d89c722a654 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Sep 2024 14:43:22 +0000 Subject: [PATCH 001/102] internal/golang: add a fast path for FormatVarType with gopls at 1.23 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that gopls is only built with Go 1.23, we can rely on types.TypeString to preserve alias information, and only need the bespoke logic of FormatVarType to handle the cases where types contain an invalid type. This should improve performance for completion, particularly since it affects the single-threaded construction of candidates. For example, here is the impact on a relevant benchmark: Completion/kubernetes_selector/edit=false/unimported=false/budget=100ms Results: │ before.txt │ after.txt │ │ sec/op │ sec/op vs base │ 81.29m ± 1% 65.83m ± 1% -19.02% (p=0.000 n=10) │ before.txt │ after.txt │ │ cpu_seconds/op │ cpu_seconds/op vs base │ 151.8m ± 15% 101.1m ± 33% -33.36% (p=0.000 n=10) Change-Id: I60d890ca102a97cf6b198621ba82afe7eeab7fb9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/611836 LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/golang/types_format.go | 34 +++++++++++++++---- gopls/internal/test/marker/marker_test.go | 2 +- .../marker/testdata/completion/foobarbaz.txt | 2 +- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/gopls/internal/golang/types_format.go b/gopls/internal/golang/types_format.go index 51584bcb013..41828244e11 100644 --- a/gopls/internal/golang/types_format.go +++ b/gopls/internal/golang/types_format.go @@ -214,6 +214,9 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si if err != nil { return nil, err } + if sig.Variadic() && i == sig.Params().Len()-1 { + typ = strings.Replace(typ, "[]", "...", 1) + } p := typ if el.Name() != "" { p = el.Name() + " " + typ @@ -261,6 +264,10 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si }, nil } +// We look for 'invalidTypeString' to determine if we can use the fast path for +// FormatVarType. +var invalidTypeString = types.Typ[types.Invalid].String() + // FormatVarType formats a *types.Var, accounting for type aliases. // To do this, it looks in the AST of the file in which the object is declared. // On any errors, it always falls back to types.TypeString. @@ -268,6 +275,21 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si // TODO(rfindley): this function could return the actual name used in syntax, // for better parameter names. func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache.Package, obj *types.Var, qf types.Qualifier, mq MetadataQualifier) (string, error) { + typeString := types.TypeString(obj.Type(), qf) + // Fast path: if the type string does not contain 'invalid type', we no + // longer need to do any special handling, thanks to materialized aliases in + // Go 1.23+. + // + // Unfortunately, due to the handling of invalid types, we can't quite delete + // the rather complicated preexisting logic of FormatVarType--it isn't an + // acceptable regression to start printing "invalid type" in completion or + // signature help. strings.Contains is conservative: the type string of a + // valid type may actually contain "invalid type" (due to struct tags or + // field formatting), but such cases should be exceedingly rare. + if !strings.Contains(typeString, invalidTypeString) { + return typeString, nil + } + // TODO(rfindley): This looks wrong. The previous comment said: // "If the given expr refers to a type parameter, then use the // object's Type instead of the type parameter declaration. This helps @@ -280,13 +302,13 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache. // // Left this during refactoring in order to preserve pre-existing logic. if typeparams.IsTypeParam(obj.Type()) { - return types.TypeString(obj.Type(), qf), nil + return typeString, nil } if isBuiltin(obj) { // This is defensive, though it is extremely unlikely we'll ever have a // builtin var. - return types.TypeString(obj.Type(), qf), nil + return typeString, nil } // TODO(rfindley): parsing to produce candidates can be costly; consider @@ -309,7 +331,7 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache. // for parameterized decls. if decl, _ := decl.(*ast.FuncDecl); decl != nil { if decl.Type.TypeParams.NumFields() > 0 { - return types.TypeString(obj.Type(), qf), nil // in generic function + return typeString, nil // in generic function } if decl.Recv != nil && len(decl.Recv.List) > 0 { rtype := decl.Recv.List[0].Type @@ -317,18 +339,18 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache. rtype = e.X } if x, _, _, _ := typeparams.UnpackIndexExpr(rtype); x != nil { - return types.TypeString(obj.Type(), qf), nil // in method of generic type + return typeString, nil // in method of generic type } } } if spec, _ := spec.(*ast.TypeSpec); spec != nil && spec.TypeParams.NumFields() > 0 { - return types.TypeString(obj.Type(), qf), nil // in generic type decl + return typeString, nil // in generic type decl } if field == nil { // TODO(rfindley): we should never reach here from an ordinary var, so // should probably return an error here. - return types.TypeString(obj.Type(), qf), nil + return typeString, nil } expr := field.Type diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index d3a7685b4dd..f5f4ea09b79 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -1412,7 +1412,7 @@ func snippetMarker(mark marker, src protocol.Location, label completionLabel, wa return } if got != want { - mark.errorf("snippets do not match: got %q, want %q", got, want) + mark.errorf("snippets do not match: got:\n%q\nwant:\n%q", got, want) } } diff --git a/gopls/internal/test/marker/testdata/completion/foobarbaz.txt b/gopls/internal/test/marker/testdata/completion/foobarbaz.txt index 24ac7171055..1da0a405404 100644 --- a/gopls/internal/test/marker/testdata/completion/foobarbaz.txt +++ b/gopls/internal/test/marker/testdata/completion/foobarbaz.txt @@ -476,7 +476,7 @@ func _() { const two = 2 var builtinTypes func([]int, [two]bool, map[string]string, struct{ i int }, interface{ foo() }, <-chan int) - builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [two]bool, m map[string]string, s struct{ i int \\}, i2 interface{ foo() \\}, c <-chan int) {$0\\}") + builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [2]bool, m map[string]string, s struct{i int\\}, i2 interface{foo()\\}, c <-chan int) {$0\\}") var _ func(ast.Node) = f //@snippet(" //", litFunc, "func(n ast.Node) {$0\\}") var _ func(error) = f //@snippet(" //", litFunc, "func(err error) {$0\\}") From 9d7d14e4699bbe94bc3bd918e16d337725dcd00f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 9 Sep 2024 12:29:51 -0400 Subject: [PATCH 002/102] x/tools/gopls: delete code obsoleted by go1.23 The util/slices package has been deleted; but util/maps has been renamed to moremaps since it still has some useful things. Note: the standard maps.Clone may return nil. Updates golang/go#65917 Change-Id: Ide8cbb9aa13d80a35cdab258912a6e18a7db97c6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/611837 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/doc/generate/generate.go | 15 +-- .../simplifycompositelit.go | 3 +- .../analysis/simplifyrange/simplifyrange.go | 3 +- .../simplifyrange/simplifyrange_test.go | 2 +- .../analysis/simplifyslice/simplifyslice.go | 3 +- .../analysis/unusedparams/unusedparams.go | 4 +- gopls/internal/cache/analysis.go | 10 +- gopls/internal/cache/check.go | 2 +- gopls/internal/cache/load.go | 2 +- gopls/internal/cache/session.go | 2 +- gopls/internal/cache/snapshot.go | 2 +- gopls/internal/cache/view.go | 8 +- gopls/internal/cache/xrefs/xrefs.go | 5 +- gopls/internal/cmd/check.go | 2 +- gopls/internal/cmd/codeaction.go | 2 +- gopls/internal/cmd/execute.go | 2 +- gopls/internal/fuzzy/symbol_test.go | 3 +- gopls/internal/golang/codeaction.go | 2 +- .../internal/golang/completion/completion.go | 12 +- gopls/internal/golang/diagnostics.go | 10 +- gopls/internal/golang/extracttofile.go | 7 +- gopls/internal/golang/freesymbols.go | 12 +- gopls/internal/golang/highlight.go | 7 +- gopls/internal/golang/hover.go | 9 +- gopls/internal/golang/lines.go | 2 +- gopls/internal/golang/pkgdoc.go | 36 +++-- gopls/internal/golang/references.go | 6 - gopls/internal/golang/semtok.go | 3 +- gopls/internal/server/code_action.go | 2 +- gopls/internal/server/diagnostics.go | 10 +- gopls/internal/server/general.go | 4 +- gopls/internal/settings/settings.go | 5 +- gopls/internal/telemetry/cmd/stacks/stacks.go | 10 +- .../internal/test/integration/fake/editor.go | 2 +- .../internal/test/integration/fake/workdir.go | 2 +- .../test/integration/misc/codeactions_test.go | 2 +- gopls/internal/test/marker/marker_test.go | 3 +- gopls/internal/util/astutil/util.go | 23 ---- gopls/internal/util/frob/frob.go | 9 +- gopls/internal/util/lru/lru_nil_test.go | 6 - .../internal/util/{maps => moremaps}/maps.go | 32 +++-- gopls/internal/util/moreslices/slices.go | 20 +++ gopls/internal/util/slices/slices.go | 124 ------------------ gopls/internal/util/typesutil/typesutil.go | 15 +-- 44 files changed, 135 insertions(+), 310 deletions(-) rename gopls/internal/util/{maps => moremaps}/maps.go (58%) create mode 100644 gopls/internal/util/moreslices/slices.go delete mode 100644 gopls/internal/util/slices/slices.go diff --git a/gopls/doc/generate/generate.go b/gopls/doc/generate/generate.go index 3fd3e58e6ed..994933a3681 100644 --- a/gopls/doc/generate/generate.go +++ b/gopls/doc/generate/generate.go @@ -23,11 +23,13 @@ import ( "go/ast" "go/token" "go/types" + "maps" "os" "os/exec" "path/filepath" "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -41,7 +43,6 @@ import ( "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/mod" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/maps" "golang.org/x/tools/gopls/internal/util/safetoken" ) @@ -56,14 +57,6 @@ func main() { // - if write, it updates them; // - if !write, it reports whether they would change. func doMain(write bool) (bool, error) { - // TODO(adonovan): when we can rely on go1.23, - // switch to gotypesalias=1 behavior. - // - // (Since this program is run by 'go run', - // the gopls/go.mod file's go 1.19 directive doesn't - // have its usual effect of setting gotypesalias=0.) - os.Setenv("GODEBUG", "gotypesalias=0") - api, err := loadAPI() if err != nil { return false, err @@ -472,9 +465,7 @@ func loadLenses(settingsPkg *packages.Package, defaults map[settings.CodeLensSou // Build list of Lens descriptors. var lenses []*doc.Lens addAll := func(sources map[settings.CodeLensSource]cache.CodeLensSourceFunc, fileType string) error { - slice := maps.Keys(sources) - sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] }) - for _, source := range slice { + for _, source := range slices.Sorted(maps.Keys(sources)) { docText, ok := enumDoc[string(source)] if !ok { return fmt.Errorf("missing CodeLensSource declaration for %s", source) diff --git a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go index 1bdce1d658c..6511477d254 100644 --- a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go +++ b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go @@ -19,7 +19,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/internal/analysisinternal" ) @@ -38,7 +37,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { - if astutil.IsGenerated(file) { + if ast.IsGenerated(file) { generated[pass.Fset.File(file.Pos())] = true } } diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange.go b/gopls/internal/analysis/simplifyrange/simplifyrange.go index ce9d450582b..4071d1b6e8a 100644 --- a/gopls/internal/analysis/simplifyrange/simplifyrange.go +++ b/gopls/internal/analysis/simplifyrange/simplifyrange.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/internal/analysisinternal" ) @@ -33,7 +32,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { - if astutil.IsGenerated(file) { + if ast.IsGenerated(file) { generated[pass.Fset.File(file.Pos())] = true } } diff --git a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go index 973144c30e8..50a600e03bf 100644 --- a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go +++ b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go @@ -6,11 +6,11 @@ package simplifyrange_test import ( "go/build" + "slices" "testing" "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/gopls/internal/analysis/simplifyrange" - "golang.org/x/tools/gopls/internal/util/slices" ) func Test(t *testing.T) { diff --git a/gopls/internal/analysis/simplifyslice/simplifyslice.go b/gopls/internal/analysis/simplifyslice/simplifyslice.go index 343fca8b185..dc99580b07e 100644 --- a/gopls/internal/analysis/simplifyslice/simplifyslice.go +++ b/gopls/internal/analysis/simplifyslice/simplifyslice.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/internal/analysisinternal" ) @@ -42,7 +41,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // Gather information whether file is generated or not generated := make(map[*token.File]bool) for _, file := range pass.Files { - if astutil.IsGenerated(file) { + if ast.IsGenerated(file) { generated[pass.Fset.File(file.Pos())] = true } } diff --git a/gopls/internal/analysis/unusedparams/unusedparams.go b/gopls/internal/analysis/unusedparams/unusedparams.go index df54293b37f..ca808a740d3 100644 --- a/gopls/internal/analysis/unusedparams/unusedparams.go +++ b/gopls/internal/analysis/unusedparams/unusedparams.go @@ -13,7 +13,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/slices" + "golang.org/x/tools/gopls/internal/util/moreslices" "golang.org/x/tools/internal/analysisinternal" ) @@ -194,7 +194,7 @@ func run(pass *analysis.Pass) (any, error) { // Edge case: f = func() {...} // should not count as a use. if pass.TypesInfo.Uses[id] != nil { - usesOutsideCall[fn] = slices.Remove(usesOutsideCall[fn], id) + usesOutsideCall[fn] = moreslices.Remove(usesOutsideCall[fn], id) } if fn == nil && id.Name == "_" { diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index 4730830cb4f..0d7fc46237d 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go @@ -24,6 +24,7 @@ import ( "reflect" "runtime" "runtime/debug" + "slices" "sort" "strings" "sync" @@ -43,8 +44,7 @@ import ( "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/frob" - "golang.org/x/tools/gopls/internal/util/maps" - "golang.org/x/tools/gopls/internal/util/slices" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/facts" @@ -769,11 +769,7 @@ func (an *analysisNode) cacheKey() [sha256.Size]byte { } // vdeps, in PackageID order - depIDs := maps.Keys(an.succs) - // TODO(adonovan): use go1.2x slices.Sort(depIDs). - sort.Slice(depIDs, func(i, j int) bool { return depIDs[i] < depIDs[j] }) - for _, depID := range depIDs { - vdep := an.succs[depID] + for _, vdep := range moremaps.Sorted(an.succs) { fmt.Fprintf(hasher, "dep: %s\n", vdep.mp.PkgPath) fmt.Fprintf(hasher, "export: %s\n", vdep.summary.DeepExportHash) diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index d9c75100443..a0fee068327 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -15,6 +15,7 @@ import ( "go/types" "regexp" "runtime" + "slices" "sort" "strings" "sync" @@ -32,7 +33,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gcimporter" diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go index 36aeddcd9e0..6c176fc85c3 100644 --- a/gopls/internal/cache/load.go +++ b/gopls/internal/cache/load.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" "path/filepath" + "slices" "sort" "strings" "sync/atomic" @@ -23,7 +24,6 @@ import ( "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/immutable" "golang.org/x/tools/gopls/internal/util/pathutil" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/packagesinternal" diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go index 23aebdb078a..c5e9aab98a5 100644 --- a/gopls/internal/cache/session.go +++ b/gopls/internal/cache/session.go @@ -10,6 +10,7 @@ import ( "fmt" "os" "path/filepath" + "slices" "sort" "strconv" "strings" @@ -24,7 +25,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/persistent" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 9014817bdff..c95fa1fdcb5 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -19,6 +19,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "sort" "strconv" "strings" @@ -43,7 +44,6 @@ import ( "golang.org/x/tools/gopls/internal/util/immutable" "golang.org/x/tools/gopls/internal/util/pathutil" "golang.org/x/tools/gopls/internal/util/persistent" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/label" diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go index 7ff3e7b0c8b..8a5a701d890 100644 --- a/gopls/internal/cache/view.go +++ b/gopls/internal/cache/view.go @@ -20,6 +20,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "sort" "strings" "sync" @@ -29,9 +30,8 @@ import ( "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/pathutil" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" @@ -253,7 +253,7 @@ func viewDefinitionsEqual(x, y *viewDefinition) bool { if x.workspaceModFilesErr.Error() != y.workspaceModFilesErr.Error() { return false } - } else if !maps.SameKeys(x.workspaceModFiles, y.workspaceModFiles) { + } else if !moremaps.SameKeys(x.workspaceModFiles, y.workspaceModFiles) { return false } if len(x.envOverlay) != len(y.envOverlay) { @@ -698,7 +698,7 @@ func (s *Snapshot) initialize(ctx context.Context, firstAttempt bool) { extractedDiags := s.extractGoCommandErrors(ctx, loadErr) initialErr = &InitializationError{ MainError: loadErr, - Diagnostics: maps.Group(extractedDiags, byURI), + Diagnostics: moremaps.Group(extractedDiags, byURI), } case s.view.workspaceModFilesErr != nil: initialErr = &InitializationError{ diff --git a/gopls/internal/cache/xrefs/xrefs.go b/gopls/internal/cache/xrefs/xrefs.go index b29b80aebf2..4113e08716e 100644 --- a/gopls/internal/cache/xrefs/xrefs.go +++ b/gopls/internal/cache/xrefs/xrefs.go @@ -18,7 +18,6 @@ import ( "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/frob" - "golang.org/x/tools/gopls/internal/util/typesutil" ) // Index constructs a serializable index of outbound cross-references @@ -93,8 +92,8 @@ func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { case *ast.ImportSpec: // Report a reference from each import path // string to the imported package. - pkgname, ok := typesutil.ImportedPkgName(info, n) - if !ok { + pkgname := info.PkgNameOf(n) + if pkgname == nil { return true // missing import } objects := getObjects(pkgname.Imported()) diff --git a/gopls/internal/cmd/check.go b/gopls/internal/cmd/check.go index a859ed2c708..d256fa9de2a 100644 --- a/gopls/internal/cmd/check.go +++ b/gopls/internal/cmd/check.go @@ -8,10 +8,10 @@ import ( "context" "flag" "fmt" + "slices" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/slices" ) // check implements the check verb for gopls. diff --git a/gopls/internal/cmd/codeaction.go b/gopls/internal/cmd/codeaction.go index cb82e951b41..63a3c999b6f 100644 --- a/gopls/internal/cmd/codeaction.go +++ b/gopls/internal/cmd/codeaction.go @@ -9,10 +9,10 @@ import ( "flag" "fmt" "regexp" + "slices" "strings" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/cmd/execute.go b/gopls/internal/cmd/execute.go index e2b3650a6e6..96b3cf3b81d 100644 --- a/gopls/internal/cmd/execute.go +++ b/gopls/internal/cmd/execute.go @@ -11,11 +11,11 @@ import ( "fmt" "log" "os" + "slices" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/server" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/tool" ) diff --git a/gopls/internal/fuzzy/symbol_test.go b/gopls/internal/fuzzy/symbol_test.go index 7204aa6b9a9..99e2152cef3 100644 --- a/gopls/internal/fuzzy/symbol_test.go +++ b/gopls/internal/fuzzy/symbol_test.go @@ -100,7 +100,8 @@ func TestMatcherSimilarities(t *testing.T) { idents := collectIdentifiers(t) t.Logf("collected %d unique identifiers", len(idents)) - // TODO: use go1.21 slices.MaxFunc. + // We can't use slices.MaxFunc because we want a custom + // scoring (not equivalence) function. topMatch := func(score func(string) float64) string { top := "" topScore := 0.0 diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 31d036bdf40..8f402267393 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -10,6 +10,7 @@ import ( "fmt" "go/ast" "go/types" + "slices" "strings" "golang.org/x/tools/go/ast/astutil" @@ -23,7 +24,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typesinternal" diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 77c8b61615d..88528df6a79 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -18,7 +18,7 @@ import ( "go/token" "go/types" "math" - "reflect" + "slices" "sort" "strconv" "strings" @@ -39,7 +39,6 @@ import ( "golang.org/x/tools/gopls/internal/settings" goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" @@ -198,7 +197,7 @@ type completer struct { // goversion is the version of Go in force in the file, as // defined by x/tools/internal/versions. Empty if unknown. - // TODO(adonovan): with go1.22+ it should always be known. + // Since go1.22 it should always be known. goversion string // (tokFile, pos) is the position at which the request was triggered. @@ -1395,12 +1394,7 @@ func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { return nil } - var goversion string - // TODO(adonovan): after go1.21, replace with: - // goversion = c.pkg.GetTypesInfo().FileVersions[c.file] - if v := reflect.ValueOf(c.pkg.TypesInfo()).Elem().FieldByName("FileVersions"); v.IsValid() { - goversion = v.Interface().(map[*ast.File]string)[c.file] // may be "" - } + goversion := c.pkg.TypesInfo().FileVersions[c.file] // Extract the package-level candidates using a quick parse. var g errgroup.Group diff --git a/gopls/internal/golang/diagnostics.go b/gopls/internal/golang/diagnostics.go index 0dc5ae22aeb..1c6da2e9d4e 100644 --- a/gopls/internal/golang/diagnostics.go +++ b/gopls/internal/golang/diagnostics.go @@ -6,13 +6,15 @@ package golang import ( "context" + "maps" + "slices" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/progress" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" ) // Analyze reports go/analysis-framework diagnostics in the specified package. @@ -28,9 +30,9 @@ func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID return nil, ctx.Err() } - analyzers := maps.Values(settings.DefaultAnalyzers) + analyzers := slices.Collect(maps.Values(settings.DefaultAnalyzers)) if snapshot.Options().Staticcheck { - analyzers = append(analyzers, maps.Values(settings.StaticcheckAnalyzers)...) + analyzers = slices.AppendSeq(analyzers, maps.Values(settings.StaticcheckAnalyzers)) } analysisDiagnostics, err := snapshot.Analyze(ctx, pkgIDs, analyzers, tracker) @@ -38,5 +40,5 @@ func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID return nil, err } byURI := func(d *cache.Diagnostic) protocol.DocumentURI { return d.URI } - return maps.Group(analysisDiagnostics, byURI), nil + return moremaps.Group(analysisDiagnostics, byURI), nil } diff --git a/gopls/internal/golang/extracttofile.go b/gopls/internal/golang/extracttofile.go index 9b3aad5bda3..0a1d74408d7 100644 --- a/gopls/internal/golang/extracttofile.go +++ b/gopls/internal/golang/extracttofile.go @@ -25,7 +25,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/typesutil" ) // canExtractToNewFile reports whether the code in the given range can be extracted to a new file. @@ -56,8 +55,8 @@ func findImportEdits(file *ast.File, info *types.Info, start, end token.Pos) (ad // TODO: support dot imports. return nil, nil, errors.New("\"extract to new file\" does not support files containing dot imports") } - pkgName, ok := typesutil.ImportedPkgName(info, spec) - if !ok { + pkgName := info.PkgNameOf(spec) + if pkgName == nil { continue } usedInSelection := false @@ -152,7 +151,7 @@ func ExtractToNewFile(ctx context.Context, snapshot *cache.Snapshot, fh file.Han return nil, fmt.Errorf("%s: %w", errorPrefix, err) } - fileStart := pgf.Tok.Pos(0) // TODO(adonovan): use go1.20 pgf.File.FileStart + fileStart := pgf.File.FileStart buf.Write(pgf.Src[start-fileStart : end-fileStart]) // TODO: attempt to duplicate the copyright header, if any. diff --git a/gopls/internal/golang/freesymbols.go b/gopls/internal/golang/freesymbols.go index f09975d759a..0e2422d421b 100644 --- a/gopls/internal/golang/freesymbols.go +++ b/gopls/internal/golang/freesymbols.go @@ -13,6 +13,7 @@ import ( "go/token" "go/types" "html" + "slices" "sort" "strings" @@ -20,9 +21,8 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/typesinternal" ) @@ -119,11 +119,7 @@ func FreeSymbolsHTML(viewID string, pkg *cache.Package, pgf *parsego.File, start // Imported symbols. // Produce one record per package, with a list of symbols. - pkgPaths := maps.Keys(imported) - sort.Strings(pkgPaths) - for _, pkgPath := range pkgPaths { - refs := imported[pkgPath] - + for pkgPath, refs := range moremaps.Sorted(imported) { var syms []string for _, ref := range refs { // strip package name (bytes.Buffer.Len -> Buffer.Len) @@ -218,7 +214,7 @@ p { max-width: 6in; } pos := start emitTo := func(end token.Pos) { if pos < end { - fileStart := pgf.Tok.Pos(0) // TODO(adonovan): use go1.20 pgf.File.FileStart + fileStart := pgf.File.FileStart text := pgf.Mapper.Content[pos-fileStart : end-fileStart] buf.WriteString(html.EscapeString(string(text))) pos = end diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index 863c09f7974..5ad0a61d0e1 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/event" ) @@ -85,7 +84,7 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa highlight(imp) // ...and all references to it in the file. - if pkgname, ok := typesutil.ImportedPkgName(info, imp); ok { + if pkgname := info.PkgNameOf(imp); pkgname != nil { ast.Inspect(file, func(n ast.Node) bool { if id, ok := n.(*ast.Ident); ok && info.Uses[id] == pkgname { @@ -586,8 +585,8 @@ func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result highlightIdent(n, protocol.Text) } case *ast.ImportSpec: - pkgname, ok := typesutil.ImportedPkgName(info, n) - if ok && pkgname == obj { + pkgname := info.PkgNameOf(n) + if pkgname == obj { if n.Name != nil { highlightNode(result, n.Name, protocol.Text) } else { diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index b315b7383d4..129adde7fcc 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -36,7 +36,6 @@ import ( gastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" @@ -1186,11 +1185,13 @@ func formatHover(h *hoverJSON, options *settings.Options, pkgURL func(path Packa 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 - for i, part := range parts { - if i > 0 { + for _, part := range parts { + if part == "" { + continue + } + if b.Len() > 0 { if options.PreferredContentFormat == protocol.Markdown { b.WriteString("\n\n") } else { diff --git a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go index 24239941a2c..6a17e928b34 100644 --- a/gopls/internal/golang/lines.go +++ b/gopls/internal/golang/lines.go @@ -13,13 +13,13 @@ import ( "go/ast" "go/token" "go/types" + "slices" "sort" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" ) // canSplitLines checks whether we can split lists of elements inside diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go index 8f6b636027d..38d043b7d74 100644 --- a/gopls/internal/golang/pkgdoc.go +++ b/gopls/internal/golang/pkgdoc.go @@ -39,7 +39,9 @@ import ( "go/token" "go/types" "html" + "iter" "path/filepath" + "slices" "strings" "golang.org/x/tools/go/ast/astutil" @@ -49,8 +51,6 @@ import ( goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" - "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/typesinternal" ) @@ -199,7 +199,7 @@ func thingAtPoint(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) t // In an import spec? if len(path) >= 3 { // [...ImportSpec GenDecl File] if spec, ok := path[len(path)-3].(*ast.ImportSpec); ok { - if pkgname, ok := typesutil.ImportedPkgName(pkg.TypesInfo(), spec); ok { + if pkgname := pkg.TypesInfo().PkgNameOf(spec); pkgname != nil { return thing{pkg: pkgname.Imported()} } } @@ -364,8 +364,8 @@ func PackageDocHTML(viewID string, pkg *cache.Package, web Web) ([]byte, error) // canonical name. for _, f := range pkg.Syntax() { for _, imp := range f.Imports { - pkgName, ok := typesutil.ImportedPkgName(pkg.TypesInfo(), imp) - if ok && pkgName.Name() == name { + pkgName := pkg.TypesInfo().PkgNameOf(imp) + if pkgName != nil && pkgName.Name() == name { return pkgName.Imported().Path(), true } } @@ -693,7 +693,7 @@ window.addEventListener('load', function() { cloneTparams(sig.RecvTypeParams()), cloneTparams(sig.TypeParams()), types.NewTuple(append( - typesSeqToSlice[*types.Var](sig.Params())[:3], + slices.Collect(tupleVariables(sig.Params()))[:3], types.NewVar(0, nil, "", types.Typ[types.Invalid]))...), sig.Results(), false) // any final ...T parameter is truncated @@ -874,18 +874,16 @@ window.addEventListener('load', function() { return buf.Bytes(), nil } -// typesSeq abstracts various go/types sequence types: -// MethodSet, Tuple, TypeParamList, TypeList. -// TODO(adonovan): replace with go1.23 iterators. -type typesSeq[T any] interface { - Len() int - At(int) T -} - -func typesSeqToSlice[T any](seq typesSeq[T]) []T { - slice := make([]T, seq.Len()) - for i := range slice { - slice[i] = seq.At(i) +// tupleVariables returns a go1.23 iterator over the variables of a tuple type. +// +// Example: for v := range tuple.Variables() { ... } +// TODO(adonovan): use t.Variables in go1.24. +func tupleVariables(t *types.Tuple) iter.Seq[*types.Var] { + return func(yield func(v *types.Var) bool) { + for i := range t.Len() { + if !yield(t.At(i)) { + break + } + } } - return slice } diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go index 52d02543a33..b78bd9041d5 100644 --- a/gopls/internal/golang/references.go +++ b/gopls/internal/golang/references.go @@ -653,12 +653,6 @@ func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Objec targets[obj] = leaf } } else { - // Note: prior to go1.21, go/types issue #60372 causes the position - // a field Var T created for struct{*p.T} to be recorded at the - // start of the field type ("*") not the location of the T. - // This affects references and other gopls operations (issue #60369). - // TODO(adonovan): delete this comment when we drop support for go1.20. - // For struct{T}, we prefer the defined field Var over the used TypeName. obj := info.ObjectOf(leaf) if obj == nil { diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index 9fd093fe5fc..0da38bbaa2b 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go @@ -28,7 +28,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol/semtok" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" ) @@ -128,7 +127,7 @@ func (tv *tokenVisitor) visit() { importByName := make(map[string]*types.PkgName) for _, pgf := range tv.pkg.CompiledGoFiles() { for _, imp := range pgf.File.Imports { - if obj, _ := typesutil.ImportedPkgName(tv.pkg.TypesInfo(), imp); obj != nil { + if obj := tv.pkg.TypesInfo().PkgNameOf(imp); obj != nil { if old, ok := importByName[obj.Name()]; ok { if old != nil && old.Imported() != obj.Imported() { importByName[obj.Name()] = nil // nil => ambiguous across files diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go index fe1c885b87f..15f659074b1 100644 --- a/gopls/internal/server/code_action.go +++ b/gopls/internal/server/code_action.go @@ -7,6 +7,7 @@ package server import ( "context" "fmt" + "slices" "sort" "strings" @@ -17,7 +18,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/event" ) diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go index 3770a735ff8..a4466e2fc76 100644 --- a/gopls/internal/server/diagnostics.go +++ b/gopls/internal/server/diagnostics.go @@ -26,7 +26,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/template" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/work" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" @@ -103,7 +103,7 @@ func sortDiagnostics(d []*cache.Diagnostic) { func (s *server) diagnoseChangedViews(ctx context.Context, modID uint64, lastChange map[*cache.View][]protocol.DocumentURI, cause ModificationSource) { // Collect views needing diagnosis. s.modificationMu.Lock() - needsDiagnosis := maps.Keys(s.viewsToDiagnose) + needsDiagnosis := moremaps.KeySlice(s.viewsToDiagnose) s.modificationMu.Unlock() // Diagnose views concurrently. @@ -288,7 +288,7 @@ func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snaps toDiagnose[meta.ID] = meta } } - diags, err := snapshot.PackageDiagnostics(ctx, maps.Keys(toDiagnose)...) + diags, err := snapshot.PackageDiagnostics(ctx, moremaps.KeySlice(toDiagnose)...) if err != nil { if ctx.Err() == nil { event.Error(ctx, "warning: diagnostics failed", err, snapshot.Labels()...) @@ -495,7 +495,7 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa go func() { defer wg.Done() var err error - pkgDiags, err = snapshot.PackageDiagnostics(ctx, maps.Keys(toDiagnose)...) + pkgDiags, err = snapshot.PackageDiagnostics(ctx, moremaps.KeySlice(toDiagnose)...) if err != nil { event.Error(ctx, "warning: diagnostics failed", err, snapshot.Labels()...) } @@ -511,7 +511,7 @@ func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMa // if err is non-nil (though as of today it's OK). analysisDiags, err = golang.Analyze(ctx, snapshot, toAnalyze, s.progress) if err != nil { - event.Error(ctx, "warning: analyzing package", err, append(snapshot.Labels(), label.Package.Of(keys.Join(maps.Keys(toDiagnose))))...) + event.Error(ctx, "warning: analyzing package", err, append(snapshot.Labels(), label.Package.Of(keys.Join(moremaps.KeySlice(toDiagnose))))...) return } }() diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index 08b65b1bc84..3fe6c9e219c 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go @@ -29,7 +29,7 @@ import ( "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/goversion" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" ) @@ -372,7 +372,7 @@ func (s *server) updateWatchedDirectories(ctx context.Context) error { defer s.watchedGlobPatternsMu.Unlock() // Nothing to do if the set of workspace directories is unchanged. - if maps.SameKeys(s.watchedGlobPatterns, patterns) { + if moremaps.SameKeys(s.watchedGlobPatterns, patterns) { return nil } diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 2cd504b2555..2b676f37132 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -6,6 +6,7 @@ package settings import ( "fmt" + "maps" "path/filepath" "runtime" "strings" @@ -14,7 +15,6 @@ import ( "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/frob" - "golang.org/x/tools/gopls/internal/util/maps" ) type Annotation string @@ -1060,6 +1060,9 @@ func (o *Options) setOne(name string, value any) error { if err != nil { return err } + if o.Codelenses == nil { + o.Codelenses = make(map[CodeLensSource]bool) + } o.Codelenses = maps.Clone(o.Codelenses) for source, enabled := range lensOverrides { o.Codelenses[source] = enabled diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go index 3da90f81f4b..e99e58915c9 100644 --- a/gopls/internal/telemetry/cmd/stacks/stacks.go +++ b/gopls/internal/telemetry/cmd/stacks/stacks.go @@ -14,6 +14,7 @@ import ( "flag" "fmt" "hash/fnv" + "io" "log" "net/http" "net/url" @@ -23,11 +24,9 @@ import ( "strings" "time" - "io" - "golang.org/x/telemetry" "golang.org/x/tools/gopls/internal/util/browser" - "golang.org/x/tools/gopls/internal/util/maps" + "golang.org/x/tools/gopls/internal/util/moremaps" ) // flags @@ -79,8 +78,7 @@ func main() { // Read all recent telemetry reports. t := time.Now() for i := 0; i < *daysFlag; i++ { - const DateOnly = "2006-01-02" // TODO(adonovan): use time.DateOnly in go1.20. - date := t.Add(-time.Duration(i+1) * 24 * time.Hour).Format(DateOnly) + date := t.Add(-time.Duration(i+1) * 24 * time.Hour).Format(time.DateOnly) url := fmt.Sprintf("https://storage.googleapis.com/prod-telemetry-merged/%s.json", date) resp, err := http.Get(url) @@ -209,7 +207,7 @@ func main() { } print := func(caption string, issues map[string]int64) { // Print items in descending frequency. - keys := maps.Keys(issues) + keys := moremaps.KeySlice(issues) sort.Slice(keys, func(i, j int) bool { return issues[keys[i]] > issues[keys[j]] }) diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index ae41bd409fa..981abce89e2 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -14,6 +14,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "strings" "sync" @@ -22,7 +23,6 @@ import ( "golang.org/x/tools/gopls/internal/test/integration/fake/glob" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/pathutil" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/xcontext" diff --git a/gopls/internal/test/integration/fake/workdir.go b/gopls/internal/test/integration/fake/workdir.go index 25b3cb5c557..be3cb3bcf15 100644 --- a/gopls/internal/test/integration/fake/workdir.go +++ b/gopls/internal/test/integration/fake/workdir.go @@ -13,13 +13,13 @@ import ( "os" "path/filepath" "runtime" + "slices" "sort" "strings" "sync" "time" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/robustio" ) diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go index b0325d0f872..70091150a87 100644 --- a/gopls/internal/test/integration/misc/codeactions_test.go +++ b/gopls/internal/test/integration/misc/codeactions_test.go @@ -6,13 +6,13 @@ package misc import ( "fmt" + "slices" "testing" "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" . "golang.org/x/tools/gopls/internal/test/integration" - "golang.org/x/tools/gopls/internal/util/slices" ) // This test exercises the filtering of code actions in generated files. diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index f5f4ea09b79..9d22d882f26 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -23,6 +23,7 @@ import ( "reflect" "regexp" "runtime" + "slices" "sort" "strings" "testing" @@ -30,7 +31,6 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "golang.org/x/tools/go/expect" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" @@ -41,7 +41,6 @@ import ( "golang.org/x/tools/gopls/internal/test/integration/fake" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/diff/myers" "golang.org/x/tools/internal/jsonrpc2" diff --git a/gopls/internal/util/astutil/util.go b/gopls/internal/util/astutil/util.go index b9cf9a03c1d..ac7515d1daf 100644 --- a/gopls/internal/util/astutil/util.go +++ b/gopls/internal/util/astutil/util.go @@ -7,7 +7,6 @@ package astutil import ( "go/ast" "go/token" - "strings" "golang.org/x/tools/internal/typeparams" ) @@ -70,25 +69,3 @@ L: // unpack receiver type func NodeContains(n ast.Node, pos token.Pos) bool { return n.Pos() <= pos && pos <= n.End() } - -// IsGenerated check if a file is generated code -func IsGenerated(file *ast.File) bool { - // TODO: replace this implementation with calling function ast.IsGenerated when go1.21 is assured - for _, group := range file.Comments { - for _, comment := range group.List { - if comment.Pos() > file.Package { - break // after package declaration - } - // opt: check Contains first to avoid unnecessary array allocation in Split. - const prefix = "// Code generated " - if strings.Contains(comment.Text, prefix) { - for _, line := range strings.Split(comment.Text, "\n") { - if strings.HasPrefix(line, prefix) && strings.HasSuffix(line, " DO NOT EDIT.") { - return true - } - } - } - } - } - return false -} diff --git a/gopls/internal/util/frob/frob.go b/gopls/internal/util/frob/frob.go index cd385a9d692..a5fa584215f 100644 --- a/gopls/internal/util/frob/frob.go +++ b/gopls/internal/util/frob/frob.go @@ -327,14 +327,9 @@ func (fr *frob) decode(in *reader, addr reflect.Value) { kfrob, vfrob := fr.elems[0], fr.elems[1] k := reflect.New(kfrob.t).Elem() v := reflect.New(vfrob.t).Elem() - kzero := reflect.Zero(kfrob.t) - vzero := reflect.Zero(vfrob.t) for i := 0; i < len; i++ { - // TODO(adonovan): use SetZero from go1.20. - // k.SetZero() - // v.SetZero() - k.Set(kzero) - v.Set(vzero) + k.SetZero() + v.SetZero() kfrob.decode(in, k) vfrob.decode(in, v) m.SetMapIndex(k, v) diff --git a/gopls/internal/util/lru/lru_nil_test.go b/gopls/internal/util/lru/lru_nil_test.go index 08ce910989c..443d2a67818 100644 --- a/gopls/internal/util/lru/lru_nil_test.go +++ b/gopls/internal/util/lru/lru_nil_test.go @@ -4,11 +4,6 @@ package lru_test -// TODO(rfindley): uncomment once -lang is at least go1.20. -// Prior to that language version, interfaces did not satisfy comparable. -// Note that we can't simply use //go:build go1.20, because we need at least Go -// 1.21 in the go.mod file for file language versions support! -/* import ( "testing" @@ -22,4 +17,3 @@ func TestSetUntypedNil(t *testing.T) { t.Errorf("cache.Get(nil) = %v, %v, want nil, true", got, ok) } } -*/ diff --git a/gopls/internal/util/maps/maps.go b/gopls/internal/util/moremaps/maps.go similarity index 58% rename from gopls/internal/util/maps/maps.go rename to gopls/internal/util/moremaps/maps.go index daa9c3dafad..c8484d9fecd 100644 --- a/gopls/internal/util/maps/maps.go +++ b/gopls/internal/util/moremaps/maps.go @@ -2,7 +2,14 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package maps +package moremaps + +import ( + "cmp" + "iter" + "maps" + "slices" +) // Group returns a new non-nil map containing the elements of s grouped by the // keys returned from the key func. @@ -15,8 +22,8 @@ func Group[K comparable, V any](s []V, key func(V) K) map[K][]V { return m } -// Keys returns the keys of the map M. -func Keys[M ~map[K]V, K comparable, V any](m M) []K { +// Keys returns the keys of the map M, like slices.Collect(maps.Keys(m)). +func KeySlice[M ~map[K]V, K comparable, V any](m M) []K { r := make([]K, 0, len(m)) for k := range m { r = append(r, k) @@ -24,8 +31,8 @@ func Keys[M ~map[K]V, K comparable, V any](m M) []K { return r } -// Values returns the values of the map M. -func Values[M ~map[K]V, K comparable, V any](m M) []V { +// Values returns the values of the map M, like slices.Collect(maps.Values(m)). +func ValueSlice[M ~map[K]V, K comparable, V any](m M) []V { r := make([]V, 0, len(m)) for _, v := range m { r = append(r, v) @@ -46,11 +53,14 @@ func SameKeys[K comparable, V1, V2 any](x map[K]V1, y map[K]V2) bool { return true } -// Clone returns a new map with the same entries as m. -func Clone[M ~map[K]V, K comparable, V any](m M) M { - copy := make(map[K]V, len(m)) - for k, v := range m { - copy[k] = v +// Sorted returns an iterator over the entries of m in key order. +func Sorted[M ~map[K]V, K cmp.Ordered, V any](m M) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + keys := slices.Sorted(maps.Keys(m)) + for _, k := range keys { + if !yield(k, m[k]) { + break + } + } } - return copy } diff --git a/gopls/internal/util/moreslices/slices.go b/gopls/internal/util/moreslices/slices.go new file mode 100644 index 00000000000..5905e360bfa --- /dev/null +++ b/gopls/internal/util/moreslices/slices.go @@ -0,0 +1,20 @@ +// Copyright 2024 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 moreslices + +// Remove removes all values equal to elem from slice. +// +// The closest equivalent in the standard slices package is: +// +// DeleteFunc(func(x T) bool { return x == elem }) +func Remove[T comparable](slice []T, elem T) []T { + out := slice[:0] + for _, v := range slice { + if v != elem { + out = append(out, v) + } + } + return out +} diff --git a/gopls/internal/util/slices/slices.go b/gopls/internal/util/slices/slices.go deleted file mode 100644 index add52b7f6b1..00000000000 --- a/gopls/internal/util/slices/slices.go +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2023 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 slices - -// Clone returns a copy of the slice. -// The elements are copied using assignment, so this is a shallow clone. -// TODO(rfindley): use go1.21 slices.Clone. -func Clone[S ~[]E, E any](s S) S { - // The s[:0:0] preserves nil in case it matters. - return append(s[:0:0], s...) -} - -// Contains reports whether x is present in slice. -// TODO(adonovan): use go1.21 slices.Contains. -func Contains[S ~[]E, E comparable](slice S, x E) bool { - for _, elem := range slice { - if elem == x { - return true - } - } - return false -} - -// IndexFunc returns the first index i satisfying f(s[i]), -// or -1 if none do. -// TODO(adonovan): use go1.21 slices.IndexFunc. -func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { - for i := range s { - if f(s[i]) { - return i - } - } - return -1 -} - -// ContainsFunc reports whether at least one -// element e of s satisfies f(e). -// TODO(adonovan): use go1.21 slices.ContainsFunc. -func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool { - return IndexFunc(s, f) >= 0 -} - -// Concat returns a new slice concatenating the passed in slices. -// TODO(rfindley): use go1.22 slices.Concat. -func Concat[S ~[]E, E any](slices ...S) S { - size := 0 - for _, s := range slices { - size += len(s) - if size < 0 { - panic("len out of range") - } - } - newslice := Grow[S](nil, size) - for _, s := range slices { - newslice = append(newslice, s...) - } - return newslice -} - -// Grow increases the slice's capacity, if necessary, to guarantee space for -// another n elements. After Grow(n), at least n elements can be appended -// to the slice without another allocation. If n is negative or too large to -// allocate the memory, Grow panics. -// TODO(rfindley): use go1.21 slices.Grow. -func Grow[S ~[]E, E any](s S, n int) S { - if n < 0 { - panic("cannot be negative") - } - if n -= cap(s) - len(s); n > 0 { - s = append(s[:cap(s)], make([]E, n)...)[:len(s)] - } - return s -} - -// DeleteFunc removes any elements from s for which del returns true, -// returning the modified slice. -// DeleteFunc zeroes the elements between the new length and the original length. -// TODO(adonovan): use go1.21 slices.DeleteFunc. -func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { - i := IndexFunc(s, del) - if i == -1 { - return s - } - // Don't start copying elements until we find one to delete. - for j := i + 1; j < len(s); j++ { - if v := s[j]; !del(v) { - s[i] = v - i++ - } - } - clear(s[i:]) // zero/nil out the obsolete elements, for GC - return s[:i] -} - -func clear[T any](slice []T) { - for i := range slice { - slice[i] = *new(T) - } -} - -// Remove removes all values equal to elem from slice. -// -// The closest equivalent in the standard slices package is: -// -// DeleteFunc(func(x T) bool { return x == elem }) -func Remove[T comparable](slice []T, elem T) []T { - out := slice[:0] - for _, v := range slice { - if v != elem { - out = append(out, v) - } - } - return out -} - -// Reverse reverses the elements of the slice in place. -// TODO(adonovan): use go1.21 slices.Reverse. -func Reverse[S ~[]E, E any](s S) { - for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { - s[i], s[j] = s[j], s[i] - } -} diff --git a/gopls/internal/util/typesutil/typesutil.go b/gopls/internal/util/typesutil/typesutil.go index 3597b4b4bbc..6e61c7ed874 100644 --- a/gopls/internal/util/typesutil/typesutil.go +++ b/gopls/internal/util/typesutil/typesutil.go @@ -9,19 +9,6 @@ import ( "go/types" ) -// ImportedPkgName returns the PkgName object declared by an ImportSpec. -// TODO(adonovan): use go1.22's Info.PkgNameOf. -func ImportedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) { - var obj types.Object - if imp.Name != nil { - obj = info.Defs[imp.Name] - } else { - obj = info.Implicits[imp] - } - pkgname, ok := obj.(*types.PkgName) - return pkgname, ok -} - // FileQualifier returns a [types.Qualifier] function that qualifies // imported symbols appropriately based on the import environment of a // given file. @@ -29,7 +16,7 @@ func FileQualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qual // Construct mapping of import paths to their defined or implicit names. imports := make(map[*types.Package]string) for _, imp := range f.Imports { - if pkgname, ok := ImportedPkgName(info, imp); ok { + if pkgname := info.PkgNameOf(imp); pkgname != nil { imports[pkgname.Imported()] = pkgname.Name() } } From 6b0cfffedff6ec3fb62c84bd37091c11dc8cacf8 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Sep 2024 17:17:28 +0000 Subject: [PATCH 003/102] internal/test/marker: support multi-line locations Address a long-standing TODO to permit multi-line locations in the codeactionedit marker. This should unblock use of codeactionedit with CL 610976. Also use a 'converter' wrapper to add a bit more type safety in argument conversion functions. Change-Id: I851785c567bcde1a8df82a7921c2fba42def9085 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612035 Reviewed-by: Alan Donovan Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/test/marker/doc.go | 30 +++-- gopls/internal/test/marker/marker_test.go | 120 +++++++++--------- .../codeaction/extract-variadic-63287.txt | 6 +- .../testdata/codeaction/extract_method.txt | 20 +-- .../marker/testdata/completion/comment.txt | 8 +- .../test/marker/testdata/definition/embed.txt | 2 +- .../marker/testdata/diagnostics/analyzers.txt | 2 +- 7 files changed, 97 insertions(+), 91 deletions(-) diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go index f81604c913d..e71bd5ba6d2 100644 --- a/gopls/internal/test/marker/doc.go +++ b/gopls/internal/test/marker/doc.go @@ -107,10 +107,10 @@ The following markers are supported within marker tests: If titles are provided, they are used to filter the matching code action. - TODO(rfindley): consolidate with codeactionedit, via a @loc2 marker that - allows binding multi-line locations. + TODO(rfindley): now that 'location' supports multi-line matches, replace + uses of 'codeaction' with codeactionedit. - - codeactionedit(range, kind, golden, ...titles): a shorter form of + - codeactionedit(location, kind, golden, ...titles): a shorter form of codeaction. Invokes a code action of the given kind for the given in-line range, and compares the resulting formatted unified *edits* (notably, not the full file content) with the golden directory. @@ -292,11 +292,15 @@ test function. Additional value conversions may occur for these argument -> parameter type pairs: - string->regexp: the argument is parsed as a regular expressions. - string->location: the argument is converted to the location of the first - instance of the argument in the partial line preceding the note. + instance of the argument in the file content starting from the beginning of + the line containing the note. Multi-line matches are permitted, but the + match must begin before the note. - regexp->location: the argument is converted to the location of the first - match for the argument in the partial line preceding the note. If the - regular expression contains exactly one subgroup, the position of the - subgroup is used rather than the position of the submatch. + match for the argument in the file content starting from the beginning of + the line containing the note. Multi-line matches are permitted, but the + match must begin before the note. If the regular expression contains + exactly one subgroup, the position of the subgroup is used rather than the + position of the submatch. - name->location: the argument is replaced by the named location. - name->Golden: the argument is used to look up golden content prefixed by @. @@ -336,12 +340,12 @@ files, and sandboxed directory. Argument converters translate the "b" and "abc" arguments into locations by interpreting each one as a substring (or as a regular expression, if of the -form re"a|b") and finding the location of its first occurrence on the preceding -portion of the line, and the abc identifier into a the golden content contained -in the file @abc. Then the hoverMarker method executes a textDocument/hover LSP -request at the src position, and ensures the result spans "abc", with the -markdown content from @abc. (Note that the markdown content includes the expect -annotation as the doc comment.) +form re"a|b") and finding the location of its first occurrence starting on the +preceding portion of the line, and the abc identifier into a the golden content +contained in the file @abc. Then the hoverMarker method executes a +textDocument/hover LSP request at the src position, and ensures the result +spans "abc", with the markdown content from @abc. (Note that the markdown +content includes the expect annotation as the doc comment.) The next hover on the same line asserts the same result, but initiates the hover immediately after "abc" in the source. This tests that we find the diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index 9d22d882f26..87aecfaf6ed 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -1047,8 +1047,16 @@ var ( // // Converters should return an error rather than calling marker.errorf(). var customConverters = map[reflect.Type]func(marker, any) (any, error){ - reflect.TypeOf(protocol.Location{}): convertLocation, - reflect.TypeOf(completionLabel("")): convertCompletionLabel, + reflect.TypeOf(protocol.Location{}): converter(convertLocation), + reflect.TypeOf(completionLabel("")): converter(convertCompletionLabel), +} + +// converter transforms a typed argument conversion function to an untyped +// conversion function. +func converter[T any](f func(marker, any) (T, error)) func(marker, any) (any, error) { + return func(m marker, arg any) (any, error) { + return f(m, arg) + } } func convert(mark marker, arg any, paramType reflect.Type) (any, error) { @@ -1086,26 +1094,64 @@ func convert(mark marker, arg any, paramType reflect.Type) (any, error) { // convertLocation converts a string or regexp argument into the protocol // location corresponding to the first position of the string (or first match // of the regexp) in the line preceding the note. -func convertLocation(mark marker, arg any) (any, error) { +func convertLocation(mark marker, arg any) (protocol.Location, error) { + // matchContent is used to match the given argument against the file content + // starting at the marker line. + var matchContent func([]byte) (int, int, error) + switch arg := arg.(type) { case protocol.Location: - return arg, nil + return arg, nil // nothing to do case string: - startOff, preceding, m, err := linePreceding(mark.run, mark.note.Pos) - if err != nil { - return protocol.Location{}, err - } - idx := bytes.Index(preceding, []byte(arg)) - if idx < 0 { - return protocol.Location{}, fmt.Errorf("substring %q not found in %q", arg, preceding) + matchContent = func(content []byte) (int, int, error) { + idx := bytes.Index(content, []byte(arg)) + if idx < 0 { + return 0, 0, fmt.Errorf("substring %q not found", arg) + } + return idx, idx + len(arg), nil } - off := startOff + idx - return m.OffsetLocation(off, off+len(arg)) case *regexp.Regexp: - return findRegexpInLine(mark.run, mark.note.Pos, arg) + matchContent = func(content []byte) (int, int, error) { + matches := arg.FindSubmatchIndex(content) + if len(matches) == 0 { + return 0, 0, fmt.Errorf("no match for regexp %q", arg) + } + switch len(matches) { + case 2: + // no subgroups: return the range of the regexp expression + return matches[0], matches[1], nil + case 4: + // one subgroup: return its range + return matches[2], matches[3], nil + default: + return 0, 0, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", arg, len(matches)/2-1) + } + } default: return protocol.Location{}, fmt.Errorf("cannot convert argument type %T to location (must be a string or regexp to match the preceding line)", arg) } + + // Now use matchFunc to match a range starting on the marker line. + + file := mark.run.test.fset.File(mark.note.Pos) + posn := safetoken.Position(file, mark.note.Pos) + lineStart := file.LineStart(posn.Line) + lineStartOff, lineEndOff, err := safetoken.Offsets(file, lineStart, mark.note.Pos) + if err != nil { + return protocol.Location{}, err + } + m := mark.mapper() + start, end, err := matchContent(m.Content[lineStartOff:]) + if err != nil { + return protocol.Location{}, err + } + startOff, endOff := lineStartOff+start, lineStartOff+end + if startOff > lineEndOff { + // The start of the match must be between the start of the line and the + // marker position (inclusive). + return protocol.Location{}, fmt.Errorf("no matching range found starting on the current line") + } + return m.OffsetLocation(startOff, endOff) } // completionLabel is a special parameter type that may be converted from a @@ -1122,7 +1168,7 @@ type completionLabel string // // This allows us to stage a migration of the "snippet" marker to a simpler // model where the completion label can just be listed explicitly. -func convertCompletionLabel(mark marker, arg any) (any, error) { +func convertCompletionLabel(mark marker, arg any) (completionLabel, error) { switch arg := arg.(type) { case string: return completionLabel(arg), nil @@ -1133,50 +1179,6 @@ func convertCompletionLabel(mark marker, arg any) (any, error) { } } -// findRegexpInLine searches the partial line preceding pos for a match for the -// regular expression re, returning a location spanning the first match. If re -// contains exactly one subgroup, the position of this subgroup match is -// returned rather than the position of the full match. -func findRegexpInLine(run *markerTestRun, pos token.Pos, re *regexp.Regexp) (protocol.Location, error) { - startOff, preceding, m, err := linePreceding(run, pos) - if err != nil { - return protocol.Location{}, err - } - - matches := re.FindSubmatchIndex(preceding) - if len(matches) == 0 { - return protocol.Location{}, fmt.Errorf("no match for regexp %q found in %q", re, string(preceding)) - } - var start, end int - switch len(matches) { - case 2: - // no subgroups: return the range of the regexp expression - start, end = matches[0], matches[1] - case 4: - // one subgroup: return its range - start, end = matches[2], matches[3] - default: - return protocol.Location{}, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", re, len(matches)/2-1) - } - - return m.OffsetLocation(start+startOff, end+startOff) -} - -func linePreceding(run *markerTestRun, pos token.Pos) (int, []byte, *protocol.Mapper, error) { - file := run.test.fset.File(pos) - posn := safetoken.Position(file, pos) - lineStart := file.LineStart(posn.Line) - startOff, endOff, err := safetoken.Offsets(file, lineStart, pos) - if err != nil { - return 0, nil, nil, err - } - m, err := run.env.Editor.Mapper(file.Name()) - if err != nil { - return 0, nil, nil, err - } - return startOff, m.Content[startOff:endOff], m, nil -} - // convertStringMatcher converts a string, regexp, or identifier // argument into a stringMatcher. The string is a substring of the // expected error, the regexp is a pattern than matches the expected diff --git a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt index d5dbe931226..1c9df2ec5bb 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt @@ -13,13 +13,13 @@ package a func _() { var logf func(string, ...any) - { println(logf) } //@loc(block, re`{.*}`) + { println(logf) } //@loc(block, re`{[^}]*}`) } -- @out/a/a.go -- @@ -7 +7 @@ -- { println(logf) } //@loc(block, re`{.*}`) -+ { newFunction(logf) } //@loc(block, re`{.*}`) +- { println(logf) } //@loc(block, re`{[^}]*}`) ++ { newFunction(logf) } //@loc(block, re`{[^}]*}`) @@ -10 +10,4 @@ +func newFunction(logf func( string, ...any)) { + println(logf) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt index 943a3ac672c..2b357be9e3c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt @@ -30,7 +30,7 @@ func (a *A) XLessThanYP() bool { func (a *A) AddP() int { sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) - return sum //@loc(A_AddP2, re`return.*sum`) + return sum //@loc(A_AddP2, re`return.*?sum`) } func (a A) XLessThanY() bool { @@ -39,7 +39,7 @@ func (a A) XLessThanY() bool { func (a A) Add() int { sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) - return sum //@loc(A_Add2, re`return.*sum`) + return sum //@loc(A_Add2, re`return.*?sum`) } -- @func1/basic.go -- @@ -63,8 +63,8 @@ func (a A) Add() int { + -- @func3/basic.go -- @@ -27 +27 @@ -- return sum //@loc(A_AddP2, re`return.*sum`) -+ return newFunction(sum) //@loc(A_AddP2, re`return.*sum`) +- return sum //@loc(A_AddP2, re`return.*?sum`) ++ return newFunction(sum) //@loc(A_AddP2, re`return.*?sum`) @@ -30 +30,4 @@ +func newFunction(sum int) int { + return sum @@ -91,8 +91,8 @@ func (a A) Add() int { + -- @func6/basic.go -- @@ -36 +36 @@ -- return sum //@loc(A_Add2, re`return.*sum`) -+ return newFunction(sum) //@loc(A_Add2, re`return.*sum`) +- return sum //@loc(A_Add2, re`return.*?sum`) ++ return newFunction(sum) //@loc(A_Add2, re`return.*?sum`) @@ -39 +39,4 @@ +func newFunction(sum int) int { + return sum @@ -119,8 +119,8 @@ func (a A) Add() int { + -- @meth3/basic.go -- @@ -27 +27 @@ -- return sum //@loc(A_AddP2, re`return.*sum`) -+ return a.newMethod(sum) //@loc(A_AddP2, re`return.*sum`) +- return sum //@loc(A_AddP2, re`return.*?sum`) ++ return a.newMethod(sum) //@loc(A_AddP2, re`return.*?sum`) @@ -30 +30,4 @@ +func (*A) newMethod(sum int) int { + return sum @@ -147,8 +147,8 @@ func (a A) Add() int { + -- @meth6/basic.go -- @@ -36 +36 @@ -- return sum //@loc(A_Add2, re`return.*sum`) -+ return a.newMethod(sum) //@loc(A_Add2, re`return.*sum`) +- return sum //@loc(A_Add2, re`return.*?sum`) ++ return a.newMethod(sum) //@loc(A_Add2, re`return.*?sum`) @@ -39 +39,4 @@ +func (A) newMethod(sum int) int { + return sum diff --git a/gopls/internal/test/marker/testdata/completion/comment.txt b/gopls/internal/test/marker/testdata/completion/comment.txt index 68f2c20cdcf..f66bfdab186 100644 --- a/gopls/internal/test/marker/testdata/completion/comment.txt +++ b/gopls/internal/test/marker/testdata/completion/comment.txt @@ -13,26 +13,26 @@ package comment_completion var p bool -//@complete(re"$") +//@complete(re"//()") func _() { var a int switch a { case 1: - //@complete(re"$") + //@complete(re"//()") _ = a } var b chan int select { case <-b: - //@complete(re"$") + //@complete(re"//()") _ = b } var ( - //@complete(re"$") + //@complete(re"//()") _ = a ) } diff --git a/gopls/internal/test/marker/testdata/definition/embed.txt b/gopls/internal/test/marker/testdata/definition/embed.txt index 4bda1d71ebc..5dc976c8b4d 100644 --- a/gopls/internal/test/marker/testdata/definition/embed.txt +++ b/gopls/internal/test/marker/testdata/definition/embed.txt @@ -47,7 +47,7 @@ type J interface { //@loc(J, "J") -- b/b.go -- package b -import "mod.com/a" //@loc(AImport, re"\".*\"") +import "mod.com/a" //@loc(AImport, re"\"[^\"]*\"") type embed struct { F int //@loc(F, "F") diff --git a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt index f041ee9d9ae..34488bec417 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt @@ -27,7 +27,7 @@ func _() { // printf func _() { - printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, re"example.com.printfWrapper format %s reads arg #1, but call has 0 args") + printfWrapper("%s") //@diag(re`printfWrapper\(.*?\)`, re"example.com.printfWrapper format %s reads arg #1, but call has 0 args") } func printfWrapper(format string, args ...interface{}) { From c2e057bbd2de0afa44c8449ee828c700db19d2e9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 9 Sep 2024 13:50:43 -0400 Subject: [PATCH 004/102] gopls: use Func.Signature everywhere Updates golang/go#65917 Change-Id: I20bec8b0a1778f0d2d81f729d12ba966799c7805 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612037 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/methodsets/methodsets.go | 2 +- gopls/internal/golang/code_lens.go | 2 +- gopls/internal/golang/codeaction.go | 2 +- gopls/internal/golang/completion/completion.go | 2 +- gopls/internal/golang/completion/format.go | 2 +- gopls/internal/golang/hover.go | 6 +++--- gopls/internal/golang/implementation.go | 4 ++-- gopls/internal/golang/pkgdoc.go | 10 +++++----- gopls/internal/golang/references.go | 2 +- gopls/internal/golang/rename.go | 6 +++--- gopls/internal/golang/rename_check.go | 2 +- gopls/internal/golang/stub.go | 4 ++-- 12 files changed, 22 insertions(+), 22 deletions(-) diff --git a/gopls/internal/cache/methodsets/methodsets.go b/gopls/internal/cache/methodsets/methodsets.go index ed7ead7a747..f6a2ba96b33 100644 --- a/gopls/internal/cache/methodsets/methodsets.go +++ b/gopls/internal/cache/methodsets/methodsets.go @@ -449,7 +449,7 @@ func fingerprint(method *types.Func) (string, bool) { } buf.WriteString(method.Id()) // e.g. "pkg.Type" - sig := method.Type().(*types.Signature) + sig := method.Signature() fprint(sig.Params()) fprint(sig.Results()) return buf.String(), tricky diff --git a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go index a4aab3e16b0..26380b865af 100644 --- a/gopls/internal/golang/code_lens.go +++ b/gopls/internal/golang/code_lens.go @@ -129,7 +129,7 @@ func matchTestFunc(fn *ast.FuncDecl, info *types.Info, nameRe *regexp.Regexp, pa if !ok { return false } - sig := obj.Type().(*types.Signature) + sig := obj.Signature() // Test functions should have only one parameter. if sig.Params().Len() != 1 { return false diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 8f402267393..1921d1326b8 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -597,7 +597,7 @@ func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File if len(path) >= 2 { // [... FuncDecl File] if decl, ok := path[len(path)-2].(*ast.FuncDecl); ok { if fn, ok := pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok { - sig := fn.Type().(*types.Signature) + sig := fn.Signature() // Compute the linker symbol of the enclosing function. var sym strings.Builder diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 88528df6a79..3fdc2a8c62a 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -1036,7 +1036,7 @@ func (c *completer) populateCommentCompletions(comment *ast.CommentGroup) { // collect receiver struct fields if node.Recv != nil { - sig := c.pkg.TypesInfo().Defs[node.Name].(*types.Func).Type().(*types.Signature) + sig := c.pkg.TypesInfo().Defs[node.Name].(*types.Func).Signature() _, named := typesinternal.ReceiverNamed(sig.Recv()) // may be nil if ill-typed if named != nil { if recvStruct, ok := named.Underlying().(*types.Struct); ok { diff --git a/gopls/internal/golang/completion/format.go b/gopls/internal/golang/completion/format.go index dbc57c18082..2e35fd5de38 100644 --- a/gopls/internal/golang/completion/format.go +++ b/gopls/internal/golang/completion/format.go @@ -95,7 +95,7 @@ func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, e break } case *types.Func: - if obj.Type().(*types.Signature).Recv() == nil { + if obj.Signature().Recv() == nil { kind = protocol.FunctionCompletion } else { kind = protocol.MethodCompletion diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 129adde7fcc..019d09ac027 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -535,7 +535,7 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro var recv types.Object switch obj := obj.(type) { case *types.Func: - sig := obj.Type().(*types.Signature) + sig := obj.Signature() if sig.Recv() != nil { tname := typeToObject(sig.Recv().Type()) if tname != nil { // beware typed nil @@ -962,7 +962,7 @@ func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file // specifically, we show the receiver name, // and replace the period in (T).f by a space (#62190). - sig := obj.Type().(*types.Signature) + sig := obj.Signature() var buf bytes.Buffer buf.WriteString("func ") @@ -1236,7 +1236,7 @@ func StdSymbolOf(obj types.Object) *stdlib.Symbol { // Handle Method. if fn, _ := obj.(*types.Func); fn != nil { - isPtr, named := typesinternal.ReceiverNamed(fn.Type().(*types.Signature).Recv()) + isPtr, named := typesinternal.ReceiverNamed(fn.Signature().Recv()) if isPackageLevel(named.Obj()) { for _, s := range symbols { if s.Kind != stdlib.Method { diff --git a/gopls/internal/golang/implementation.go b/gopls/internal/golang/implementation.go index 72679ad7176..b3accff452f 100644 --- a/gopls/internal/golang/implementation.go +++ b/gopls/internal/golang/implementation.go @@ -126,7 +126,7 @@ func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand return obj.Type(), "" case *types.Func: // For methods, use the receiver type, which may be anonymous. - if recv := obj.Type().(*types.Signature).Recv(); recv != nil { + if recv := obj.Signature().Recv(); recv != nil { return recv.Type(), obj.Id() } } @@ -317,7 +317,7 @@ func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.D case *types.TypeName: // ok case *types.Func: - if obj.Type().(*types.Signature).Recv() == nil { + if obj.Signature().Recv() == nil { return nil, nil, fmt.Errorf("%s is a function, not a method", id.Name) } case nil: diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go index 38d043b7d74..ed8f1b388f0 100644 --- a/gopls/internal/golang/pkgdoc.go +++ b/gopls/internal/golang/pkgdoc.go @@ -120,7 +120,7 @@ func DocFragment(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (p if !sym.Exported() { // Unexported method of exported type? if fn, ok := sym.(*types.Func); ok { - if recv := fn.Type().(*types.Signature).Recv(); recv != nil { + if recv := fn.Signature().Recv(); recv != nil { _, named := typesinternal.ReceiverNamed(recv) if named != nil && named.Obj().Exported() { sym = named.Obj() @@ -147,7 +147,7 @@ func DocFragment(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (p // Inv: sym is field or method, or local. switch sym := sym.(type) { case *types.Func: // => method - sig := sym.Type().(*types.Signature) + sig := sym.Signature() isPtr, named := typesinternal.ReceiverNamed(sig.Recv()) if named != nil { if !named.Obj().Exported() { @@ -469,7 +469,7 @@ window.addEventListener('load', function() { label := obj.Name() // for a type if fn, ok := obj.(*types.Func); ok { var buf strings.Builder - sig := fn.Type().(*types.Signature) + sig := fn.Signature() if sig.Recv() != nil { fmt.Fprintf(&buf, "(%s) ", sig.Recv().Name()) fragment = recvType + "." + fn.Name() @@ -551,7 +551,7 @@ window.addEventListener('load', function() { // method of package-level named type? if fn, ok := obj.(*types.Func); ok { - sig := fn.Type().(*types.Signature) + sig := fn.Signature() if sig.Recv() != nil { _, named := typesinternal.ReceiverNamed(sig.Recv()) if named != nil { @@ -648,7 +648,7 @@ window.addEventListener('load', function() { fnString := func(fn *types.Func) string { pkgRelative := typesinternal.NameRelativeTo(pkg.Types()) - sig := fn.Type().(*types.Signature) + sig := fn.Signature() // Emit "func (recv T) F". var buf bytes.Buffer diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go index b78bd9041d5..6679b45df6b 100644 --- a/gopls/internal/golang/references.go +++ b/gopls/internal/golang/references.go @@ -615,7 +615,7 @@ func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspo // comparisons for obj, if it is a method, or nil otherwise. func effectiveReceiver(obj types.Object) types.Type { if fn, ok := obj.(*types.Func); ok { - if recv := fn.Type().(*types.Signature).Recv(); recv != nil { + if recv := fn.Signature().Recv(); recv != nil { return methodsets.EnsurePointer(recv.Type()) } } diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index c5cf0ac0932..12d9d283915 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -426,7 +426,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle // contain a reference (xrefs) to the target field. case *types.Func: - if obj.Type().(*types.Signature).Recv() != nil { + if obj.Signature().Recv() != nil { transitive = true // method } @@ -978,7 +978,7 @@ func renameObjects(newName string, pkg *cache.Package, targets ...types.Object) // TODO(adonovan): pull this into the caller. for _, obj := range targets { if obj, ok := obj.(*types.Func); ok { - recv := obj.Type().(*types.Signature).Recv() + recv := obj.Signature().Recv() if recv != nil && types.IsInterface(recv.Type().Underlying()) { r.changeMethods = true break @@ -1168,7 +1168,7 @@ func (r *renamer) updateCommentDocLinks() (map[protocol.DocumentURI][]diff.Edit, if !isFunc { continue } - recv := obj.Type().(*types.Signature).Recv() + recv := obj.Signature().Recv() if recv == nil { continue } diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 497f1a09ca2..574ea8dbea7 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -883,7 +883,7 @@ func (r *renamer) satisfy() map[satisfy.Constraint]bool { // recv returns the method's receiver. func recv(meth *types.Func) *types.Var { - return meth.Type().(*types.Signature).Recv() + return meth.Signature().Recv() } // someUse returns an arbitrary use of obj within info. diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go index 47bcf3a7dcf..db405631c9e 100644 --- a/gopls/internal/golang/stub.go +++ b/gopls/internal/golang/stub.go @@ -208,7 +208,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. // Otherwise, use lowercase for the first letter of the object. rn := strings.ToLower(si.Concrete.Obj().Name()[0:1]) for i := 0; i < si.Concrete.NumMethods(); i++ { - if recv := si.Concrete.Method(i).Type().(*types.Signature).Recv(); recv.Name() != "" { + if recv := si.Concrete.Method(i).Signature().Recv(); recv.Name() != "" { rn = recv.Name() break } @@ -229,7 +229,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. for index := range missing { mrn := rn + " " - sig := missing[index].fn.Type().(*types.Signature) + sig := missing[index].fn.Signature() if checkRecvName(sig.Params()) || checkRecvName(sig.Results()) { mrn = "" } From c055e89c761bbbe60c0a8c53b86dd2b202565160 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 9 Sep 2024 14:03:46 -0400 Subject: [PATCH 005/102] x/tools: deprecate astutil.Unparen The go1.22 go/ast package now provides it. Updates golang/go#60061 Change-Id: I24e201660e3a8752dd505eefc894c7acae4c99f3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612038 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/assign/assign.go | 3 +-- go/analysis/passes/bools/bools.go | 3 +-- go/analysis/passes/cgocall/cgocall.go | 3 +-- go/analysis/passes/copylock/copylock.go | 5 ++--- .../passes/testinggoroutine/testinggoroutine.go | 9 ++++----- go/analysis/passes/testinggoroutine/util.go | 3 +-- go/analysis/passes/unsafeptr/unsafeptr.go | 5 ++--- go/analysis/passes/unusedresult/unusedresult.go | 3 +-- go/ast/astutil/util.go | 12 ++---------- go/ssa/util.go | 3 +-- go/types/typeutil/callee.go | 3 +-- internal/refactor/inline/callee.go | 3 +-- internal/refactor/inline/inline.go | 6 +++--- refactor/satisfy/find.go | 3 +-- 14 files changed, 22 insertions(+), 42 deletions(-) diff --git a/go/analysis/passes/assign/assign.go b/go/analysis/passes/assign/assign.go index 3bfd501226f..0d95fefcb5a 100644 --- a/go/analysis/passes/assign/assign.go +++ b/go/analysis/passes/assign/assign.go @@ -18,7 +18,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" ) @@ -78,7 +77,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // isMapIndex returns true if e is a map index expression. func isMapIndex(info *types.Info, e ast.Expr) bool { - if idx, ok := astutil.Unparen(e).(*ast.IndexExpr); ok { + if idx, ok := ast.Unparen(e).(*ast.IndexExpr); ok { if typ := info.Types[idx.X].Type; typ != nil { _, ok := typ.Underlying().(*types.Map) return ok diff --git a/go/analysis/passes/bools/bools.go b/go/analysis/passes/bools/bools.go index 564329774ef..8cec6e8224a 100644 --- a/go/analysis/passes/bools/bools.go +++ b/go/analysis/passes/bools/bools.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" ) @@ -169,7 +168,7 @@ func (op boolOp) checkSuspect(pass *analysis.Pass, exprs []ast.Expr) { // seen[e] is already true; any newly processed exprs are added to seen. func (op boolOp) split(e ast.Expr, seen map[*ast.BinaryExpr]bool) (exprs []ast.Expr) { for { - e = astutil.Unparen(e) + e = ast.Unparen(e) if b, ok := e.(*ast.BinaryExpr); ok && b.Op == op.tok { seen[b] = true exprs = append(exprs, op.split(b.Y, seen)...) diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go index 4e864397574..26ec0683158 100644 --- a/go/analysis/passes/cgocall/cgocall.go +++ b/go/analysis/passes/cgocall/cgocall.go @@ -19,7 +19,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" - "golang.org/x/tools/go/ast/astutil" ) const debug = false @@ -65,7 +64,7 @@ func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(t // Is this a C.f() call? var name string - if sel, ok := astutil.Unparen(call.Fun).(*ast.SelectorExpr); ok { + if sel, ok := ast.Unparen(call.Fun).(*ast.SelectorExpr); ok { if id, ok := sel.X.(*ast.Ident); ok && id.Name == "C" { name = sel.Sel.Name } diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go index 0d63cd16124..3e9a55be791 100644 --- a/go/analysis/passes/copylock/copylock.go +++ b/go/analysis/passes/copylock/copylock.go @@ -16,7 +16,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" @@ -253,7 +252,7 @@ func (path typePath) String() string { } func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath { - x = astutil.Unparen(x) // ignore parens on rhs + x = ast.Unparen(x) // ignore parens on rhs if _, ok := x.(*ast.CompositeLit); ok { return nil @@ -263,7 +262,7 @@ func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath { return nil } if star, ok := x.(*ast.StarExpr); ok { - if _, ok := astutil.Unparen(star.X).(*ast.CallExpr); ok { + if _, ok := ast.Unparen(star.X).(*ast.CallExpr); ok { // A call may return a pointer to a zero value. return nil } diff --git a/go/analysis/passes/testinggoroutine/testinggoroutine.go b/go/analysis/passes/testinggoroutine/testinggoroutine.go index 828f95bc862..aaf1c76e570 100644 --- a/go/analysis/passes/testinggoroutine/testinggoroutine.go +++ b/go/analysis/passes/testinggoroutine/testinggoroutine.go @@ -14,7 +14,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/aliases" @@ -186,7 +185,7 @@ func withinScope(scope ast.Node, x *types.Var) bool { func goAsyncCall(info *types.Info, goStmt *ast.GoStmt, toDecl func(*types.Func) *ast.FuncDecl) *asyncCall { call := goStmt.Call - fun := astutil.Unparen(call.Fun) + fun := ast.Unparen(call.Fun) if id := funcIdent(fun); id != nil { if lit := funcLitInScope(id); lit != nil { return &asyncCall{region: lit, async: goStmt, scope: nil, fun: fun} @@ -213,7 +212,7 @@ func tRunAsyncCall(info *types.Info, call *ast.CallExpr) *asyncCall { return nil } - fun := astutil.Unparen(call.Args[1]) + fun := ast.Unparen(call.Args[1]) if lit, ok := fun.(*ast.FuncLit); ok { // function lit? return &asyncCall{region: lit, async: call, scope: lit, fun: fun} } @@ -243,7 +242,7 @@ var forbidden = []string{ // Returns (nil, nil, nil) if call is not of this form. func forbiddenMethod(info *types.Info, call *ast.CallExpr) (*types.Var, *types.Selection, *types.Func) { // Compare to typeutil.StaticCallee. - fun := astutil.Unparen(call.Fun) + fun := ast.Unparen(call.Fun) selExpr, ok := fun.(*ast.SelectorExpr) if !ok { return nil, nil, nil @@ -254,7 +253,7 @@ func forbiddenMethod(info *types.Info, call *ast.CallExpr) (*types.Var, *types.S } var x *types.Var - if id, ok := astutil.Unparen(selExpr.X).(*ast.Ident); ok { + if id, ok := ast.Unparen(selExpr.X).(*ast.Ident); ok { x, _ = info.Uses[id].(*types.Var) } if x == nil { diff --git a/go/analysis/passes/testinggoroutine/util.go b/go/analysis/passes/testinggoroutine/util.go index ad815f19010..8c7a51ca525 100644 --- a/go/analysis/passes/testinggoroutine/util.go +++ b/go/analysis/passes/testinggoroutine/util.go @@ -8,7 +8,6 @@ import ( "go/ast" "go/types" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/typeparams" ) @@ -56,7 +55,7 @@ func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool { } func funcIdent(fun ast.Expr) *ast.Ident { - switch fun := astutil.Unparen(fun).(type) { + switch fun := ast.Unparen(fun).(type) { case *ast.IndexExpr, *ast.IndexListExpr: x, _, _, _ := typeparams.UnpackIndexExpr(fun) // necessary? id, _ := x.(*ast.Ident) diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go index 14e4a6c1e4b..f4261a67cb4 100644 --- a/go/analysis/passes/unsafeptr/unsafeptr.go +++ b/go/analysis/passes/unsafeptr/unsafeptr.go @@ -15,7 +15,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/aliases" ) @@ -70,7 +69,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // Check unsafe.Pointer safety rules according to // https://golang.org/pkg/unsafe/#Pointer. - switch x := astutil.Unparen(x).(type) { + switch x := ast.Unparen(x).(type) { case *ast.SelectorExpr: // "(6) Conversion of a reflect.SliceHeader or // reflect.StringHeader Data field to or from Pointer." @@ -119,7 +118,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // isSafeArith reports whether x is a pointer arithmetic expression that is safe // to convert to unsafe.Pointer. func isSafeArith(info *types.Info, x ast.Expr) bool { - switch x := astutil.Unparen(x).(type) { + switch x := ast.Unparen(x).(type) { case *ast.CallExpr: // Base case: initial conversion from unsafe.Pointer to uintptr. return len(x.Args) == 1 && diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index 76f42b052e4..c27d26dd6ec 100644 --- a/go/analysis/passes/unusedresult/unusedresult.go +++ b/go/analysis/passes/unusedresult/unusedresult.go @@ -24,7 +24,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" ) @@ -101,7 +100,7 @@ func run(pass *analysis.Pass) (interface{}, error) { (*ast.ExprStmt)(nil), } inspect.Preorder(nodeFilter, func(n ast.Node) { - call, ok := astutil.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr) + call, ok := ast.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr) if !ok { return // not a call statement } diff --git a/go/ast/astutil/util.go b/go/ast/astutil/util.go index 6bdcf70ac27..ca71e3e1055 100644 --- a/go/ast/astutil/util.go +++ b/go/ast/astutil/util.go @@ -7,13 +7,5 @@ package astutil import "go/ast" // Unparen returns e with any enclosing parentheses stripped. -// TODO(adonovan): use go1.22's ast.Unparen. -func Unparen(e ast.Expr) ast.Expr { - for { - p, ok := e.(*ast.ParenExpr) - if !ok { - return e - } - e = p.X - } -} +// Deprecated: use [ast.Unparen]. +func Unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } diff --git a/go/ssa/util.go b/go/ssa/util.go index 549c9c819ea..4ac2b198eca 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -15,7 +15,6 @@ import ( "os" "sync" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" @@ -36,7 +35,7 @@ func assert(p bool, msg string) { //// AST utilities -func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } +func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } // isBlankIdent returns true iff e is an Ident with name "_". // They have no associated types.Object, and thus no type. diff --git a/go/types/typeutil/callee.go b/go/types/typeutil/callee.go index 90dc541adfe..754380351e8 100644 --- a/go/types/typeutil/callee.go +++ b/go/types/typeutil/callee.go @@ -8,7 +8,6 @@ import ( "go/ast" "go/types" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/typeparams" ) @@ -17,7 +16,7 @@ import ( // // Functions and methods may potentially have type parameters. func Callee(info *types.Info, call *ast.CallExpr) types.Object { - fun := astutil.Unparen(call.Fun) + fun := ast.Unparen(call.Fun) // Look through type instantiation if necessary. isInstance := false diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go index 09deda33b5e..c72699cb772 100644 --- a/internal/refactor/inline/callee.go +++ b/internal/refactor/inline/callee.go @@ -16,7 +16,6 @@ import ( "go/types" "strings" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" ) @@ -242,7 +241,7 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa // not just a return statement } else if ret, ok := decl.Body.List[0].(*ast.ReturnStmt); ok && len(ret.Results) == 1 { validForCallStmt = func() bool { - switch expr := astutil.Unparen(ret.Results[0]).(type) { + switch expr := ast.Unparen(ret.Results[0]).(type) { case *ast.CallExpr: // f(x) callee := typeutil.Callee(info, expr) if callee == nil { diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index da2d26e8d4d..21f3147d000 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -527,7 +527,7 @@ func (st *state) inlineCall() (*inlineCallResult, error) { // or time.Second.String()) will remain after // inlining, as arguments. if pkgName, ok := existing.(*types.PkgName); ok { - if sel, ok := astutil.Unparen(caller.Call.Fun).(*ast.SelectorExpr); ok { + if sel, ok := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr); ok { if sole := soleUse(caller.Info, pkgName); sole == sel.X { for _, spec := range caller.File.Imports { pkgName2, ok := importedPkgName(caller.Info, spec) @@ -1263,7 +1263,7 @@ func (st *state) arguments(caller *Caller, calleeDecl *ast.FuncDecl, assign1 fun callArgs := caller.Call.Args if calleeDecl.Recv != nil { - sel := astutil.Unparen(caller.Call.Fun).(*ast.SelectorExpr) + sel := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr) seln := caller.Info.Selections[sel] var recvArg ast.Expr switch seln.Kind() { @@ -2227,7 +2227,7 @@ func pure(info *types.Info, assign1 func(*types.Var) bool, e ast.Expr) bool { // be evaluated at any point--though not necessarily at multiple // points (consider new, make). func callsPureBuiltin(info *types.Info, call *ast.CallExpr) bool { - if id, ok := astutil.Unparen(call.Fun).(*ast.Ident); ok { + if id, ok := ast.Unparen(call.Fun).(*ast.Ident); ok { if b, ok := info.ObjectOf(id).(*types.Builtin); ok { switch b.Name() { case "len", "cap", "complex", "imag", "real", "make", "new", "max", "min": diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index bab0e3cfd3f..3d693aa04ab 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go @@ -43,7 +43,6 @@ import ( "go/token" "go/types" - "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" ) @@ -708,7 +707,7 @@ func (f *Finder) stmt(s ast.Stmt) { // -- Plundered from golang.org/x/tools/go/ssa ----------------- -func unparen(e ast.Expr) ast.Expr { return astutil.Unparen(e) } +func unparen(e ast.Expr) ast.Expr { return ast.Unparen(e) } func isInterface(T types.Type) bool { return types.IsInterface(T) } From 1dc949f0bf3eadbcc4a29bf94cdac0ebc2adcead Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Sep 2024 18:17:33 +0000 Subject: [PATCH 006/102] internal/settings: simplify linking now that we only build with 1.23 Since gopls now only builds with the latest version of Go, we no longer need special linking to align with the compatibility windows of gufumpt or staticcheck. Updates golang/go#65917 Change-Id: I7d5ebe6807b34ed8d44e726c7a6585d4c7c7e696 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612055 LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/settings/analysis.go | 4 ---- gopls/internal/settings/analysis_119.go | 10 --------- .../settings/{gofumpt_120.go => gofumpt.go} | 5 ----- gopls/internal/settings/gofumpt_119.go | 14 ------------- .../{gofumpt_120_test.go => gofumpt_test.go} | 3 --- gopls/internal/settings/settings.go | 21 ++----------------- gopls/internal/settings/settings_test.go | 9 -------- .../{analysis_120.go => staticcheck.go} | 7 +++---- 8 files changed, 5 insertions(+), 68 deletions(-) delete mode 100644 gopls/internal/settings/analysis_119.go rename gopls/internal/settings/{gofumpt_120.go => gofumpt.go} (96%) delete mode 100644 gopls/internal/settings/gofumpt_119.go rename gopls/internal/settings/{gofumpt_120_test.go => gofumpt_test.go} (97%) rename gopls/internal/settings/{analysis_120.go => staticcheck.go} (90%) diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go index bbf3c75bc83..65ecb215c02 100644 --- a/gopls/internal/settings/analysis.go +++ b/gopls/internal/settings/analysis.go @@ -216,7 +216,3 @@ func init() { DefaultAnalyzers[analyzer.analyzer.Name] = analyzer } } - -// StaticcheckAnalzyers describes available Staticcheck analyzers, keyed by -// analyzer name. -var StaticcheckAnalyzers = make(map[string]*Analyzer) // written by analysis_.go diff --git a/gopls/internal/settings/analysis_119.go b/gopls/internal/settings/analysis_119.go deleted file mode 100644 index 4493a380237..00000000000 --- a/gopls/internal/settings/analysis_119.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2021 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. - -//go:build !go1.20 -// +build !go1.20 - -package settings - -const StaticcheckSupported = false diff --git a/gopls/internal/settings/gofumpt_120.go b/gopls/internal/settings/gofumpt.go similarity index 96% rename from gopls/internal/settings/gofumpt_120.go rename to gopls/internal/settings/gofumpt.go index eebf1f77b57..7a3541d4778 100644 --- a/gopls/internal/settings/gofumpt_120.go +++ b/gopls/internal/settings/gofumpt.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.20 -// +build go1.20 - package settings import ( @@ -14,8 +11,6 @@ import ( "mvdan.cc/gofumpt/format" ) -const GofumptSupported = true - // GofumptFormat allows the gopls module to wire in a call to // gofumpt/format.Source. langVersion and modulePath are used for some // Gofumpt formatting rules -- see the Gofumpt documentation for details. diff --git a/gopls/internal/settings/gofumpt_119.go b/gopls/internal/settings/gofumpt_119.go deleted file mode 100644 index 5734802c686..00000000000 --- a/gopls/internal/settings/gofumpt_119.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2021 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. - -//go:build !go1.20 -// +build !go1.20 - -package settings - -import "context" - -const GofumptSupported = false - -var GofumptFormat func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) diff --git a/gopls/internal/settings/gofumpt_120_test.go b/gopls/internal/settings/gofumpt_test.go similarity index 97% rename from gopls/internal/settings/gofumpt_120_test.go rename to gopls/internal/settings/gofumpt_test.go index 7ed54d5c888..090ebcb601f 100644 --- a/gopls/internal/settings/gofumpt_120_test.go +++ b/gopls/internal/settings/gofumpt_test.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.20 -// +build go1.20 - package settings import "testing" diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 2b676f37132..c9e155fcbc7 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -8,7 +8,6 @@ import ( "fmt" "maps" "path/filepath" - "runtime" "strings" "time" @@ -1073,15 +1072,7 @@ func (o *Options) setOne(name string, value any) error { } case "staticcheck": - v, err := asBool(value) - if err != nil { - return err - } - if v && !StaticcheckSupported { - return fmt.Errorf("staticcheck is not supported at %s;"+ - " rebuild gopls with a more recent version of Go", runtime.Version()) - } - o.Staticcheck = v + return setBool(&o.Staticcheck, value) case "local": return setString(&o.Local, value) @@ -1096,15 +1087,7 @@ func (o *Options) setOne(name string, value any) error { return setBool(&o.ShowBugReports, value) case "gofumpt": - v, err := asBool(value) - if err != nil { - return err - } - if v && !GofumptSupported { - return fmt.Errorf("gofumpt is not supported at %s;"+ - " rebuild gopls with a more recent version of Go", runtime.Version()) - } - o.Gofumpt = v + return setBool(&o.Gofumpt, value) case "completeFunctionCalls": return setBool(&o.CompleteFunctionCalls, value) diff --git a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go index e2375222639..6f865083a9d 100644 --- a/gopls/internal/settings/settings_test.go +++ b/gopls/internal/settings/settings_test.go @@ -199,15 +199,6 @@ func TestOptions_Set(t *testing.T) { }, } - if !StaticcheckSupported { - tests = append(tests, testCase{ - name: "staticcheck", - value: true, - check: func(o Options) bool { return o.Staticcheck == true }, - wantError: true, // o.StaticcheckSupported is unset - }) - } - for _, test := range tests { var opts Options err := opts.Set(map[string]any{test.name: test.value}) diff --git a/gopls/internal/settings/analysis_120.go b/gopls/internal/settings/staticcheck.go similarity index 90% rename from gopls/internal/settings/analysis_120.go rename to gopls/internal/settings/staticcheck.go index 6a53f365eaa..fca3e55f17e 100644 --- a/gopls/internal/settings/analysis_120.go +++ b/gopls/internal/settings/staticcheck.go @@ -2,9 +2,6 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.20 -// +build go1.20 - package settings import ( @@ -16,7 +13,9 @@ import ( "honnef.co/go/tools/stylecheck" ) -const StaticcheckSupported = true +// StaticcheckAnalzyers describes available Staticcheck analyzers, keyed by +// analyzer name. +var StaticcheckAnalyzers = make(map[string]*Analyzer) // written by analysis_.go func init() { mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { From bfc94c967a49df3b8941b7228d8823a480a22fe7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 4 Sep 2024 00:04:25 -0400 Subject: [PATCH 007/102] go/ssa: extract type recursion into a helper package This change moves ssa.forEachReachable into internal/typesinternal.ForEachElement, simplifies the signature (by internalizing the type map part) and adds a test. There are two copies of this algorithm (the other in go/callgraph/rta), and we will need it again in ssautil.Reachable (see golang/go#69291). A follow-up change will make the copy in rta delegate to this one (small steps). Also, make ssa.Program.RuntimeTypes do the type analysis when it is called, instead of doing it eagerly each time we encounter a MakeInterface instruction. This should reduce eventually costs since RuntimeTypes shouldn't be needed: it was added for the pointer analysis (since deleted) and it used by ssautil.AllFunctions (to be replaced, see golang/go#69291), and in both cases it is the wrong tool for the job because: (a) it is more precise to accumulate runtime types in the subset of the program of interest, while doing some kind of reachability fixed-point computation; and (b) its use in AllFunctions is unsound because although it accounts for all (too many!) MakeInterface operations it does not account for types exposed through public API (see the proposed replacement, ssautil.Reachable) when analyzing incomplete programs. Updates golang/go#69291 Change-Id: Ib369278e50295b9287fe95c06169b81425193e90 Reviewed-on: https://go-review.googlesource.com/c/tools/+/610939 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI --- go/ssa/builder.go | 20 ++-- go/ssa/emit.go | 2 +- go/ssa/methods.go | 137 +++------------------- go/ssa/ssa.go | 5 +- internal/typesinternal/element.go | 134 +++++++++++++++++++++ internal/typesinternal/element_test.go | 154 +++++++++++++++++++++++++ 6 files changed, 320 insertions(+), 132 deletions(-) create mode 100644 internal/typesinternal/element.go create mode 100644 internal/typesinternal/element_test.go diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 55943e45d24..f1fa43c51a0 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -3104,17 +3104,17 @@ func (b *builder) buildYieldFunc(fn *Function) { fn.finishBody() } -// addRuntimeType records t as a runtime type, -// along with all types derivable from it using reflection. +// addMakeInterfaceType records non-interface type t as the type of +// the operand a MakeInterface operation, for [Program.RuntimeTypes]. // -// Acquires prog.runtimeTypesMu. -func addRuntimeType(prog *Program, t types.Type) { - prog.runtimeTypesMu.Lock() - defer prog.runtimeTypesMu.Unlock() - forEachReachable(&prog.MethodSets, t, func(t types.Type) bool { - prev, _ := prog.runtimeTypes.Set(t, true).(bool) - return !prev // already seen? - }) +// Acquires prog.makeInterfaceTypesMu. +func addMakeInterfaceType(prog *Program, t types.Type) { + prog.makeInterfaceTypesMu.Lock() + defer prog.makeInterfaceTypesMu.Unlock() + if prog.makeInterfaceTypes == nil { + prog.makeInterfaceTypes = make(map[types.Type]unit) + } + prog.makeInterfaceTypes[t] = unit{} } // Build calls Package.Build for each package in prog. diff --git a/go/ssa/emit.go b/go/ssa/emit.go index c664ff85a0f..176c1e1a748 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -249,7 +249,7 @@ func emitConv(f *Function, val Value, typ types.Type) Value { // non-parameterized, as they are the set of runtime types. t := val.Type() if f.typeparams.Len() == 0 || !f.Prog.isParameterized(t) { - addRuntimeType(f.Prog, t) + addMakeInterfaceType(f.Prog, t) } mi := &MakeInterface{X: val} diff --git a/go/ssa/methods.go b/go/ssa/methods.go index b9560183a95..4b116f43072 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -11,7 +11,7 @@ import ( "go/types" "golang.org/x/tools/go/types/typeutil" - "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) // MethodValue returns the Function implementing method sel, building @@ -158,124 +158,23 @@ type methodSet struct { // // Thread-safe. // -// Acquires prog.runtimeTypesMu. +// Acquires prog.makeInterfaceTypesMu. func (prog *Program) RuntimeTypes() []types.Type { - prog.runtimeTypesMu.Lock() - defer prog.runtimeTypesMu.Unlock() - return prog.runtimeTypes.Keys() -} - -// forEachReachable calls f for type T and each type reachable from -// its type through reflection. -// -// The function f must use memoization to break cycles and -// return false when the type has already been visited. -// -// TODO(adonovan): publish in typeutil and share with go/callgraph/rta. -func forEachReachable(msets *typeutil.MethodSetCache, T types.Type, f func(types.Type) bool) { - var visit func(T types.Type, skip bool) - visit = func(T types.Type, skip bool) { - if !skip { - if !f(T) { - return - } - } - - // Recursion over signatures of each method. - tmset := msets.MethodSet(T) - for i := 0; i < tmset.Len(); i++ { - sig := tmset.At(i).Type().(*types.Signature) - // It is tempting to call visit(sig, false) - // but, as noted in golang.org/cl/65450043, - // the Signature.Recv field is ignored by - // types.Identical and typeutil.Map, which - // is confusing at best. - // - // More importantly, the true signature rtype - // reachable from a method using reflection - // has no receiver but an extra ordinary parameter. - // For the Read method of io.Reader we want: - // func(Reader, []byte) (int, error) - // but here sig is: - // func([]byte) (int, error) - // with .Recv = Reader (though it is hard to - // notice because it doesn't affect Signature.String - // or types.Identical). - // - // TODO(adonovan): construct and visit the correct - // non-method signature with an extra parameter - // (though since unnamed func types have no methods - // there is essentially no actual demand for this). - // - // TODO(adonovan): document whether or not it is - // safe to skip non-exported methods (as RTA does). - visit(sig.Params(), true) // skip the Tuple - visit(sig.Results(), true) // skip the Tuple - } - - switch T := T.(type) { - case *aliases.Alias: - visit(aliases.Unalias(T), skip) // emulates the pre-Alias behavior - - case *types.Basic: - // nop - - case *types.Interface: - // nop---handled by recursion over method set. - - case *types.Pointer: - visit(T.Elem(), false) - - case *types.Slice: - visit(T.Elem(), false) - - case *types.Chan: - visit(T.Elem(), false) - - case *types.Map: - visit(T.Key(), false) - visit(T.Elem(), false) - - case *types.Signature: - if T.Recv() != nil { - panic(fmt.Sprintf("Signature %s has Recv %s", T, T.Recv())) - } - visit(T.Params(), true) // skip the Tuple - visit(T.Results(), true) // skip the Tuple - - case *types.Named: - // A pointer-to-named type can be derived from a named - // type via reflection. It may have methods too. - visit(types.NewPointer(T), false) - - // Consider 'type T struct{S}' where S has methods. - // Reflection provides no way to get from T to struct{S}, - // only to S, so the method set of struct{S} is unwanted, - // so set 'skip' flag during recursion. - visit(T.Underlying(), true) // skip the unnamed type - - case *types.Array: - visit(T.Elem(), false) - - case *types.Struct: - for i, n := 0, T.NumFields(); i < n; i++ { - // TODO(adonovan): document whether or not - // it is safe to skip non-exported fields. - visit(T.Field(i).Type(), false) - } - - case *types.Tuple: - for i, n := 0, T.Len(); i < n; i++ { - visit(T.At(i).Type(), false) - } - - case *types.TypeParam, *types.Union: - // forEachReachable must not be called on parameterized types. - panic(T) - - default: - panic(T) - } + prog.makeInterfaceTypesMu.Lock() + defer prog.makeInterfaceTypesMu.Unlock() + + // Compute the derived types on demand, since many SSA clients + // never call RuntimeTypes, and those that do typically call + // it once (often within ssautil.AllFunctions, which will + // eventually not use it; see Go issue #69291.) This + // eliminates the need to eagerly compute all the element + // types during SSA building. + var runtimeTypes []types.Type + add := func(t types.Type) { runtimeTypes = append(runtimeTypes, t) } + var set typeutil.Map // for de-duping identical types + for t := range prog.makeInterfaceTypes { + typesinternal.ForEachElement(&set, &prog.MethodSets, t, add) } - visit(T, false) + + return runtimeTypes } diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 1231afd9e0c..df673e2fc99 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -37,8 +37,9 @@ type Program struct { hasParamsMu sync.Mutex hasParams typeparams.Free - runtimeTypesMu sync.Mutex - runtimeTypes typeutil.Map // set of runtime types (from MakeInterface) + // set of concrete types used as MakeInterface operands + makeInterfaceTypesMu sync.Mutex + makeInterfaceTypes map[types.Type]unit // (may contain redundant identical types) // objectMethods is a memoization of objectMethod // to avoid creation of duplicate methods from type information. diff --git a/internal/typesinternal/element.go b/internal/typesinternal/element.go new file mode 100644 index 00000000000..1512039bb00 --- /dev/null +++ b/internal/typesinternal/element.go @@ -0,0 +1,134 @@ +// Copyright 2024 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 typesinternal + +import ( + "fmt" + "go/types" + + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" +) + +// ForEachElement calls f for type T and each type reachable from its +// type through reflection. It does this by recursively stripping off +// type constructors; in addition, for each named type N, the type *N +// is added to the result as it may have additional methods. +// +// The caller must provide an initially empty set used to de-duplicate +// identical types, potentially across multiple calls to ForEachElement. +// (Its final value holds all the elements seen, matching the arguments +// passed to f.) +// +// TODO(adonovan): share/harmonize with go/callgraph/rta. +func ForEachElement(rtypes *typeutil.Map, msets *typeutil.MethodSetCache, T types.Type, f func(types.Type)) { + var visit func(T types.Type, skip bool) + visit = func(T types.Type, skip bool) { + if !skip { + if seen, _ := rtypes.Set(T, true).(bool); seen { + return // de-dup + } + + f(T) // notify caller of new element type + } + + // Recursion over signatures of each method. + tmset := msets.MethodSet(T) + for i := 0; i < tmset.Len(); i++ { + sig := tmset.At(i).Type().(*types.Signature) + // It is tempting to call visit(sig, false) + // but, as noted in golang.org/cl/65450043, + // the Signature.Recv field is ignored by + // types.Identical and typeutil.Map, which + // is confusing at best. + // + // More importantly, the true signature rtype + // reachable from a method using reflection + // has no receiver but an extra ordinary parameter. + // For the Read method of io.Reader we want: + // func(Reader, []byte) (int, error) + // but here sig is: + // func([]byte) (int, error) + // with .Recv = Reader (though it is hard to + // notice because it doesn't affect Signature.String + // or types.Identical). + // + // TODO(adonovan): construct and visit the correct + // non-method signature with an extra parameter + // (though since unnamed func types have no methods + // there is essentially no actual demand for this). + // + // TODO(adonovan): document whether or not it is + // safe to skip non-exported methods (as RTA does). + visit(sig.Params(), true) // skip the Tuple + visit(sig.Results(), true) // skip the Tuple + } + + switch T := T.(type) { + case *aliases.Alias: + visit(aliases.Unalias(T), skip) // emulates the pre-Alias behavior + + case *types.Basic: + // nop + + case *types.Interface: + // nop---handled by recursion over method set. + + case *types.Pointer: + visit(T.Elem(), false) + + case *types.Slice: + visit(T.Elem(), false) + + case *types.Chan: + visit(T.Elem(), false) + + case *types.Map: + visit(T.Key(), false) + visit(T.Elem(), false) + + case *types.Signature: + if T.Recv() != nil { + panic(fmt.Sprintf("Signature %s has Recv %s", T, T.Recv())) + } + visit(T.Params(), true) // skip the Tuple + visit(T.Results(), true) // skip the Tuple + + case *types.Named: + // A pointer-to-named type can be derived from a named + // type via reflection. It may have methods too. + visit(types.NewPointer(T), false) + + // Consider 'type T struct{S}' where S has methods. + // Reflection provides no way to get from T to struct{S}, + // only to S, so the method set of struct{S} is unwanted, + // so set 'skip' flag during recursion. + visit(T.Underlying(), true) // skip the unnamed type + + case *types.Array: + visit(T.Elem(), false) + + case *types.Struct: + for i, n := 0, T.NumFields(); i < n; i++ { + // TODO(adonovan): document whether or not + // it is safe to skip non-exported fields. + visit(T.Field(i).Type(), false) + } + + case *types.Tuple: + for i, n := 0, T.Len(); i < n; i++ { + visit(T.At(i).Type(), false) + } + + case *types.TypeParam, *types.Union: + // forEachReachable must not be called on parameterized types. + panic(T) + + default: + panic(T) + } + } + visit(T, false) +} diff --git a/internal/typesinternal/element_test.go b/internal/typesinternal/element_test.go new file mode 100644 index 00000000000..63d4d8d3c37 --- /dev/null +++ b/internal/typesinternal/element_test.go @@ -0,0 +1,154 @@ +// Copyright 2024 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 typesinternal_test + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "strings" + "testing" + + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" +) + +const elementSrc = ` +package p + +type A = int + +type B = *map[chan int][]func() [2]bool + +type C = T + +type T struct{ x int } +func (T) method() uint +func (*T) ptrmethod() complex128 + +type D = A + +type E = struct{ x int } + +type F = func(int8, int16) (int32, int64) + +type G = struct { U } + +type U struct{} +func (U) method() uint32 + +` + +func TestForEachElement(t *testing.T) { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "a.go", elementSrc, 0) + if err != nil { + t.Fatal(err) // parse error + } + var config types.Config + pkg, err := config.Check(f.Name.Name, fset, []*ast.File{f}, nil) + if err != nil { + t.Fatal(err) // type error + } + + tests := []struct { + name string // name of a type alias whose RHS type's elements to compute + want []string // strings of types that are/are not elements (! => not) + }{ + // simple type + {"A", []string{"int"}}, + + // compound type + {"B", []string{ + "*map[chan int][]func() [2]bool", + "map[chan int][]func() [2]bool", + "chan int", + "int", + "[]func() [2]bool", + "func() [2]bool", + "[2]bool", + "bool", + }}, + + // defined struct type with methods, incl. pointer methods. + // Observe that it descends into the field type, but + // the result does not include the struct type itself. + // (This follows the Go toolchain behavior , and finesses the need + // to create wrapper methods for that struct type.) + {"C", []string{"T", "*T", "int", "uint", "complex128", "!struct{x int}"}}, + + // alias type + {"D", []string{"int"}}, + + // struct type not beneath a defined type + {"E", []string{"struct{x int}", "int"}}, + + // signature types: the params/results tuples + // are traversed but not included. + {"F", []string{"func(int8, int16) (int32, int64)", + "int8", "int16", "int32", "int64"}}, + + // struct with embedded field that has methods + {"G", []string{"*U", "struct{U}", "uint32", "U"}}, + } + var msets typeutil.MethodSetCache + for _, test := range tests { + tname, ok := pkg.Scope().Lookup(test.name).(*types.TypeName) + if !ok { + t.Errorf("no such type %q", test.name) + continue + } + T := aliases.Unalias(tname.Type()) + + toStr := func(T types.Type) string { + return types.TypeString(T, func(*types.Package) string { return "" }) + } + + got := make(map[string]bool) + set := new(typeutil.Map) // for de-duping + set2 := new(typeutil.Map) // for consistency check + typesinternal.ForEachElement(set, &msets, T, func(elem types.Type) { + got[toStr(elem)] = true + set2.Set(elem, true) + }) + + // Assert that set==set2, meaning f(x) was + // called for each x in the de-duping map. + if set.Len() != set2.Len() { + t.Errorf("ForEachElement called f %d times yet de-dup set has %d elements", + set2.Len(), set.Len()) + } else { + set.Iterate(func(key types.Type, _ any) { + if set2.At(key) == nil { + t.Errorf("ForEachElement did not call f(%v)", key) + } + }) + } + + // Assert than all expected (and no unexpected) elements were found. + fail := false + for _, typstr := range test.want { + found := got[typstr] + typstr, unwanted := strings.CutPrefix(typstr, "!") + if found && unwanted { + fail = true + t.Errorf("ForEachElement(%s): unwanted element %q", T, typstr) + } else if !found && !unwanted { + fail = true + t.Errorf("ForEachElement(%s): element %q not found", T, typstr) + } + } + if fail { + for k := range got { + t.Logf("got element: %s", k) + } + // TODO(adonovan): use this when go1.23 is assured: + // t.Logf("got elements:\n%s", + // strings.Join(slices.Sorted(maps.Keys(got)), "\n")) + } + } +} From 8ba91691647756357cce2b7fa3dcf4051528cfc4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 9 Sep 2024 17:39:08 -0400 Subject: [PATCH 008/102] gopls/internal/golang: Highlight: work around go/types bug go/types sometimes fails to annotate type information onto nested composite literals when there is a type error (#69092). This CL adds a workaround to one particularly vulnerable place in gopls that crashes when this happens. (There are potentially many others.) + test Fixes golang/go#68918 Change-Id: I73e8e1dd8eb8965bde44d8ee3672a50ac362af52 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612042 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/golang/highlight.go | 5 +++++ .../test/marker/testdata/highlight/issue68918.txt | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 gopls/internal/test/marker/testdata/highlight/issue68918.txt diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index 5ad0a61d0e1..f53e73f3053 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go @@ -558,6 +558,11 @@ func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result highlightWriteInExpr(n.Chan) case *ast.CompositeLit: t := info.TypeOf(n) + // Every expression should have a type; + // work around https://github.com/golang/go/issues/69092. + if t == nil { + t = types.Typ[types.Invalid] + } if ptr, ok := t.Underlying().(*types.Pointer); ok { t = ptr.Elem() } diff --git a/gopls/internal/test/marker/testdata/highlight/issue68918.txt b/gopls/internal/test/marker/testdata/highlight/issue68918.txt new file mode 100644 index 00000000000..ff2afc18f07 --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/issue68918.txt @@ -0,0 +1,9 @@ +Regression test for https://github.com/golang/go/issues/68918: +crash due to missing type information in CompositeLit. + +-- a.go -- +package a + +var _ = T{{ x }} //@hiloc(x, "x", text), diag("T", re"undefined"), diag("{ ", re"missing type") + +//@highlight(x, x) From dc4c52551c5c99d3ee45dacb91f903c6affd8013 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Mon, 15 Jul 2024 18:03:06 -0500 Subject: [PATCH 009/102] gopls/internal: test discovery Implements test discovery. Tests are discovered as part of the type checking process, at the same time as method sets and xrefs, and cached. Does not implement the Modules command. Adds static detection of simple subtests. This provides a framework for static analysis of subtests but intentionally does not support more than the most trivial case in order to minimize the complexity of this CL. Fixes golang/go#59445. Updates golang/go#59445, golang/vscode-go#1602, golang/vscode-go#2445. Change-Id: Ief497977da09a1e07831e6c5f3b7d28d6874fd9f Reviewed-on: https://go-review.googlesource.com/c/tools/+/548675 Reviewed-by: Alan Donovan Reviewed-by: Robert Findley Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/check.go | 1 + gopls/internal/cache/package.go | 11 + gopls/internal/cache/snapshot.go | 28 ++ gopls/internal/cache/testfuncs/match.go | 116 ++++++ gopls/internal/cache/testfuncs/tests.go | 324 +++++++++++++++ .../protocol/command/commandmeta/meta.go | 6 + gopls/internal/protocol/command/interface.go | 6 +- gopls/internal/server/command.go | 109 +++-- .../test/integration/bench/tests_test.go | 89 +++++ .../integration/workspace/packages_test.go | 376 +++++++++++++++++- gopls/internal/test/integration/wrappers.go | 15 + 11 files changed, 1025 insertions(+), 56 deletions(-) create mode 100644 gopls/internal/cache/testfuncs/match.go create mode 100644 gopls/internal/cache/testfuncs/tests.go create mode 100644 gopls/internal/test/integration/bench/tests_test.go diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index a0fee068327..68bf64de114 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -601,6 +601,7 @@ func storePackageResults(ctx context.Context, ph *packageHandle, p *Package) { toCache := map[string][]byte{ xrefsKind: p.pkg.xrefs(), methodSetsKind: p.pkg.methodsets().Encode(), + testsKind: p.pkg.tests().Encode(), diagnosticsKind: encodeDiagnostics(p.pkg.diagnostics), } diff --git a/gopls/internal/cache/package.go b/gopls/internal/cache/package.go index e2555c8ed98..5c0da7e6af0 100644 --- a/gopls/internal/cache/package.go +++ b/gopls/internal/cache/package.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/testfuncs" "golang.org/x/tools/gopls/internal/cache/xrefs" "golang.org/x/tools/gopls/internal/protocol" ) @@ -60,6 +61,9 @@ type syntaxPackage struct { methodsetsOnce sync.Once _methodsets *methodsets.Index // only used by the methodsets method + + testsOnce sync.Once + _tests *testfuncs.Index // only used by the tests method } func (p *syntaxPackage) xrefs() []byte { @@ -76,6 +80,13 @@ func (p *syntaxPackage) methodsets() *methodsets.Index { return p._methodsets } +func (p *syntaxPackage) tests() *testfuncs.Index { + p.testsOnce.Do(func() { + p._tests = testfuncs.NewIndex(p.compiledGoFiles, p.typesInfo) + }) + return p._tests +} + func (p *Package) String() string { return string(p.metadata.ID) } func (p *Package) Metadata() *metadata.Package { return p.metadata } diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index c95fa1fdcb5..5d3ced1244d 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -31,6 +31,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/methodsets" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/cache/testfuncs" "golang.org/x/tools/gopls/internal/cache/typerefs" "golang.org/x/tools/gopls/internal/cache/xrefs" "golang.org/x/tools/gopls/internal/file" @@ -572,6 +573,7 @@ func (s *Snapshot) Overlays() []*overlay { const ( xrefsKind = "xrefs" methodSetsKind = "methodsets" + testsKind = "tests" exportDataKind = "export" diagnosticsKind = "diagnostics" typerefsKind = "typerefs" @@ -673,6 +675,32 @@ func (s *Snapshot) MethodSets(ctx context.Context, ids ...PackageID) ([]*methods return indexes, s.forEachPackage(ctx, ids, pre, post) } +// Tests returns test-set indexes for the specified packages. There is a +// one-to-one correspondence between ID and Index. +// +// If these indexes cannot be loaded from cache, the requested packages may be +// type-checked. +func (s *Snapshot) Tests(ctx context.Context, ids ...PackageID) ([]*testfuncs.Index, error) { + ctx, done := event.Start(ctx, "cache.snapshot.Tests") + defer done() + + indexes := make([]*testfuncs.Index, len(ids)) + pre := func(i int, ph *packageHandle) bool { + data, err := filecache.Get(testsKind, ph.key) + if err == nil { // hit + indexes[i] = testfuncs.Decode(data) + return false + } else if err != filecache.ErrNotFound { + event.Error(ctx, "reading tests from filecache", err) + } + return true + } + post := func(i int, pkg *Package) { + indexes[i] = pkg.pkg.tests() + } + return indexes, s.forEachPackage(ctx, ids, pre, post) +} + // MetadataForFile returns a new slice containing metadata for each // package containing the Go file identified by uri, ordered by the // number of CompiledGoFiles (i.e. "narrowest" to "widest" package), diff --git a/gopls/internal/cache/testfuncs/match.go b/gopls/internal/cache/testfuncs/match.go new file mode 100644 index 00000000000..a7b5cb7dd58 --- /dev/null +++ b/gopls/internal/cache/testfuncs/match.go @@ -0,0 +1,116 @@ +// Copyright 2024 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 testfuncs + +import ( + "fmt" + "strconv" + "strings" +) + +// The functions in this file are copies of those from the testing package. +// +// https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/testing/match.go + +// uniqueName creates a unique name for the given parent and subname by affixing +// it with one or more counts, if necessary. +func (b *indexBuilder) uniqueName(parent, subname string) string { + base := parent + "/" + subname + + for { + n := b.subNames[base] + if n < 0 { + panic("subtest count overflow") + } + b.subNames[base] = n + 1 + + if n == 0 && subname != "" { + prefix, nn := parseSubtestNumber(base) + if len(prefix) < len(base) && nn < b.subNames[prefix] { + // This test is explicitly named like "parent/subname#NN", + // and #NN was already used for the NNth occurrence of "parent/subname". + // Loop to add a disambiguating suffix. + continue + } + return base + } + + name := fmt.Sprintf("%s#%02d", base, n) + if b.subNames[name] != 0 { + // This is the nth occurrence of base, but the name "parent/subname#NN" + // collides with the first occurrence of a subtest *explicitly* named + // "parent/subname#NN". Try the next number. + continue + } + + return name + } +} + +// parseSubtestNumber splits a subtest name into a "#%02d"-formatted int +// suffix (if present), and a prefix preceding that suffix (always). +func parseSubtestNumber(s string) (prefix string, nn int) { + i := strings.LastIndex(s, "#") + if i < 0 { + return s, 0 + } + + prefix, suffix := s[:i], s[i+1:] + if len(suffix) < 2 || (len(suffix) > 2 && suffix[0] == '0') { + // Even if suffix is numeric, it is not a possible output of a "%02" format + // string: it has either too few digits or too many leading zeroes. + return s, 0 + } + if suffix == "00" { + if !strings.HasSuffix(prefix, "/") { + // We only use "#00" as a suffix for subtests named with the empty + // string — it isn't a valid suffix if the subtest name is non-empty. + return s, 0 + } + } + + n, err := strconv.ParseInt(suffix, 10, 32) + if err != nil || n < 0 { + return s, 0 + } + return prefix, int(n) +} + +// rewrite rewrites a subname to having only printable characters and no white +// space. +func rewrite(s string) string { + b := []byte{} + for _, r := range s { + switch { + case isSpace(r): + b = append(b, '_') + case !strconv.IsPrint(r): + s := strconv.QuoteRune(r) + b = append(b, s[1:len(s)-1]...) + default: + b = append(b, string(r)...) + } + } + return string(b) +} + +func isSpace(r rune) bool { + if r < 0x2000 { + switch r { + // Note: not the same as Unicode Z class. + case '\t', '\n', '\v', '\f', '\r', ' ', 0x85, 0xA0, 0x1680: + return true + } + } else { + if r <= 0x200a { + return true + } + switch r { + case 0x2028, 0x2029, 0x202f, 0x205f, 0x3000: + return true + } + } + return false +} diff --git a/gopls/internal/cache/testfuncs/tests.go b/gopls/internal/cache/testfuncs/tests.go new file mode 100644 index 00000000000..cfef3c54164 --- /dev/null +++ b/gopls/internal/cache/testfuncs/tests.go @@ -0,0 +1,324 @@ +// Copyright 2024 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 testfuncs + +import ( + "go/ast" + "go/constant" + "go/types" + "regexp" + "strings" + + "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/frob" + "golang.org/x/tools/gopls/internal/util/safetoken" +) + +// An Index records the test set of a package. +type Index struct { + pkg gobPackage +} + +// Decode decodes the given gob-encoded data as an Index. +func Decode(data []byte) *Index { + var pkg gobPackage + packageCodec.Decode(data, &pkg) + return &Index{pkg} +} + +// Encode encodes the receiver as gob-encoded data. +func (index *Index) Encode() []byte { + return packageCodec.Encode(index.pkg) +} + +func (index *Index) All() []Result { + var results []Result + for _, file := range index.pkg.Files { + for _, test := range file.Tests { + results = append(results, test.result()) + } + } + return results +} + +// A Result reports a test function +type Result struct { + Location protocol.Location // location of the test + Name string // name of the test +} + +// NewIndex returns a new index of method-set information for all +// package-level types in the specified package. +func NewIndex(files []*parsego.File, info *types.Info) *Index { + b := &indexBuilder{ + fileIndex: make(map[protocol.DocumentURI]int), + subNames: make(map[string]int), + } + return b.build(files, info) +} + +// build adds to the index all tests of the specified package. +func (b *indexBuilder) build(files []*parsego.File, info *types.Info) *Index { + for _, file := range files { + if !strings.HasSuffix(file.Tok.Name(), "_test.go") { + continue + } + + for _, decl := range file.File.Decls { + decl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + obj, ok := info.ObjectOf(decl.Name).(*types.Func) + if !ok || !obj.Exported() { + continue + } + + // error.Error has empty Position, PkgPath, and ObjectPath. + if obj.Pkg() == nil { + continue + } + + isTest, isExample := isTestOrExample(obj) + if !isTest && !isExample { + continue + } + + var t gobTest + t.Name = decl.Name.Name + t.Location.URI = file.URI + t.Location.Range, _ = file.NodeRange(decl) + + i, ok := b.fileIndex[t.Location.URI] + if !ok { + i = len(b.Files) + b.Files = append(b.Files, gobFile{}) + b.fileIndex[t.Location.URI] = i + } + + b.Files[i].Tests = append(b.Files[i].Tests, t) + + // Check for subtests + if isTest { + b.Files[i].Tests = append(b.Files[i].Tests, b.findSubtests(t, decl.Type, decl.Body, file, files, info)...) + } + } + } + + return &Index{pkg: b.gobPackage} +} + +func (b *indexBuilder) findSubtests(parent gobTest, typ *ast.FuncType, body *ast.BlockStmt, file *parsego.File, files []*parsego.File, info *types.Info) []gobTest { + if body == nil { + return nil + } + + // If the [testing.T] parameter is unnamed, the func cannot call + // [testing.T.Run] and thus cannot create any subtests + if len(typ.Params.List[0].Names) == 0 { + return nil + } + + // This "can't fail" because testKind should guarantee that the function has + // one parameter and the check above guarantees that parameter is named + param := info.ObjectOf(typ.Params.List[0].Names[0]) + + // Find statements of form t.Run(name, func(...) {...}) where t is the + // parameter of the enclosing test function. + var tests []gobTest + for _, stmt := range body.List { + expr, ok := stmt.(*ast.ExprStmt) + if !ok { + continue + } + + call, ok := expr.X.(*ast.CallExpr) + if !ok || len(call.Args) != 2 { + continue + } + fun, ok := call.Fun.(*ast.SelectorExpr) + if !ok || fun.Sel.Name != "Run" { + continue + } + recv, ok := fun.X.(*ast.Ident) + if !ok || info.ObjectOf(recv) != param { + continue + } + + sig, ok := info.TypeOf(call.Args[1]).(*types.Signature) + if !ok { + continue + } + if _, ok := testKind(sig); !ok { + continue // subtest has wrong signature + } + + val := info.Types[call.Args[0]].Value + if val == nil || val.Kind() != constant.String { + continue + } + + var t gobTest + t.Name = b.uniqueName(parent.Name, rewrite(constant.StringVal(val))) + t.Location.URI = file.URI + t.Location.Range, _ = file.NodeRange(call) + tests = append(tests, t) + + if typ, body := findFunc(files, info, body, call.Args[1]); typ != nil { + tests = append(tests, b.findSubtests(t, typ, body, file, files, info)...) + } + } + return tests +} + +// findFunc finds the type and body of the given expr, which may be a function +// literal or reference to a declared function. +// +// If no function is found, findFunc returns (nil, nil). +func findFunc(files []*parsego.File, info *types.Info, body *ast.BlockStmt, expr ast.Expr) (*ast.FuncType, *ast.BlockStmt) { + var obj types.Object + switch arg := expr.(type) { + case *ast.FuncLit: + return arg.Type, arg.Body + + case *ast.Ident: + obj = info.ObjectOf(arg) + if obj == nil { + return nil, nil + } + + case *ast.SelectorExpr: + // Look for methods within the current package. We will not handle + // imported functions and methods for now, as that would require access + // to the source of other packages and would be substantially more + // complex. However, those cases should be rare. + sel, ok := info.Selections[arg] + if !ok { + return nil, nil + } + obj = sel.Obj() + + default: + return nil, nil + } + + if v, ok := obj.(*types.Var); ok { + // TODO: Handle vars. This could handled by walking over the body (and + // the file), but that doesn't account for assignment. If the variable + // is assigned multiple times, we could easily get the wrong one. + _, _ = v, body + return nil, nil + } + + for _, file := range files { + // Skip files that don't contain the object (there should only be a + // single file that _does_ contain it) + if _, err := safetoken.Offset(file.Tok, obj.Pos()); err != nil { + continue + } + + for _, decl := range file.File.Decls { + decl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + + if info.ObjectOf(decl.Name) == obj { + return decl.Type, decl.Body + } + } + } + return nil, nil +} + +var ( + reTest = regexp.MustCompile(`^Test([A-Z]|$)`) + reBenchmark = regexp.MustCompile(`^Benchmark([A-Z]|$)`) + reFuzz = regexp.MustCompile(`^Fuzz([A-Z]|$)`) + reExample = regexp.MustCompile(`^Example([A-Z]|$)`) +) + +// isTestOrExample reports whether the given func is a testing func or an +// example func (or neither). isTestOrExample returns (true, false) for testing +// funcs, (false, true) for example funcs, and (false, false) otherwise. +func isTestOrExample(fn *types.Func) (isTest, isExample bool) { + sig := fn.Type().(*types.Signature) + if sig.Params().Len() == 0 && + sig.Results().Len() == 0 { + return false, reExample.MatchString(fn.Name()) + } + + kind, ok := testKind(sig) + if !ok { + return false, false + } + switch kind.Name() { + case "T": + return reTest.MatchString(fn.Name()), false + case "B": + return reBenchmark.MatchString(fn.Name()), false + case "F": + return reFuzz.MatchString(fn.Name()), false + default: + return false, false // "can't happen" (see testKind) + } +} + +// testKind returns the parameter type TypeName of a test, benchmark, or fuzz +// function (one of testing.[TBF]). +func testKind(sig *types.Signature) (*types.TypeName, bool) { + if sig.Params().Len() != 1 || + sig.Results().Len() != 0 { + return nil, false + } + + ptr, ok := sig.Params().At(0).Type().(*types.Pointer) + if !ok { + return nil, false + } + + named, ok := ptr.Elem().(*types.Named) + if !ok || named.Obj().Pkg().Path() != "testing" { + return nil, false + } + + switch named.Obj().Name() { + case "T", "B", "F": + return named.Obj(), true + } + return nil, false +} + +// An indexBuilder builds an index for a single package. +type indexBuilder struct { + gobPackage + fileIndex map[protocol.DocumentURI]int + subNames map[string]int +} + +// -- serial format of index -- + +// (The name says gob but in fact we use frob.) +var packageCodec = frob.CodecFor[gobPackage]() + +// A gobPackage records the test set of each package-level type for a single package. +type gobPackage struct { + Files []gobFile +} + +type gobFile struct { + Tests []gobTest +} + +// A gobTest records the name, type, and position of a single test. +type gobTest struct { + Location protocol.Location // location of the test + Name string // name of the test +} + +func (t *gobTest) result() Result { + return Result(*t) +} diff --git a/gopls/internal/protocol/command/commandmeta/meta.go b/gopls/internal/protocol/command/commandmeta/meta.go index 0ef80b72f02..7cc4786f549 100644 --- a/gopls/internal/protocol/command/commandmeta/meta.go +++ b/gopls/internal/protocol/command/commandmeta/meta.go @@ -146,6 +146,12 @@ func (l *fieldLoader) loadField(pkg *packages.Package, obj *types.Var, doc, tag Type: obj.Type(), JSONTag: reflect.StructTag(tag).Get("json"), } + + // This must be done here to handle nested types, such as: + // + // type Test struct { Subtests []Test } + l.loaded[obj] = fld + under := fld.Type.Underlying() // Quick-and-dirty handling for various underlying types. switch p := under.(type) { diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index 35e191eb413..4ea9157d7c1 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -599,7 +599,7 @@ type PackagesArgs struct { // the result may describe any of them. Files []protocol.DocumentURI - // Enumerate all packages under the directry loadable with + // Enumerate all packages under the directory loadable with // the ... pattern. // The search does not cross the module boundaries and // does not return packages that are not yet loaded. @@ -637,6 +637,8 @@ type Package struct { // Module path. Empty if the package doesn't // belong to any module. ModulePath string + // q in a "p [q.test]" package. + ForTest string // Note: the result does not include the directory name // of the package because mapping between a package and @@ -683,7 +685,7 @@ type TestCase struct { // analysis; if so, it should aim to simulate the actual computed // name of the test, including any disambiguating suffix such as "#01". // To run only this test, clients need to compute the -run, -bench, -fuzz - // flag values by first splitting the Name with “/” and + // flag values by first splitting the Name with "/" and // quoting each element with "^" + regexp.QuoteMeta(Name) + "$". // e.g. TestToplevel/Inner.Subtest → -run=^TestToplevel$/^Inner\.Subtest$ Name string diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 98e4bf92e32..85e544714dc 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -35,6 +35,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" + "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/gopls/internal/vulncheck/scan" "golang.org/x/tools/internal/diff" @@ -144,18 +145,20 @@ func (h *commandHandler) Modules(ctx context.Context, args command.ModulesArgs) } func (h *commandHandler) Packages(ctx context.Context, args command.PackagesArgs) (command.PackagesResult, error) { - wantTests := args.Mode&command.NeedTests != 0 - result := command.PackagesResult{ - Module: make(map[string]command.Module), + // Convert file arguments into directories + dirs := make([]protocol.DocumentURI, len(args.Files)) + for i, file := range args.Files { + if filepath.Ext(file.Path()) == ".go" { + dirs[i] = file.Dir() + } else { + dirs[i] = file + } } keepPackage := func(pkg *metadata.Package) bool { for _, file := range pkg.GoFiles { - for _, arg := range args.Files { - if file == arg || file.Dir() == arg { - return true - } - if args.Recursive && arg.Encloses(file) { + for _, dir := range dirs { + if file.Dir() == dir || args.Recursive && dir.Encloses(file) { return true } } @@ -163,27 +166,8 @@ func (h *commandHandler) Packages(ctx context.Context, args command.PackagesArgs return false } - buildPackage := func(snapshot *cache.Snapshot, meta *metadata.Package) (command.Package, command.Module) { - if wantTests { - // These will be used in the next CL to query tests - _, _ = ctx, snapshot - panic("unimplemented") - } - - pkg := command.Package{ - Path: string(meta.PkgPath), - } - if meta.Module == nil { - return pkg, command.Module{} - } - - mod := command.Module{ - Path: meta.Module.Path, - Version: meta.Module.Version, - GoMod: protocol.URIFromPath(meta.Module.GoMod), - } - pkg.ModulePath = mod.Path - return pkg, mod + result := command.PackagesResult{ + Module: make(map[string]command.Module), } err := h.run(ctx, commandConfig{ @@ -201,20 +185,67 @@ func (h *commandHandler) Packages(ctx context.Context, args command.PackagesArgs return err } + // Filter out unwanted packages + metas = slices.DeleteFunc(metas, func(meta *metadata.Package) bool { + return meta.IsIntermediateTestVariant() || + !keepPackage(meta) + }) + + start := len(result.Packages) for _, meta := range metas { - if meta.IsIntermediateTestVariant() { - continue - } - if !keepPackage(meta) { - continue + var mod command.Module + if meta.Module != nil { + mod = command.Module{ + Path: meta.Module.Path, + Version: meta.Module.Version, + GoMod: protocol.URIFromPath(meta.Module.GoMod), + } + result.Module[mod.Path] = mod // Overwriting is ok } - pkg, mod := buildPackage(snapshot, meta) - result.Packages = append(result.Packages, pkg) + result.Packages = append(result.Packages, command.Package{ + Path: string(meta.PkgPath), + ForTest: string(meta.ForTest), + ModulePath: mod.Path, + }) + } + + if args.Mode&command.NeedTests == 0 { + continue + } + + // Make a single request to the index (per snapshot) to minimize the + // performance hit + var ids []cache.PackageID + for _, meta := range metas { + ids = append(ids, meta.ID) + } + + allTests, err := snapshot.Tests(ctx, ids...) + if err != nil { + return err + } - // Overwriting is ok - if mod.Path != "" { - result.Module[mod.Path] = mod + for i, tests := range allTests { + pkg := &result.Packages[start+i] + fileByPath := map[protocol.DocumentURI]*command.TestFile{} + for _, test := range tests.All() { + test := command.TestCase{ + Name: test.Name, + Loc: test.Location, + } + + file, ok := fileByPath[test.Loc.URI] + if !ok { + f := command.TestFile{ + URI: test.Loc.URI, + } + i := len(pkg.TestFiles) + pkg.TestFiles = append(pkg.TestFiles, f) + file = &pkg.TestFiles[i] + fileByPath[test.Loc.URI] = file + } + file.Tests = append(file.Tests, test) } } } diff --git a/gopls/internal/test/integration/bench/tests_test.go b/gopls/internal/test/integration/bench/tests_test.go new file mode 100644 index 00000000000..3c330461e6b --- /dev/null +++ b/gopls/internal/test/integration/bench/tests_test.go @@ -0,0 +1,89 @@ +// Copyright 2023 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 bench + +import ( + "encoding/json" + "testing" + + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" + "golang.org/x/tools/gopls/internal/test/integration" +) + +func BenchmarkPackagesCommand(b *testing.B) { + tests := []struct { + repo string + files []string + recurse bool + }{ + {"tools", []string{"internal/lsp/debounce_test.go"}, false}, + } + for _, test := range tests { + b.Run(test.repo, func(b *testing.B) { + args := command.PackagesArgs{ + Mode: command.NeedTests, + } + + env := getRepo(b, test.repo).sharedEnv(b) + for _, file := range test.files { + env.OpenFile(file) + defer closeBuffer(b, env, file) + args.Files = append(args.Files, env.Editor.DocumentURI(file)) + } + env.AfterChange() + + result := executePackagesCmd(b, env, args) // pre-warm + + // sanity check JSON {en,de}coding + var pkgs command.PackagesResult + data, err := json.Marshal(result) + if err != nil { + b.Fatal(err) + } + err = json.Unmarshal(data, &pkgs) + if err != nil { + b.Fatal(err) + } + var haveTest bool + for _, pkg := range pkgs.Packages { + for _, file := range pkg.TestFiles { + if len(file.Tests) > 0 { + haveTest = true + break + } + } + } + if !haveTest { + b.Fatalf("Expected tests") + } + + b.ResetTimer() + + if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "packages")); stopAndRecord != nil { + defer stopAndRecord() + } + + for i := 0; i < b.N; i++ { + executePackagesCmd(b, env, args) + } + }) + } +} + +func executePackagesCmd(t testing.TB, env *integration.Env, args command.PackagesArgs) any { + t.Helper() + cmd, err := command.NewPackagesCommand("Packages", args) + if err != nil { + t.Fatal(err) + } + result, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{ + Command: command.Packages.String(), + Arguments: cmd.Arguments, + }) + if err != nil { + t.Fatal(err) + } + return result +} diff --git a/gopls/internal/test/integration/workspace/packages_test.go b/gopls/internal/test/integration/workspace/packages_test.go index ebeb518c644..0a5b445d1e5 100644 --- a/gopls/internal/test/integration/workspace/packages_test.go +++ b/gopls/internal/test/integration/workspace/packages_test.go @@ -16,7 +16,7 @@ import ( ) func TestPackages(t *testing.T) { - const goModView = ` + const files = ` -- go.mod -- module foo @@ -37,8 +37,8 @@ func Baz() ` t.Run("file", func(t *testing.T) { - Run(t, goModView, func(t *testing.T, env *Env) { - checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("foo.go")}, false, []command.Package{ + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("foo.go")}, false, 0, []command.Package{ { Path: "foo", ModulePath: "foo", @@ -48,13 +48,13 @@ func Baz() Path: "foo", GoMod: env.Editor.DocumentURI("go.mod"), }, - }) + }, []string{}) }) }) t.Run("package", func(t *testing.T) { - Run(t, goModView, func(t *testing.T, env *Env) { - checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("bar")}, false, []command.Package{ + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("bar")}, false, 0, []command.Package{ { Path: "foo/bar", ModulePath: "foo", @@ -64,13 +64,13 @@ func Baz() Path: "foo", GoMod: env.Editor.DocumentURI("go.mod"), }, - }) + }, []string{}) }) }) t.Run("workspace", func(t *testing.T) { - Run(t, goModView, func(t *testing.T, env *Env) { - checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("")}, true, []command.Package{ + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("")}, true, 0, []command.Package{ { Path: "foo", ModulePath: "foo", @@ -84,17 +84,17 @@ func Baz() Path: "foo", GoMod: env.Editor.DocumentURI("go.mod"), }, - }) + }, []string{}) }) }) t.Run("nested module", func(t *testing.T) { - Run(t, goModView, func(t *testing.T, env *Env) { + Run(t, files, func(t *testing.T, env *Env) { // Load the nested module env.OpenFile("baz/baz.go") // Request packages using the URI of the nested module _directory_ - checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("baz")}, true, []command.Package{ + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("baz")}, true, 0, []command.Package{ { Path: "baz", ModulePath: "baz", @@ -104,15 +104,332 @@ func Baz() Path: "baz", GoMod: env.Editor.DocumentURI("baz/go.mod"), }, + }, []string{}) + }) + }) +} + +func TestPackagesWithTests(t *testing.T) { + const files = ` +-- go.mod -- +module foo + +-- foo.go -- +package foo +import "testing" +func Foo() +func TestFoo2(t *testing.T) + +-- foo_test.go -- +package foo +import "testing" +func TestFoo(t *testing.T) + +-- foo2_test.go -- +package foo_test +import "testing" +func TestBar(t *testing.T) {} + +-- baz/baz_test.go -- +package baz +import "testing" +func TestBaz(*testing.T) +func BenchmarkBaz(*testing.B) +func FuzzBaz(*testing.F) +func ExampleBaz() + +-- bat/go.mod -- +module bat + +-- bat/bat_test.go -- +package bat +import "testing" +func Test(*testing.T) +` + + t.Run("file", func(t *testing.T) { + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("foo_test.go")}, false, command.NeedTests, []command.Package{ + { + Path: "foo", + ModulePath: "foo", + }, + { + Path: "foo", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo_test.go"), + Tests: []command.TestCase{ + {Name: "TestFoo"}, + }, + }, + }, + }, + { + Path: "foo_test", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo2_test.go"), + Tests: []command.TestCase{ + {Name: "TestBar"}, + }, + }, + }, + }, + }, map[string]command.Module{ + "foo": { + Path: "foo", + GoMod: env.Editor.DocumentURI("go.mod"), + }, + }, []string{ + "func TestFoo(t *testing.T)", + "func TestBar(t *testing.T) {}", + }) + }) + }) + + t.Run("package", func(t *testing.T) { + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("baz")}, false, command.NeedTests, []command.Package{ + { + Path: "foo/baz", + ForTest: "foo/baz", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("baz/baz_test.go"), + Tests: []command.TestCase{ + {Name: "TestBaz"}, + {Name: "BenchmarkBaz"}, + {Name: "FuzzBaz"}, + {Name: "ExampleBaz"}, + }, + }, + }, + }, + }, map[string]command.Module{ + "foo": { + Path: "foo", + GoMod: env.Editor.DocumentURI("go.mod"), + }, + }, []string{ + "func TestBaz(*testing.T)", + "func BenchmarkBaz(*testing.B)", + "func FuzzBaz(*testing.F)", + "func ExampleBaz()", + }) + }) + }) + + t.Run("workspace", func(t *testing.T) { + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI(".")}, true, command.NeedTests, []command.Package{ + { + Path: "foo", + ModulePath: "foo", + }, + { + Path: "foo", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo_test.go"), + Tests: []command.TestCase{ + {Name: "TestFoo"}, + }, + }, + }, + }, + { + Path: "foo/baz", + ForTest: "foo/baz", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("baz/baz_test.go"), + Tests: []command.TestCase{ + {Name: "TestBaz"}, + {Name: "BenchmarkBaz"}, + {Name: "FuzzBaz"}, + {Name: "ExampleBaz"}, + }, + }, + }, + }, + { + Path: "foo_test", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo2_test.go"), + Tests: []command.TestCase{ + {Name: "TestBar"}, + }, + }, + }, + }, + }, map[string]command.Module{ + "foo": { + Path: "foo", + GoMod: env.Editor.DocumentURI("go.mod"), + }, + }, []string{ + "func TestFoo(t *testing.T)", + "func TestBaz(*testing.T)", + "func BenchmarkBaz(*testing.B)", + "func FuzzBaz(*testing.F)", + "func ExampleBaz()", + "func TestBar(t *testing.T) {}", + }) + }) + }) + + t.Run("nested module", func(t *testing.T) { + Run(t, files, func(t *testing.T, env *Env) { + // Load the nested module + env.OpenFile("bat/bat_test.go") + + // Request packages using the URI of the nested module _directory_ + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("bat")}, true, command.NeedTests, []command.Package{ + { + Path: "bat", + ForTest: "bat", + ModulePath: "bat", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("bat/bat_test.go"), + Tests: []command.TestCase{ + {Name: "Test"}, + }, + }, + }, + }, + }, map[string]command.Module{ + "bat": { + Path: "bat", + GoMod: env.Editor.DocumentURI("bat/go.mod"), + }, + }, []string{ + "func Test(*testing.T)", }) }) }) } -func checkPackages(t testing.TB, env *Env, files []protocol.DocumentURI, recursive bool, wantPkg []command.Package, wantModule map[string]command.Module) { +func TestPackagesWithSubtests(t *testing.T) { + const files = ` +-- go.mod -- +module foo + +-- foo_test.go -- +package foo + +import "testing" + +// Verify that examples don't break subtest detection +func ExampleFoo() {} + +func TestFoo(t *testing.T) { + t.Run("Bar", func(t *testing.T) { + t.Run("Baz", func(t *testing.T) {}) + }) + t.Run("Bar", func(t *testing.T) {}) + t.Run("Bar", func(t *testing.T) {}) + t.Run("with space", func(t *testing.T) {}) + + var x X + y := func(t *testing.T) { + t.Run("VarSub", func(t *testing.T) {}) + } + t.Run("SubtestFunc", SubtestFunc) + t.Run("SubtestMethod", x.SubtestMethod) + t.Run("SubtestVar", y) +} + +func SubtestFunc(t *testing.T) { + t.Run("FuncSub", func(t *testing.T) {}) +} + +type X int +func (X) SubtestMethod(t *testing.T) { + t.Run("MethodSub", func(t *testing.T) {}) +} +` + + Run(t, files, func(t *testing.T, env *Env) { + checkPackages(t, env, []protocol.DocumentURI{env.Editor.DocumentURI("foo_test.go")}, false, command.NeedTests, []command.Package{ + { + Path: "foo", + ForTest: "foo", + ModulePath: "foo", + TestFiles: []command.TestFile{ + { + URI: env.Editor.DocumentURI("foo_test.go"), + Tests: []command.TestCase{ + {Name: "ExampleFoo"}, + {Name: "TestFoo"}, + {Name: "TestFoo/Bar"}, + {Name: "TestFoo/Bar/Baz"}, + {Name: "TestFoo/Bar#01"}, + {Name: "TestFoo/Bar#02"}, + {Name: "TestFoo/with_space"}, + {Name: "TestFoo/SubtestFunc"}, + {Name: "TestFoo/SubtestFunc/FuncSub"}, + {Name: "TestFoo/SubtestMethod"}, + {Name: "TestFoo/SubtestMethod/MethodSub"}, + {Name: "TestFoo/SubtestVar"}, + // {Name: "TestFoo/SubtestVar/VarSub"}, // TODO + }, + }, + }, + }, + }, map[string]command.Module{ + "foo": { + Path: "foo", + GoMod: env.Editor.DocumentURI("go.mod"), + }, + }, []string{ + "func ExampleFoo() {}", + `func TestFoo(t *testing.T) { + t.Run("Bar", func(t *testing.T) { + t.Run("Baz", func(t *testing.T) {}) + }) + t.Run("Bar", func(t *testing.T) {}) + t.Run("Bar", func(t *testing.T) {}) + t.Run("with space", func(t *testing.T) {}) + + var x X + y := func(t *testing.T) { + t.Run("VarSub", func(t *testing.T) {}) + } + t.Run("SubtestFunc", SubtestFunc) + t.Run("SubtestMethod", x.SubtestMethod) + t.Run("SubtestVar", y) +}`, + "t.Run(\"Bar\", func(t *testing.T) {\n\t\tt.Run(\"Baz\", func(t *testing.T) {})\n\t})", + `t.Run("Baz", func(t *testing.T) {})`, + `t.Run("Bar", func(t *testing.T) {})`, + `t.Run("Bar", func(t *testing.T) {})`, + `t.Run("with space", func(t *testing.T) {})`, + `t.Run("SubtestFunc", SubtestFunc)`, + `t.Run("FuncSub", func(t *testing.T) {})`, + `t.Run("SubtestMethod", x.SubtestMethod)`, + `t.Run("MethodSub", func(t *testing.T) {})`, + `t.Run("SubtestVar", y)`, + }) + }) +} + +func checkPackages(t testing.TB, env *Env, files []protocol.DocumentURI, recursive bool, mode command.PackagesMode, wantPkg []command.Package, wantModule map[string]command.Module, wantSource []string) { t.Helper() - cmd, err := command.NewPackagesCommand("Packages", command.PackagesArgs{Files: files, Recursive: recursive}) + cmd, err := command.NewPackagesCommand("Packages", command.PackagesArgs{Files: files, Recursive: recursive, Mode: mode}) if err != nil { t.Fatal(err) } @@ -126,9 +443,31 @@ func checkPackages(t testing.TB, env *Env, files []protocol.DocumentURI, recursi // consistency sort.Slice(result.Packages, func(i, j int) bool { a, b := result.Packages[i], result.Packages[j] - return strings.Compare(a.Path, b.Path) < 0 + c := strings.Compare(a.Path, b.Path) + if c != 0 { + return c < 0 + } + return strings.Compare(a.ForTest, b.ForTest) < 0 }) + // Instead of testing the exact values of the test locations (which would + // make these tests significantly more trouble to maintain), verify the + // source range they refer to. + gotSource := []string{} // avoid issues with comparing null to [] + for i := range result.Packages { + pkg := &result.Packages[i] + for i := range pkg.TestFiles { + file := &pkg.TestFiles[i] + env.OpenFile(file.URI.Path()) + + for i := range file.Tests { + test := &file.Tests[i] + gotSource = append(gotSource, env.FileContentAt(test.Loc)) + test.Loc = protocol.Location{} + } + } + } + if diff := cmp.Diff(wantPkg, result.Packages); diff != "" { t.Errorf("Packages(%v) returned unexpected packages (-want +got):\n%s", files, diff) } @@ -136,4 +475,11 @@ func checkPackages(t testing.TB, env *Env, files []protocol.DocumentURI, recursi if diff := cmp.Diff(wantModule, result.Module); diff != "" { t.Errorf("Packages(%v) returned unexpected modules (-want +got):\n%s", files, diff) } + + // Don't check the source if the response is incorrect + if !t.Failed() { + if diff := cmp.Diff(wantSource, gotSource); diff != "" { + t.Errorf("Packages(%v) returned unexpected test case ranges (-want +got):\n%s", files, diff) + } + } } diff --git a/gopls/internal/test/integration/wrappers.go b/gopls/internal/test/integration/wrappers.go index 3247fac1761..56ed490c660 100644 --- a/gopls/internal/test/integration/wrappers.go +++ b/gopls/internal/test/integration/wrappers.go @@ -136,6 +136,21 @@ func (e *Env) FileContent(name string) string { return string(content) } +// FileContentAt returns the file content at the given location, using the +// file's mapper. +func (e *Env) FileContentAt(location protocol.Location) string { + e.T.Helper() + mapper, err := e.Editor.Mapper(location.URI.Path()) + if err != nil { + e.T.Fatal(err) + } + start, end, err := mapper.RangeOffsets(location.Range) + if err != nil { + e.T.Fatal(err) + } + return string(mapper.Content[start:end]) +} + // RegexpSearch returns the starting position of the first match for re in the // buffer specified by name, calling t.Fatal on any error. It first searches // for the position in open buffers, then in workspace files. From fd7ab2daaaf4b704fd477436b9c6f7732bbabbed Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 10 Sep 2024 16:30:10 +0000 Subject: [PATCH 010/102] gopls/internal/server: fix build following semantic merge conflict Fix the gopls build, which was broken by a semantic merge conflict with CL 548675 (the slices package was made obsolete after updating to Go 1.23.1). Change-Id: Ic8b82598b66c560298dd9b8f968a1087e12320f3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/611839 Reviewed-by: Hongxiang Jiang Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King --- gopls/internal/server/command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 85e544714dc..352008ac43b 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -17,6 +17,7 @@ import ( "regexp" "runtime" "runtime/pprof" + "slices" "sort" "strings" "sync" @@ -35,7 +36,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/gopls/internal/vulncheck/scan" "golang.org/x/tools/internal/diff" From b7af269531b4c4f7403fac16567925a0a2e1557b Mon Sep 17 00:00:00 2001 From: xieyuschen Date: Tue, 10 Sep 2024 13:23:31 +0000 Subject: [PATCH 011/102] go/ssa: improve sanity checking * sanity check transient fields * sanity check signature recv/params and the Params field * fix error checking 'err != err' Change-Id: I5a9df1d59176c8ddc5521849490518eb277a51fa GitHub-Last-Rev: c96247666108a5350edc8275a75e9c8375b31fb2 GitHub-Pull-Request: golang/tools#514 Reviewed-on: https://go-review.googlesource.com/c/tools/+/610059 Reviewed-by: Tim King Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- go/ssa/func.go | 14 +++++++++ go/ssa/instantiate_test.go | 6 ++-- go/ssa/sanity.go | 64 +++++++++++++++++++++++++++++++------- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/go/ssa/func.go b/go/ssa/func.go index 2ed63bfd53e..010c128a9ec 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -186,6 +186,20 @@ func targetedBlock(f *Function, tok token.Token) *BasicBlock { return targetedBlock(f.parent, tok) } +// instrs returns an iterator that returns each reachable instruction of the SSA function. +// TODO: return an iter.Seq once x/tools is on 1.23 +func (f *Function) instrs() func(yield func(i Instruction) bool) { + return func(yield func(i Instruction) bool) { + for _, block := range f.Blocks { + for _, instr := range block.Instrs { + if !yield(instr) { + return + } + } + } + } +} + // addResultVar adds a result for a variable v to f.results and v to f.returnVars. func (f *Function) addResultVar(v *types.Var) { result := emitLocalVar(f, v) diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index 25f78492874..fcf682c88a7 100644 --- a/go/ssa/instantiate_test.go +++ b/go/ssa/instantiate_test.go @@ -75,7 +75,7 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) // var init$guard bool lprog, err := loadProgram(input) - if err != err { + if err != nil { t.Fatal(err) } @@ -194,7 +194,7 @@ func entry(i int, a A) int { } ` lprog, err := loadProgram(input) - if err != err { + if err != nil { t.Fatal(err) } @@ -310,7 +310,7 @@ func Foo[T any, S any](t T, s S) { } ` lprog, err := loadProgram(input) - if err != err { + if err != nil { t.Fatal(err) } diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 285cba04a9f..dfc95769bce 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -407,14 +407,45 @@ func (s *sanity) checkReferrerList(v Value) { } } +func (s *sanity) checkFunctionParams(fn *Function) { + signature := fn.Signature + params := fn.Params + + // startSigParams is the start of signature.Params() within params. + startSigParams := 0 + if signature.Recv() != nil { + startSigParams = 1 + } + + if startSigParams+signature.Params().Len() != len(params) { + s.errorf("function has %d parameters in signature but has %d after building", + startSigParams+signature.Params().Len(), len(params)) + return + } + + for i, param := range params { + var sigType types.Type + si := i - startSigParams + if si < 0 { + sigType = signature.Recv().Type() + } else { + sigType = signature.Params().At(si).Type() + } + + if !types.Identical(sigType, param.Type()) { + s.errorf("expect type %s in signature but got type %s in param %d", param.Type(), sigType, i) + } + } +} + func (s *sanity) checkFunction(fn *Function) bool { - // TODO(adonovan): check Function invariants: - // - check params match signature - // - check transient fields are nil - // - warn if any fn.Locals do not appear among block instructions. + s.fn = fn + // TODO(yuchen): fix the bug caught on fn.targets when checking transient fields + // see https://go-review.googlesource.com/c/tools/+/610059/comment/4287a123_dc6cbc44/ + + s.checkFunctionParams(fn) // TODO(taking): Sanity check origin, typeparams, and typeargs. - s.fn = fn if fn.Prog == nil { s.errorf("nil Prog") } @@ -452,7 +483,23 @@ func (s *sanity) checkFunction(fn *Function) bool { s.errorf("got fromSource=%t, hasSyntax=%t; want same values", src, syn) } } + + // Build the set of valid referrers. + s.instrs = make(map[Instruction]unit) + + // TODO: switch to range-over-func when x/tools updates to 1.23. + // instrs are the instructions that are present in the function. + fn.instrs()(func(instr Instruction) bool { + s.instrs[instr] = unit{} + return true + }) + + // Check all Locals allocations appear in the function instruction. for i, l := range fn.Locals { + if _, present := s.instrs[l]; !present { + s.warnf("function doesn't contain Local alloc %s", l.Name()) + } + if l.Parent() != fn { s.errorf("Local %s at index %d has wrong parent", l.Name(), i) } @@ -460,13 +507,6 @@ func (s *sanity) checkFunction(fn *Function) bool { s.errorf("Local %s at index %d has Heap flag set", l.Name(), i) } } - // Build the set of valid referrers. - s.instrs = make(map[Instruction]unit) - for _, b := range fn.Blocks { - for _, instr := range b.Instrs { - s.instrs[instr] = unit{} - } - } for i, p := range fn.Params { if p.Parent() != fn { s.errorf("Param %s at index %d has wrong parent", p.Name(), i) From b0f680ccb8947d4e0d40e324862ccbc683afaa20 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 10 Sep 2024 13:48:55 -0400 Subject: [PATCH 012/102] go/ssa: reenable TestStdlib It wasn't broken on the builder--only with my local go toolchain. (There was some cgo-related problem with cmd/cgo/internal/test.) At least that explains why we didn't notice the failures... This CL also adds a missing error check that made the real cause of the problem hard to spot, and improves some comments and assertions. Fixes golang/go#69287 Change-Id: Iccbe2a72770499749ca780f78e2a61d5576f613b Reviewed-on: https://go-review.googlesource.com/c/tools/+/612044 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/ssa/stdlib_test.go | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go index e56d6a98156..391240ad757 100644 --- a/go/ssa/stdlib_test.go +++ b/go/ssa/stdlib_test.go @@ -35,13 +35,18 @@ func bytesAllocated() uint64 { return stats.Alloc } -// TestStdlib loads the entire standard library and its tools. +// TestStdlib loads the entire standard library and its tools and all +// their dependencies. +// +// (As of go1.23, std is transitively closed, so adding the -deps flag +// doesn't increase its result set. The cmd pseudomodule of course +// depends on a good chunk of std, but the std+cmd set is also +// transitively closed, so long as -pgo=off.) // // Apart from a small number of internal packages that are not // returned by the 'std' query, the set is essentially transitively // closed, so marginal per-dependency costs are invisible. func TestStdlib(t *testing.T) { - t.Skip("broken; see https://go.dev/issues/69287") testLoad(t, 500, "std", "cmd") } @@ -78,6 +83,9 @@ func testLoad(t *testing.T, minPkgs int, patterns ...string) { if err != nil { t.Fatal(err) } + if packages.PrintErrors(pkgs) > 0 { + t.Fatal("there were errors loading the packages") + } t1 := time.Now() alloc1 := bytesAllocated() @@ -195,9 +203,13 @@ func srcFunctions(prog *ssa.Program, pkgs []*packages.Package) (res []*ssa.Funct if decl, ok := decl.(*ast.FuncDecl); ok { obj := pkg.TypesInfo.Defs[decl.Name].(*types.Func) if obj == nil { - panic("nil *Func") + panic("nil *types.Func: " + decl.Name.Name) + } + fn := prog.FuncValue(obj) + if fn == nil { + panic("nil *ssa.Function: " + obj.String()) } - addSrcFunc(prog.FuncValue(obj)) + addSrcFunc(fn) } } } From 288437536fb16daa8736da03ddd11eadfc571dc9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 10 Sep 2024 15:53:05 -0400 Subject: [PATCH 013/102] gopls/internal/golang: Definitions: support renaming imports in doc links This CL adds support for jumping to the definition of a doc link when the import is renamed. Before, the doc link had to use the local (renamed) name, which is unnatural; now, it can use either the local name or the package's declared name. + test Updates golang/go#61677 Change-Id: Ibbe18ab1527800c41900d42781677ad892b55cd4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612045 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Commit-Queue: Alan Donovan --- gopls/internal/golang/comment.go | 40 +++++++++++++++---- .../marker/testdata/definition/comment.txt | 13 ++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/gopls/internal/golang/comment.go b/gopls/internal/golang/comment.go index dc8c1c83f77..3a0d8153665 100644 --- a/gopls/internal/golang/comment.go +++ b/gopls/internal/golang/comment.go @@ -111,7 +111,7 @@ func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.O for _, idx := range docLinkRegex.FindAllStringSubmatchIndex(text, -1) { // The [idx[2], idx[3]) identifies the first submatch, - // which is the reference name in the doc link. + // which is the reference name in the doc link (sans '*'). // e.g. The "[fmt.Println]" reference name is "fmt.Println". if !(idx[2] <= lineOffset && lineOffset < idx[3]) { continue @@ -126,7 +126,7 @@ func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.O name = name[:i] i = strings.LastIndexByte(name, '.') } - obj := lookupObjectByName(pkg, pgf, name) + obj := lookupDocLinkSymbol(pkg, pgf, name) if obj == nil { return nil, protocol.Range{}, errNoCommentReference } @@ -141,19 +141,42 @@ func parseDocLink(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (types.O return nil, protocol.Range{}, errNoCommentReference } -func lookupObjectByName(pkg *cache.Package, pgf *parsego.File, name string) types.Object { +// lookupDocLinkSymbol returns the symbol denoted by a doc link such +// as "fmt.Println" or "bytes.Buffer.Write" in the specified file. +func lookupDocLinkSymbol(pkg *cache.Package, pgf *parsego.File, name string) types.Object { scope := pkg.Types().Scope() + + prefix, suffix, _ := strings.Cut(name, ".") + + // Try treating the prefix as a package name, + // allowing for non-renaming and renaming imports. fileScope := pkg.TypesInfo().Scopes[pgf.File] - pkgName, suffix, _ := strings.Cut(name, ".") - obj, ok := fileScope.Lookup(pkgName).(*types.PkgName) - if ok { - scope = obj.Imported().Scope() + pkgname, ok := fileScope.Lookup(prefix).(*types.PkgName) // ok => prefix is imported name + if !ok { + // Handle renaming import, e.g. + // [path.Join] after import pathpkg "path". + // (Should we look at all files of the package?) + for _, imp := range pgf.File.Imports { + pkgname2 := pkg.TypesInfo().PkgNameOf(imp) + if pkgname2 != nil && pkgname2.Imported().Name() == prefix { + pkgname = pkgname2 + break + } + } + } + if pkgname != nil { + scope = pkgname.Imported().Scope() if suffix == "" { - return obj + return pkgname // not really a valid doc link } name = suffix } + // TODO(adonovan): try searching the forward closure for packages + // that define the symbol but are not directly imported; + // see https://github.com/golang/go/issues/61677 + + // Type.Method? recv, method, ok := strings.Cut(name, ".") if ok { obj, ok := scope.Lookup(recv).(*types.TypeName) @@ -173,5 +196,6 @@ func lookupObjectByName(pkg *cache.Package, pgf *parsego.File, name string) type return nil } + // package-level symbol return scope.Lookup(name) } diff --git a/gopls/internal/test/marker/testdata/definition/comment.txt b/gopls/internal/test/marker/testdata/definition/comment.txt index ac253b27310..39c860708b8 100644 --- a/gopls/internal/test/marker/testdata/definition/comment.txt +++ b/gopls/internal/test/marker/testdata/definition/comment.txt @@ -5,10 +5,16 @@ module mod.com go 1.19 +-- path/path.go -- +package path + +func Join() //@loc(Join, "Join") + -- a.go -- package p import "strconv" //@loc(strconv, `"strconv"`) +import pathpkg "mod.com/path" const NumberBase = 10 //@loc(NumberBase, "NumberBase") @@ -19,3 +25,10 @@ func Conv(s string) int { //@loc(Conv, "Conv") i, _ := strconv.ParseInt(s, NumberBase, 64) return int(i) } + +// The declared and imported names of the package both work: +// [path.Join] //@ def("Join", Join) +// [pathpkg.Join] //@ def("Join", Join) +func _() { + pathpkg.Join() +} From 94ac686dafcbb8e58ccf13f5326bb7b4153fe867 Mon Sep 17 00:00:00 2001 From: xieyuschen Date: Tue, 10 Sep 2024 17:26:28 +0000 Subject: [PATCH 014/102] go/ssa: pop targets stack on range-over-func Pop Function.targets when building a call to a range-over-func yield function and when building the yield function. Also adds sanity checks to ensure all function transient fields are cleared. Fixes golang/go#69298 Change-Id: I38b80ce8939cf2cd6cfd0ce0c119d75356d80ebf GitHub-Last-Rev: 8c45b9c36e370bc74bdf765b0aa37b743735db8e GitHub-Pull-Request: golang/tools#516 Reviewed-on: https://go-review.googlesource.com/c/tools/+/611055 LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King Reviewed-by: Dmitri Shuralyov --- go/ssa/builder.go | 5 ++ go/ssa/interp/interp_go122_test.go | 12 ++++ .../interp/testdata/fixedbugs/issue69298.go | 31 ++++++++++ go/ssa/sanity.go | 56 ++++++++++++++++--- 4 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 go/ssa/interp/testdata/fixedbugs/issue69298.go diff --git a/go/ssa/builder.go b/go/ssa/builder.go index f1fa43c51a0..cd29fed5f46 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -2566,6 +2566,8 @@ func (b *builder) rangeFunc(fn *Function, x Value, tk, tv types.Type, rng *ast.R emitJump(fn, done) fn.currentBlock = done + // pop the stack for the range-over-func + fn.targets = fn.targets.tail } // buildYieldResume emits to fn code for how to resume execution once a call to @@ -2998,6 +3000,7 @@ func (b *builder) buildYieldFunc(fn *Function) { } } fn.targets = &targets{ + tail: fn.targets, _continue: ycont, // `break` statement targets fn.parent.targets._break. } @@ -3075,6 +3078,8 @@ func (b *builder) buildYieldFunc(fn *Function) { // unreachable. emitJump(fn, ycont) } + // pop the stack for the yield function + fn.targets = fn.targets.tail // Clean up exits and promote any unresolved exits to fn.parent. for _, e := range fn.exits { diff --git a/go/ssa/interp/interp_go122_test.go b/go/ssa/interp/interp_go122_test.go index aedb5880f3e..6e2ab801780 100644 --- a/go/ssa/interp/interp_go122_test.go +++ b/go/ssa/interp/interp_go122_test.go @@ -39,6 +39,18 @@ func TestExperimentRange(t *testing.T) { run(t, filepath.Join(cwd, "testdata", "rangeoverint.go"), goroot) } +func TestIssue69298(t *testing.T) { + testenv.NeedsGo1Point(t, 23) + + // TODO: Is cwd actually needed here? + goroot := makeGoroot(t) + cwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + run(t, filepath.Join(cwd, "testdata", "fixedbugs/issue69298.go"), goroot) +} + // TestRangeFunc tests range-over-func in a subprocess. func TestRangeFunc(t *testing.T) { testenv.NeedsGo1Point(t, 23) diff --git a/go/ssa/interp/testdata/fixedbugs/issue69298.go b/go/ssa/interp/testdata/fixedbugs/issue69298.go new file mode 100644 index 00000000000..72ea0f54647 --- /dev/null +++ b/go/ssa/interp/testdata/fixedbugs/issue69298.go @@ -0,0 +1,31 @@ +// Copyright 2024 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 main + +import ( + "fmt" +) + +type Seq[V any] func(yield func(V) bool) + +func AppendSeq[Slice ~[]E, E any](s Slice, seq Seq[E]) Slice { + for v := range seq { + s = append(s, v) + } + return s +} + +func main() { + seq := func(yield func(int) bool) { + for i := 0; i < 10; i += 2 { + if !yield(i) { + return + } + } + } + + s := AppendSeq([]int{1, 2}, seq) + fmt.Println(s) +} diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index dfc95769bce..3d82e936518 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -407,9 +407,9 @@ func (s *sanity) checkReferrerList(v Value) { } } -func (s *sanity) checkFunctionParams(fn *Function) { - signature := fn.Signature - params := fn.Params +func (s *sanity) checkFunctionParams() { + signature := s.fn.Signature + params := s.fn.Params // startSigParams is the start of signature.Params() within params. startSigParams := 0 @@ -438,12 +438,54 @@ func (s *sanity) checkFunctionParams(fn *Function) { } } +// checkTransientFields checks whether all transient fields of Function are cleared. +func (s *sanity) checkTransientFields() { + fn := s.fn + if fn.build != nil { + s.errorf("function transient field 'build' is not nil") + } + if fn.currentBlock != nil { + s.errorf("function transient field 'currentBlock' is not nil") + } + if fn.vars != nil { + s.errorf("function transient field 'vars' is not nil") + } + if fn.results != nil { + s.errorf("function transient field 'results' is not nil") + } + if fn.returnVars != nil { + s.errorf("function transient field 'returnVars' is not nil") + } + if fn.targets != nil { + s.errorf("function transient field 'targets' is not nil") + } + if fn.lblocks != nil { + s.errorf("function transient field 'lblocks' is not nil") + } + if fn.subst != nil { + s.errorf("function transient field 'subst' is not nil") + } + if fn.jump != nil { + s.errorf("function transient field 'jump' is not nil") + } + if fn.deferstack != nil { + s.errorf("function transient field 'deferstack' is not nil") + } + if fn.source != nil { + s.errorf("function transient field 'source' is not nil") + } + if fn.exits != nil { + s.errorf("function transient field 'exits' is not nil") + } + if fn.uniq != 0 { + s.errorf("function transient field 'uniq' is not zero") + } +} + func (s *sanity) checkFunction(fn *Function) bool { s.fn = fn - // TODO(yuchen): fix the bug caught on fn.targets when checking transient fields - // see https://go-review.googlesource.com/c/tools/+/610059/comment/4287a123_dc6cbc44/ - - s.checkFunctionParams(fn) + s.checkFunctionParams() + s.checkTransientFields() // TODO(taking): Sanity check origin, typeparams, and typeargs. if fn.Prog == nil { From 515711824d76668446153a24ead0753c9879ed26 Mon Sep 17 00:00:00 2001 From: Viktor Stanchev Date: Wed, 11 Sep 2024 19:10:31 +0000 Subject: [PATCH 015/102] gopls/internal/lsp/source: put testing.T/B first when extracting Put the testing.T/B second when extracting functions/methods. It's next after context.Context. Fixes golang/go#69341 Change-Id: Idcfc0e09e4174646a3f136dcc5badfda4af9938e GitHub-Last-Rev: 99de9722e6856e13dc2bc4d069cee149c62e3631 GitHub-Pull-Request: golang/tools#517 Reviewed-on: https://go-review.googlesource.com/c/tools/+/610976 Auto-Submit: Robert Findley Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI Run-TryBot: Tim King Reviewed-by: Robert Findley Auto-Submit: Tim King TryBot-Result: Gopher Robot --- gopls/internal/golang/extract.go | 14 ++++- .../testdata/codeaction/extract_method.txt | 60 +++++++++++++++---- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go index cc82a53f966..3610deeead3 100644 --- a/gopls/internal/golang/extract.go +++ b/gopls/internal/golang/extract.go @@ -651,8 +651,12 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte }, nil } -// isSelector reports if e is the selector expr , . +// isSelector reports if e is the selector expr , . It works for pointer and non-pointer selector expressions. func isSelector(e ast.Expr, x, sel string) bool { + unary, ok := e.(*ast.UnaryExpr) + if ok && unary.Op == token.MUL { + e = unary.X + } selectorExpr, ok := e.(*ast.SelectorExpr) if !ok { return false @@ -666,9 +670,15 @@ func isSelector(e ast.Expr, x, sel string) bool { // reorderParams reorders the given parameters in-place to follow common Go conventions. func reorderParams(params []ast.Expr, paramTypes []*ast.Field) { + moveParamToFrontIfFound(params, paramTypes, "testing", "T") + moveParamToFrontIfFound(params, paramTypes, "testing", "B") + moveParamToFrontIfFound(params, paramTypes, "context", "Context") +} + +func moveParamToFrontIfFound(params []ast.Expr, paramTypes []*ast.Field, x, sel string) { // Move Context parameter (if any) to front. for i, t := range paramTypes { - if isSelector(t.Type, "context", "Context") { + if isSelector(t.Type, x, sel) { p, t := params[i], paramTypes[i] copy(params[1:], params[:i]) copy(paramTypes[1:], paramTypes[:i]) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt index 2b357be9e3c..75800504006 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt @@ -157,12 +157,17 @@ func (a A) Add() int { -- context.go -- package extract -import "context" +import ( + "context" + "testing" +) //@codeactionedit(B_AddP, "refactor.extract", contextMeth1, "Extract method") //@codeactionedit(B_AddP, "refactor.extract", contextFunc1, "Extract function") //@codeactionedit(B_LongList, "refactor.extract", contextMeth2, "Extract method") //@codeactionedit(B_LongList, "refactor.extract", contextFunc2, "Extract function") +//@codeactionedit(B_AddPWithB, "refactor.extract", contextFuncB, "Extract function") +//@codeactionedit(B_LongListWithT, "refactor.extract", contextFuncT, "Extract function") type B struct { x int @@ -180,39 +185,72 @@ func (b *B) LongList(ctx context.Context) (int, error) { p3 := 1 return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) } + +func (b *B) AddPWithB(ctx context.Context, tB *testing.B) (int, error) { + sum := b.x + b.y //@loc(B_AddPWithB, re`(?s:^.*?Err\(\))`) + tB.Skip() + return sum, ctx.Err() +} + +func (b *B) LongListWithT(ctx context.Context, t *testing.T) (int, error) { + p1 := 1 + p2 := 1 + p3 := 1 + p4 := p1 + p2 //@loc(B_LongListWithT, re`(?s:^.*?Err\(\))`) + t.Skip() + return p4 + p3, ctx.Err() +} -- @contextMeth1/context.go -- -@@ -17 +17 @@ +@@ -22 +22 @@ - return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) + return b.newMethod(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) -@@ -20 +20,4 @@ +@@ -25 +25,4 @@ +func (*B) newMethod(ctx context.Context, sum int) (int, error) { + return sum, ctx.Err() +} + -- @contextMeth2/context.go -- -@@ -24 +24 @@ +@@ -29 +29 @@ - return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) + return b.newMethod(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) -@@ -26 +26,4 @@ -+ +@@ -32 +32,4 @@ +func (*B) newMethod(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { + return p1 + p2 + p3, ctx.Err() +} ++ -- @contextFunc2/context.go -- -@@ -24 +24 @@ +@@ -29 +29 @@ - return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) + return newFunction(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) -@@ -26 +26,4 @@ -+ +@@ -32 +32,4 @@ +func newFunction(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { + return p1 + p2 + p3, ctx.Err() +} ++ -- @contextFunc1/context.go -- -@@ -17 +17 @@ +@@ -22 +22 @@ - return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) + return newFunction(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) -@@ -20 +20,4 @@ +@@ -25 +25,4 @@ +func newFunction(ctx context.Context, sum int) (int, error) { + return sum, ctx.Err() +} + +-- @contextFuncB/context.go -- +@@ -33 +33,6 @@ +- sum := b.x + b.y //@loc(B_AddPWithB, re`(?s:^.*?Err\(\))`) ++ //@loc(B_AddPWithB, re`(?s:^.*?Err\(\))`) ++ return newFunction(ctx, tB, b) ++} ++ ++func newFunction(ctx context.Context, tB *testing.B, b *B) (int, error) { ++ sum := b.x + b.y +-- @contextFuncT/context.go -- +@@ -42 +42,6 @@ +- p4 := p1 + p2 //@loc(B_LongListWithT, re`(?s:^.*?Err\(\))`) ++ //@loc(B_LongListWithT, re`(?s:^.*?Err\(\))`) ++ return newFunction(ctx, t, p1, p2, p3) ++} ++ ++func newFunction(ctx context.Context, t *testing.T, p1 int, p2 int, p3 int) (int, error) { ++ p4 := p1 + p2 From 42a6477e4efe48405882cc5fd5bbe17b77d07b5e Mon Sep 17 00:00:00 2001 From: Tim King Date: Wed, 11 Sep 2024 15:01:53 -0700 Subject: [PATCH 016/102] go/ssa: disable TestTypeparamTest/chan.go on wasm Also enables TestTypeparamTest/issue58513.go, which was disabled for ssa/interp. Fixes golang/go#64726 Change-Id: I7966fd09e6ec8de662a99f21086f6a5c34d86ab9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612398 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- go/ssa/builder_test.go | 7 ++++--- go/ssa/testdata/src/runtime/runtime.go | 2 ++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index f6fae50bb67..921fe51aa16 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -17,6 +17,7 @@ import ( "os/exec" "path/filepath" "reflect" + "runtime" "sort" "strings" "testing" @@ -773,7 +774,7 @@ var indirect = R[int].M // TestTypeparamTest builds SSA over compilable examples in $GOROOT/test/typeparam/*.go. func TestTypeparamTest(t *testing.T) { - // Tests use a fake goroot to stub out standard libraries with delcarations in + // Tests use a fake goroot to stub out standard libraries with declarations in // testdata/src. Decreases runtime from ~80s to ~1s. dir := filepath.Join(build.Default.GOROOT, "test", "typeparam") @@ -785,8 +786,8 @@ func TestTypeparamTest(t *testing.T) { } for _, entry := range list { - if entry.Name() == "issue58513.go" { - continue // uses runtime.Caller; unimplemented by go/ssa/interp + if entry.Name() == "chans.go" && runtime.GOARCH == "wasm" { + continue // https://go.dev/issues/64726 runtime: found bad pointer } if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { continue // Consider standalone go files. diff --git a/go/ssa/testdata/src/runtime/runtime.go b/go/ssa/testdata/src/runtime/runtime.go index 9feed5c995c..0363c85aaf1 100644 --- a/go/ssa/testdata/src/runtime/runtime.go +++ b/go/ssa/testdata/src/runtime/runtime.go @@ -3,3 +3,5 @@ package runtime func GC() func SetFinalizer(obj, finalizer any) + +func Caller(skip int) (pc uintptr, file string, line int, ok bool) From 6a387a400b7de86c9e97f1e0432973cdaa32c2ac Mon Sep 17 00:00:00 2001 From: Tim King Date: Wed, 11 Sep 2024 15:29:45 -0700 Subject: [PATCH 017/102] go/ssa: require 1.23 for TestCycles TestCycles requires internal/trace/testtrace which was added in 1.23. Fixes golang/go#69387 Fixes golang/go#69408 Change-Id: Iaa6b24a94216e7f2d9f7239b131e4a7e9ef9210e Reviewed-on: https://go-review.googlesource.com/c/tools/+/612399 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Tim King --- go/ssa/stdlib_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go index 391240ad757..9b78cfbf839 100644 --- a/go/ssa/stdlib_test.go +++ b/go/ssa/stdlib_test.go @@ -63,6 +63,7 @@ func TestNetHTTP(t *testing.T) { // This can under some schedules create a cycle of dependencies // where both need to wait on the other to finish building. func TestCycles(t *testing.T) { + testenv.NeedsGo1Point(t, 23) // internal/trace/testtrace was added in 1.23. testLoad(t, 120, "net/http", "internal/trace/testtrace") } From 4e8d5c8a99f2f6196230b60da2be0578dbe8fcb1 Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Wed, 11 Sep 2024 18:57:20 -0700 Subject: [PATCH 018/102] gopls: bump gofumpt to 0.7.0 Release notes: https://github.com/mvdan/gofumpt/releases/tag/v0.7.0 Note gofumpt now uses go/version to parse version string, so fixLangVersion is no longer needed, but "go" prefix is a must. Change-Id: I8b8e0eb3f0268cddf31ef2fcee90566a17a065ee Reviewed-on: https://go-review.googlesource.com/c/tools/+/609655 Reviewed-by: Robert Findley Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/go.mod | 2 +- gopls/go.sum | 8 ++-- gopls/internal/golang/format.go | 4 +- gopls/internal/settings/gofumpt.go | 56 +------------------------ gopls/internal/settings/gofumpt_test.go | 50 ---------------------- 5 files changed, 9 insertions(+), 111 deletions(-) delete mode 100644 gopls/internal/settings/gofumpt_test.go diff --git a/gopls/go.mod b/gopls/go.mod index a6128333050..759670af3cd 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -15,7 +15,7 @@ require ( golang.org/x/vuln v1.0.4 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.4.7 - mvdan.cc/gofumpt v0.6.0 + mvdan.cc/gofumpt v0.7.0 mvdan.cc/xurls/v2 v2.5.0 ) diff --git a/gopls/go.sum b/gopls/go.sum index c380cc75d5d..2819e487d71 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -1,7 +1,7 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= @@ -55,7 +55,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= -mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= -mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= +mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= +mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index 5755f7ae2ea..3fffbe3bfff 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go @@ -80,7 +80,9 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr meta, err := NarrowestMetadataForFile(ctx, snapshot, fh.URI()) if err == nil { if mi := meta.Module; mi != nil { - langVersion = mi.GoVersion + if v := mi.GoVersion; v != "" { + langVersion = "go" + v + } modulePath = mi.Path } } diff --git a/gopls/internal/settings/gofumpt.go b/gopls/internal/settings/gofumpt.go index 7a3541d4778..d9bf3109d8c 100644 --- a/gopls/internal/settings/gofumpt.go +++ b/gopls/internal/settings/gofumpt.go @@ -6,7 +6,6 @@ package settings import ( "context" - "fmt" "mvdan.cc/gofumpt/format" ) @@ -15,61 +14,8 @@ import ( // gofumpt/format.Source. langVersion and modulePath are used for some // Gofumpt formatting rules -- see the Gofumpt documentation for details. var GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { - fixedVersion, err := fixLangVersion(langVersion) - if err != nil { - return nil, err - } return format.Source(src, format.Options{ - LangVersion: fixedVersion, + LangVersion: langVersion, ModulePath: modulePath, }) } - -// fixLangVersion function cleans the input so that gofumpt doesn't panic. It is -// rather permissive, and accepts version strings that aren't technically valid -// in a go.mod file. -// -// More specifically, it looks for an optional 'v' followed by 1-3 -// '.'-separated numbers. The resulting string is stripped of any suffix beyond -// this expected version number pattern. -// -// See also golang/go#61692: gofumpt does not accept the new language versions -// appearing in go.mod files (e.g. go1.21rc3). -func fixLangVersion(input string) (string, error) { - bad := func() (string, error) { - return "", fmt.Errorf("invalid language version syntax %q", input) - } - if input == "" { - return input, nil - } - i := 0 - if input[0] == 'v' { // be flexible about 'v' - i++ - } - // takeDigits consumes ascii numerals 0-9 and reports if at least one was - // consumed. - takeDigits := func() bool { - found := false - for ; i < len(input) && '0' <= input[i] && input[i] <= '9'; i++ { - found = true - } - return found - } - if !takeDigits() { // versions must start with at least one number - return bad() - } - - // Accept optional minor and patch versions. - for n := 0; n < 2; n++ { - if i < len(input) && input[i] == '.' { - // Look for minor/patch version. - i++ - if !takeDigits() { - i-- - break - } - } - } - // Accept any suffix. - return input[:i], nil -} diff --git a/gopls/internal/settings/gofumpt_test.go b/gopls/internal/settings/gofumpt_test.go deleted file mode 100644 index 090ebcb601f..00000000000 --- a/gopls/internal/settings/gofumpt_test.go +++ /dev/null @@ -1,50 +0,0 @@ -// 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. - -package settings - -import "testing" - -func TestFixLangVersion(t *testing.T) { - tests := []struct { - input, want string - wantErr bool - }{ - {"", "", false}, - {"1.18", "1.18", false}, - {"v1.18", "v1.18", false}, - {"1.21", "1.21", false}, - {"1.21rc3", "1.21", false}, - {"1.21.0", "1.21.0", false}, - {"1.21.1", "1.21.1", false}, - {"v1.21.1", "v1.21.1", false}, - {"v1.21.0rc1", "v1.21.0", false}, // not technically valid, but we're flexible - {"v1.21.0.0", "v1.21.0", false}, // also technically invalid - {"1.1", "1.1", false}, - {"v1", "v1", false}, - {"1", "1", false}, - {"v1.21.", "v1.21", false}, // also invalid - {"1.21.", "1.21", false}, - - // Error cases. - {"rc1", "", true}, - {"x1.2.3", "", true}, - } - - for _, test := range tests { - got, err := fixLangVersion(test.input) - if test.wantErr { - if err == nil { - t.Errorf("fixLangVersion(%q) succeeded unexpectedly", test.input) - } - continue - } - if err != nil { - t.Fatalf("fixLangVersion(%q) failed: %v", test.input, err) - } - if got != test.want { - t.Errorf("fixLangVersion(%q) = %s, want %s", test.input, got, test.want) - } - } -} From 15b8886a84d5cf396f39e693ae05c4f1f87a1a9e Mon Sep 17 00:00:00 2001 From: xizi Date: Thu, 12 Sep 2024 02:47:39 +0000 Subject: [PATCH 019/102] SignatureHelp: report signature of Ident if no enclosing CallExpr Currently, SignatureHelp reports information only about an enclosing call expression. But it would be useful to show signature information after entering the name of a function such as "f" or "fmt.Println", without call parens. So, if there is no enclosing call, this change reports the signature of the selected identifier if it is callable. Fixes golang/go#68922 Change-Id: Ibb0700e354c5d6e5937fc7f7b5db65e9d96574bb GitHub-Last-Rev: 5e7965edd342a450fd486ccd1fb7149ad20a2733 GitHub-Pull-Request: golang/tools#510 Reviewed-on: https://go-review.googlesource.com/c/tools/+/605983 Reviewed-by: Alan Donovan Reviewed-by: Robert Findley Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/doc/features/passive.md | 4 ++ gopls/doc/release/v0.17.0.md | 4 ++ gopls/internal/golang/signature_help.go | 45 ++++++++++++++----- .../marker/testdata/signature/signature.txt | 27 ++++++++++- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/gopls/doc/features/passive.md b/gopls/doc/features/passive.md index 92ae929ad5e..23a1938cbeb 100644 --- a/gopls/doc/features/passive.md +++ b/gopls/doc/features/passive.md @@ -107,6 +107,10 @@ function call. +Call parens are not necessary if the cursor is within an identifier +that denotes a function or method. For example, Signature Help at +`once.Do(initialize‸)` will describe `initialize`, not `once.Do`. + Client support: - **VS Code**: enabled by default. Also known as "[parameter hints](https://code.visualstudio.com/api/references/vscode-api#SignatureHelpProvider)" in the [IntelliSense settings](https://code.visualstudio.com/docs/editor/intellisense#_settings). diff --git a/gopls/doc/release/v0.17.0.md b/gopls/doc/release/v0.17.0.md index dba85fef46c..12f04dadf2d 100644 --- a/gopls/doc/release/v0.17.0.md +++ b/gopls/doc/release/v0.17.0.md @@ -35,3 +35,7 @@ constructor of the type of each symbol: `interface`, `struct`, `signature`, `pointer`, `array`, `map`, `slice`, `chan`, `string`, `number`, `bool`, and `invalid`. Editors may use this for syntax coloring. +## SignatureHelp for ident and values. + +Now, function signature help can be used on any identifier with a function +signature, not just within the parentheses of a function being called. diff --git a/gopls/internal/golang/signature_help.go b/gopls/internal/golang/signature_help.go index a91be296cbd..26cb92c643b 100644 --- a/gopls/internal/golang/signature_help.go +++ b/gopls/internal/golang/signature_help.go @@ -45,13 +45,32 @@ func SignatureHelp(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle if path == nil { return nil, 0, fmt.Errorf("cannot find node enclosing position") } -FindCall: - for _, node := range path { + info := pkg.TypesInfo() + var fnval ast.Expr +loop: + for i, node := range path { switch node := node.(type) { + case *ast.Ident: + // If the selected text is a function/method Ident orSelectorExpr, + // even one not in function call position, + // show help for its signature. Example: + // once.Do(initialize⁁) + // should show help for initialize, not once.Do. + if t := info.TypeOf(node); t != nil && + info.Defs[node] == nil && + is[*types.Signature](t.Underlying()) { + if sel, ok := path[i+1].(*ast.SelectorExpr); ok && sel.Sel == node { + fnval = sel // e.g. fmt.Println⁁ + } else { + fnval = node + } + break loop + } case *ast.CallExpr: if pos >= node.Lparen && pos <= node.Rparen { callExpr = node - break FindCall + fnval = callExpr.Fun + break loop } case *ast.FuncLit, *ast.FuncType: // The user is within an anonymous function, @@ -70,20 +89,19 @@ FindCall: } } - if callExpr == nil || callExpr.Fun == nil { + + if fnval == nil { return nil, 0, nil } - info := pkg.TypesInfo() - // Get the type information for the function being called. var sig *types.Signature - if tv, ok := info.Types[callExpr.Fun]; !ok { - return nil, 0, fmt.Errorf("cannot get type for Fun %[1]T (%[1]v)", callExpr.Fun) + if tv, ok := info.Types[fnval]; !ok { + return nil, 0, fmt.Errorf("cannot get type for Fun %[1]T (%[1]v)", fnval) } else if tv.IsType() { return nil, 0, nil // a conversion, not a call } else if sig, ok = tv.Type.Underlying().(*types.Signature); !ok { - return nil, 0, fmt.Errorf("call operand is not a func or type: %[1]T (%[1]v)", callExpr.Fun) + return nil, 0, fmt.Errorf("call operand is not a func or type: %[1]T (%[1]v)", fnval) } // Inv: sig != nil @@ -93,7 +111,7 @@ FindCall: // There is no object in certain cases such as calling a function returned by // a function (e.g. "foo()()"). var obj types.Object - switch t := callExpr.Fun.(type) { + switch t := fnval.(type) { case *ast.Ident: obj = info.ObjectOf(t) case *ast.SelectorExpr: @@ -116,7 +134,12 @@ FindCall: return nil, 0, bug.Errorf("call to unexpected built-in %v (%T)", obj, obj) } - activeParam := activeParameter(callExpr, sig.Params().Len(), sig.Variadic(), pos) + activeParam := 0 + if callExpr != nil { + // only return activeParam when CallExpr + // because we don't modify arguments when get function signature only + activeParam = activeParameter(callExpr, sig.Params().Len(), sig.Variadic(), pos) + } var ( name string diff --git a/gopls/internal/test/marker/testdata/signature/signature.txt b/gopls/internal/test/marker/testdata/signature/signature.txt index 7bdd6341818..1da4eb5843e 100644 --- a/gopls/internal/test/marker/testdata/signature/signature.txt +++ b/gopls/internal/test/marker/testdata/signature/signature.txt @@ -16,6 +16,7 @@ import ( "bytes" "encoding/json" "math/big" + "fmt" ) func Foo(a string, b int) (c bool) { @@ -49,6 +50,9 @@ func Qux() { Foo("foo", 123) //@signature(",", "Foo(a string, b int) (c bool)", 0) Foo("foo", 123) //@signature(" 1", "Foo(a string, b int) (c bool)", 1) Foo("foo", 123) //@signature(")", "Foo(a string, b int) (c bool)", 1) + Foo("foo", 123) //@signature("o", "Foo(a string, b int) (c bool)", 0) + _ = Foo //@signature("o", "Foo(a string, b int) (c bool)", 0) + Foo //@signature("o", "Foo(a string, b int) (c bool)", 0) Bar(13.37, 0x13) //@signature("13.37", "Bar(float64, ...byte)", 0) Bar(13.37, 0x37) //@signature("0x37", "Bar(float64, ...byte)", 1) @@ -78,9 +82,28 @@ func Qux() { _ = make([]int, 1, 2) //@signature("2", "make(t Type, size ...int) Type", 1) - Foo(myFunc(123), 456) //@signature("myFunc", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("o(", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("(m", "Foo(a string, b int) (c bool)", 0) + Foo( myFunc(123), 456) //@signature(" m", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature(", ", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("456", "Foo(a string, b int) (c bool)", 1) + Foo(myFunc) //@signature(")", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("(1", "myFunc(foo int) string", 0) Foo(myFunc(123), 456) //@signature("123", "myFunc(foo int) string", 0) + fmt.Println //@signature("ln", "Println(a ...any) (n int, err error)", 0) + fmt.Println(myFunc) //@signature("ln", "Println(a ...any) (n int, err error)", 0) + fmt.Println(myFunc) //@signature("Func", "myFunc(foo int) string", 0) + + var hi string = "hello" + var wl string = " world: %s" + fmt.Println(fmt.Sprintf(wl, myFunc)) //@signature("Func", "myFunc(foo int) string", 0) + fmt.Println(fmt.Sprintf(wl, myFunc)) //@signature("wl", "Sprintf(format string, a ...any) string", 0) + fmt.Println(fmt.Sprintf(wl, myFunc)) //@signature(" m", "Sprintf(format string, a ...any) string", 1) + fmt.Println(hi, fmt.Sprintf(wl, myFunc)) //@signature("Sprint", "Sprintf(format string, a ...any) string", 0) + fmt.Println(hi, fmt.Sprintf(wl, myFunc)) //@signature(" fmt", "Println(a ...any) (n int, err error)", 0) + fmt.Println(hi, fmt.Sprintf(wl, myFunc)) //@signature("hi", "Println(a ...any) (n int, err error)", 0) + panic("oops!") //@signature(")", "panic(v any)", 0) println("hello", "world") //@signature(",", "println(args ...Type)", 0) @@ -100,6 +123,8 @@ package signature func _() { Foo(//@signature("//", "Foo(a string, b int) (c bool)", 0) + Foo.//@signature("//", "Foo(a string, b int) (c bool)", 0) + Foo.//@signature("oo", "Foo(a string, b int) (c bool)", 0) } -- signature/signature3.go -- From beed481fb54ead699f9619d6ae6bd247035d8d5a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 12 Aug 2024 15:09:16 -0400 Subject: [PATCH 020/102] gopls/internal/settings: use CodeActionKind hierarchy This change causes all CodeActions returned by gopls to use a specific leaf type such as refactor.inline.call, instead of a general category such as refactor or refactor.inline. The categories may continue to be specified in the "Only" parameter of a CodeActions request, and they apply hierarchically. This allows clients to be more specific in requesting a particular Code Action. Details of golang/codeaction.go (messy diff): - don't separate "typed" from "syntax only" operations; in practice we always need types. - enabled (func) replaces want (map). It is computed in server.CodeAction and plumbed down. - the "add" helper wraps a Command in a CodeAction and adds it to the result. Also: - use camelCase names, following LSP's source.organizeImports. - document the specific kinds in the user manual and add a release note. - remove the CodeAction title regexp parameter from @codeaction{,err} marker tests since the kind is now sufficiently expressive. Fixes golang/go#40438 Updates golang/go#68791 Updates golang/go#68783 Change-Id: I1898f79d8af441a0376ad5067d05e0621840d987 Reviewed-on: https://go-review.googlesource.com/c/tools/+/604818 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/doc/features/transformation.md | 41 +- gopls/doc/features/web.md | 6 +- gopls/doc/release/v0.17.0.md | 9 +- gopls/internal/cmd/codeaction.go | 23 +- gopls/internal/cmd/integration_test.go | 11 + gopls/internal/cmd/usage/codeaction.hlp | 23 +- gopls/internal/golang/change_quote.go | 3 +- gopls/internal/golang/codeaction.go | 552 ++++++++++-------- gopls/internal/server/code_action.go | 98 ++-- gopls/internal/settings/codeactionkind.go | 85 +-- gopls/internal/settings/default.go | 32 +- .../internal/test/integration/fake/editor.go | 10 - .../test/integration/misc/codeactions_test.go | 10 +- .../test/integration/misc/extract_test.go | 3 +- .../test/integration/misc/fix_test.go | 3 +- .../test/integration/misc/webserver_test.go | 2 +- gopls/internal/test/marker/doc.go | 6 +- gopls/internal/test/marker/marker_test.go | 37 +- .../testdata/codeaction/change_quote.txt | 54 +- .../codeaction/extract-variadic-63287.txt | 2 +- .../testdata/codeaction/extract_method.txt | 38 +- .../codeaction/extract_variable-67905.txt | 6 +- .../testdata/codeaction/extract_variable.txt | 36 +- .../codeaction/extract_variable_resolve.txt | 36 +- .../testdata/codeaction/extracttofile.txt | 96 +-- .../testdata/codeaction/fill_struct.txt | 232 ++++---- .../codeaction/fill_struct_resolve.txt | 232 ++++---- .../testdata/codeaction/fill_switch.txt | 10 +- .../codeaction/fill_switch_resolve.txt | 10 +- .../codeaction/functionextraction.txt | 100 ++-- .../functionextraction_issue44813.txt | 4 +- .../marker/testdata/codeaction/grouplines.txt | 40 +- .../marker/testdata/codeaction/inline.txt | 6 +- .../testdata/codeaction/inline_resolve.txt | 6 +- .../marker/testdata/codeaction/invertif.txt | 66 +-- .../marker/testdata/codeaction/issue64558.txt | 2 +- .../testdata/codeaction/removeparam.txt | 26 +- .../codeaction/removeparam_formatting.txt | 4 +- .../codeaction/removeparam_funcvalue.txt | 2 +- .../codeaction/removeparam_imports.txt | 8 +- .../codeaction/removeparam_issue65217.txt | 4 +- .../codeaction/removeparam_method.txt | 8 +- .../codeaction/removeparam_resolve.txt | 26 +- .../codeaction/removeparam_satisfies.txt | 4 +- .../codeaction/removeparam_witherrs.txt | 2 +- .../marker/testdata/codeaction/splitlines.txt | 44 +- 46 files changed, 1089 insertions(+), 969 deletions(-) diff --git a/gopls/doc/features/transformation.md b/gopls/doc/features/transformation.md index 579c14818fa..bf5df29c01b 100644 --- a/gopls/doc/features/transformation.md +++ b/gopls/doc/features/transformation.md @@ -50,12 +50,12 @@ The main difference between code lenses and code actions is this: All the commands are presented together in a menu at that location. Each action has a _kind_, -which is a hierarchical identifier such as `refactor.inline`. +which is a hierarchical identifier such as `refactor.inline.call`. Clients may filter actions based on their kind. For example, VS Code has: two menus, "Refactor..." and "Source action...", each populated by -different kinds of code actions (`refactor.*` and `source.*`); -a lightbulb icon that triggers a menu of "quick fixes" (of kind `quickfix.*`); +different kinds of code actions (`refactor` and `source`); +a lightbulb icon that triggers a menu of "quick fixes" (of kind `quickfix`); and a "Fix All" command that executes all code actions of kind `source.fixAll`, which are those deemed unambiguously safe to apply. @@ -187,7 +187,7 @@ Client support: - **CLI**: `gopls fix -a file.go:#offset source.organizeImports` -## Rename +## `refactor.rename`: Rename The LSP [`textDocument/rename`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_rename) @@ -269,7 +269,7 @@ Client support: -## Extract function/method/variable +## `refactor.extract`: Extract function/method/variable The `refactor.extract` family of code actions all return commands that replace the selected expression or statements with a reference to a @@ -278,7 +278,7 @@ newly created declaration that contains the selected code: -- **Extract function** replaces one or more complete statements by a +- **`refactor.extract.function`** replaces one or more complete statements by a call to a new function named `newFunction` whose body contains the statements. The selection must enclose fewer statements than the entire body of the existing function. @@ -286,11 +286,11 @@ newly created declaration that contains the selected code: ![Before extracting a function](../assets/extract-function-before.png) ![After extracting a function](../assets/extract-function-after.png) -- **Extract method** is a variant of "Extract function" offered when +- **`refactor.extract.method`** is a variant of "Extract function" offered when the selected statements belong to a method. The newly created function will be a method of the same receiver type. -- **Extract variable** replaces an expression by a reference to a new +- **`refactor.extract.variable`** replaces an expression by a reference to a new local variable named `x` initialized by the expression: ![Before extracting a var](../assets/extract-var-before.png) @@ -330,7 +330,7 @@ The following Extract features are planned for 2024 but not yet supported: -## Extract declarations to new file +## `refactor.extract.toNewFile`: Extract declarations to new file (Available from gopls/v0.17.0) @@ -347,11 +347,11 @@ first token of the declaration, such as `func` or `type`. -## Inline call to function +## `refactor.inline.call`: Inline call to function For a `codeActions` request where the selection is (or is within) a call of a function or method, gopls will return a command of kind -`refactor.inline`, whose effect is to inline the function call. +`refactor.inline.call`, whose effect is to inline the function call. The screenshots below show a call to `sum` before and after inlining: - -### Remove unused parameter +### `refactor.rewrite.removeUnusedParam`: Remove unused parameter The [`unusedparams` analyzer](../analyzers.md#unusedparams) reports a diagnostic for each parameter that is not used within the function body. @@ -544,7 +541,7 @@ Observe that in the first call, the argument `chargeCreditCard()` was not deleted because of potential side effects, whereas in the second call, the argument 2, a constant, was safely deleted. -### Convert string literal between raw and interpreted +### `refactor.rewrite.changeQuote`: Convert string literal between raw and interpreted When the selection is a string literal, gopls offers a code action to convert the string between raw form (`` `abc` ``) and interpreted @@ -556,7 +553,7 @@ form (`"abc"`) where this is possible: Applying the code action a second time reverts back to the original form. -### Invert 'if' condition +### `refactor.rewrite.invertIf`: Invert 'if' condition When the selection is within an `if`/`else` statement that is not followed by `else if`, gopls offers a code action to invert the @@ -571,7 +568,7 @@ blocks. if the else block ends with a return statement; and thus applying the operation twice does not get you back to where you started. --> -### Split elements into separate lines +### `refactor.rewrite.{split,join}Lines`: Split elements into separate lines When the selection is within a bracketed list of items such as: @@ -619,7 +616,7 @@ comments, which run to the end of the line. -### Fill struct literal +### `refactor.rewrite.fillStruct`: Fill struct literal When the cursor is within a struct literal `S{}`, gopls offers the "Fill S" code action, which populates each missing field of the @@ -651,7 +648,7 @@ Caveats: or in other files in the package, are not considered; see golang/go#68224. -### Fill switch +### `refactor.rewrite.fillSwitch`: Fill switch When the cursor is within a switch statement whose operand type is an _enum_ (a finite set of named constants), or within a type switch, diff --git a/gopls/doc/features/web.md b/gopls/doc/features/web.md index 698cd837f69..46a9f91477b 100644 --- a/gopls/doc/features/web.md +++ b/gopls/doc/features/web.md @@ -49,7 +49,7 @@ your source code has been modified but not saved. like your editor to raise its window when handling this event.) -## Browse package documentation +## `source.doc`: Browse package documentation In any Go source file, a code action request returns a command to "Browse package documentation". This command opens a browser window @@ -75,7 +75,7 @@ Client support: -## Browse free symbols +## `source.freesymbols`: Browse free symbols When studying code, either to understand it or to evaluate a different organization or factoring, it is common to need to know what the @@ -108,7 +108,7 @@ Client support: -## Browse assembly +## `source.assembly`: Browse assembly When you're optimizing the performance of your code or investigating an unexpected crash, it may sometimes be helpful to inspect the diff --git a/gopls/doc/release/v0.17.0.md b/gopls/doc/release/v0.17.0.md index 12f04dadf2d..45eecef4735 100644 --- a/gopls/doc/release/v0.17.0.md +++ b/gopls/doc/release/v0.17.0.md @@ -6,11 +6,18 @@ The `fieldalignment` analyzer, previously disabled by default, has been removed: it is redundant with the hover size/offset information displayed by v0.16.0 and its diagnostics were confusing. +The kind (identifiers) of all of gopls' code actions have changed +to use more specific hierarchical names. For example, "Inline call" +has changed from `refactor.inline` to `refactor.inline.call`. +This allows clients to request particular code actions more precisely. +The user manual now includes the identifier in the documentation for each code action. # New features ## Extract declarations to new file -Gopls now offers another code action, "Extract declarations to new file", + +Gopls now offers another code action, +"Extract declarations to new file" (`refactor.extract.toNewFile`), which moves selected code sections to a newly created file within the same package. The created filename is chosen as the first {function, type, const, var} name encountered. In addition, import declarations are added or diff --git a/gopls/internal/cmd/codeaction.go b/gopls/internal/cmd/codeaction.go index 63a3c999b6f..84d7d181b88 100644 --- a/gopls/internal/cmd/codeaction.go +++ b/gopls/internal/cmd/codeaction.go @@ -47,21 +47,34 @@ The -kind flag specifies a comma-separated list of LSP CodeAction kinds. Only actions of these kinds will be requested from the server. Valid kinds include: + gopls.doc.features quickfix refactor refactor.extract + refactor.extract.function + refactor.extract.method + refactor.extract.toNewFile + refactor.extract.variable refactor.inline + refactor.inline.call refactor.rewrite - source.organizeImports - source.fixAll + refactor.rewrite.changeQuote + refactor.rewrite.fillStruct + refactor.rewrite.fillSwitch + refactor.rewrite.invertIf + refactor.rewrite.joinLines + refactor.rewrite.removeUnusedParam + refactor.rewrite.splitLines + source source.assembly source.doc + source.fixAll source.freesymbols - goTest - gopls.doc.features + source.organizeImports + source.test Kinds are hierarchical, so "refactor" includes "refactor.inline". -(Note: actions of kind "goTest" are not returned unless explicitly +(Note: actions of kind "source.test" are not returned unless explicitly requested.) The -title flag specifies a regular expression that must match the diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index 0bc066b02e0..dfb2a164a42 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -988,6 +988,17 @@ type C struct{} t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) } } + // list code actions in file, filtering (hierarchically) by kind + { + res := gopls(t, tree, "codeaction", "-kind=source", "a.go") + res.checkExit(true) + got := res.stdout + want := `command "Browse documentation for package a" [source.doc]` + + "\n" + if got != want { + t.Errorf("codeaction: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) + } + } // list code actions at position (of io.Reader) { res := gopls(t, tree, "codeaction", "b.go:#31") diff --git a/gopls/internal/cmd/usage/codeaction.hlp b/gopls/internal/cmd/usage/codeaction.hlp index edc6a3e8f99..6d6923ef458 100644 --- a/gopls/internal/cmd/usage/codeaction.hlp +++ b/gopls/internal/cmd/usage/codeaction.hlp @@ -18,21 +18,34 @@ The -kind flag specifies a comma-separated list of LSP CodeAction kinds. Only actions of these kinds will be requested from the server. Valid kinds include: + gopls.doc.features quickfix refactor refactor.extract + refactor.extract.function + refactor.extract.method + refactor.extract.toNewFile + refactor.extract.variable refactor.inline + refactor.inline.call refactor.rewrite - source.organizeImports - source.fixAll + refactor.rewrite.changeQuote + refactor.rewrite.fillStruct + refactor.rewrite.fillSwitch + refactor.rewrite.invertIf + refactor.rewrite.joinLines + refactor.rewrite.removeUnusedParam + refactor.rewrite.splitLines + source source.assembly source.doc + source.fixAll source.freesymbols - goTest - gopls.doc.features + source.organizeImports + source.test Kinds are hierarchical, so "refactor" includes "refactor.inline". -(Note: actions of kind "goTest" are not returned unless explicitly +(Note: actions of kind "source.test" are not returned unless explicitly requested.) The -title flag specifies a regular expression that must match the diff --git a/gopls/internal/golang/change_quote.go b/gopls/internal/golang/change_quote.go index e20b1ea88fb..6fa56d42615 100644 --- a/gopls/internal/golang/change_quote.go +++ b/gopls/internal/golang/change_quote.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" @@ -72,7 +73,7 @@ func convertStringLiteral(pgf *parsego.File, fh file.Handle, startPos, endPos to } return protocol.CodeAction{ Title: title, - Kind: protocol.RefactorRewrite, + Kind: settings.RefactorRewriteChangeQuote, Edit: protocol.NewWorkspaceEdit(protocol.DocumentChangeEdit(fh, textedits)), }, true } diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 1921d1326b8..4e6e23ef128 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -29,174 +29,189 @@ import ( "golang.org/x/tools/internal/typesinternal" ) -// CodeActions returns all wanted code actions (edits and other +// CodeActions returns all enabled code actions (edits and other // commands) available for the selected range. // // Depending on how the request was triggered, fewer actions may be // offered, e.g. to avoid UI distractions after mere cursor motion. // // See ../protocol/codeactionkind.go for some code action theory. -func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, want map[protocol.CodeActionKind]bool, trigger protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) { - // Only compute quick fixes if there are any diagnostics to fix. - wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0 +func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool, trigger protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) { + + // code actions that require only a parse tree + + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) + if err != nil { + return nil, err + } + start, end, err := pgf.RangePos(rng) + if err != nil { + return nil, err + } + + // add adds a code action to the result. + add := func(cmd protocol.Command, kind protocol.CodeActionKind) { + action := newCodeAction(cmd.Title, kind, &cmd, nil, snapshot.Options()) + actions = append(actions, action) + } // Note: don't forget to update the allow-list in Server.CodeAction // when adding new query operations like GoTest and GoDoc that - // are permitted even in generated source files + // are permitted even in generated source files. - // Code actions that can be offered based on syntax information alone. - if wantQuickFixes || - want[protocol.SourceOrganizeImports] || - want[protocol.RefactorExtract] || - want[settings.GoFreeSymbols] || - want[settings.GoplsDocFeatures] { - - pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) + // Only compute quick fixes if there are any diagnostics to fix. + wantQuickFixes := len(diagnostics) > 0 && enabled(protocol.QuickFix) + if wantQuickFixes || enabled(protocol.SourceOrganizeImports) { + // Process any missing imports and pair them with the diagnostics they fix. + importEdits, importEditsPerFix, err := allImportsFixes(ctx, snapshot, pgf) if err != nil { - return nil, err + event.Error(ctx, "imports fixes", err, label.File.Of(fh.URI().Path())) + importEdits = nil + importEditsPerFix = nil } - // Process any missing imports and pair them with the diagnostics they fix. - if wantQuickFixes || want[protocol.SourceOrganizeImports] { - importEdits, importEditsPerFix, err := allImportsFixes(ctx, snapshot, pgf) - if err != nil { - event.Error(ctx, "imports fixes", err, label.File.Of(fh.URI().Path())) - importEdits = nil - importEditsPerFix = nil - } - - // Separate this into a set of codeActions per diagnostic, where - // each action is the addition, removal, or renaming of one import. - if wantQuickFixes { - for _, importFix := range importEditsPerFix { - fixed := fixedByImportFix(importFix.fix, diagnostics) - if len(fixed) == 0 { - continue - } - actions = append(actions, protocol.CodeAction{ - Title: importFixTitle(importFix.fix), - Kind: protocol.QuickFix, - Edit: protocol.NewWorkspaceEdit( - protocol.DocumentChangeEdit(fh, importFix.edits)), - Diagnostics: fixed, - }) + // Separate this into a set of codeActions per diagnostic, where + // each action is the addition, removal, or renaming of one import. + if wantQuickFixes { + for _, importFix := range importEditsPerFix { + fixed := fixedByImportFix(importFix.fix, diagnostics) + if len(fixed) == 0 { + continue } - } - - // Send all of the import edits as one code action if the file is - // being organized. - if want[protocol.SourceOrganizeImports] && len(importEdits) > 0 { actions = append(actions, protocol.CodeAction{ - Title: "Organize Imports", - Kind: protocol.SourceOrganizeImports, + Title: importFixTitle(importFix.fix), + Kind: protocol.QuickFix, Edit: protocol.NewWorkspaceEdit( - protocol.DocumentChangeEdit(fh, importEdits)), + protocol.DocumentChangeEdit(fh, importFix.edits)), + Diagnostics: fixed, }) } } - if want[protocol.RefactorExtract] { - extractions, err := getExtractCodeActions(pgf, rng, snapshot.Options()) - if err != nil { - return nil, err - } - actions = append(actions, extractions...) - } - - if want[settings.GoFreeSymbols] && rng.End != rng.Start { - loc := protocol.Location{URI: pgf.URI, Range: rng} - cmd, err := command.NewFreeSymbolsCommand("Browse free symbols", snapshot.View().ID(), loc) - if err != nil { - return nil, err - } - // For implementation, see commandHandler.FreeSymbols. + // Send all of the import edits as one code action if the file is + // being organized. + if len(importEdits) > 0 && enabled(protocol.SourceOrganizeImports) { actions = append(actions, protocol.CodeAction{ - Title: cmd.Title, - Kind: settings.GoFreeSymbols, - Command: &cmd, + Title: "Organize Imports", + Kind: protocol.SourceOrganizeImports, + Edit: protocol.NewWorkspaceEdit( + protocol.DocumentChangeEdit(fh, importEdits)), }) } + } - if want[settings.GoplsDocFeatures] { - // TODO(adonovan): after the docs are published in gopls/v0.17.0, - // use the gopls release tag instead of master. - cmd, err := command.NewClientOpenURLCommand( - "Browse gopls feature documentation", - "https://github.com/golang/tools/blob/master/gopls/doc/features/README.md") - if err != nil { - return nil, err - } - actions = append(actions, protocol.CodeAction{ - Title: cmd.Title, - Kind: settings.GoplsDocFeatures, - Command: &cmd, - }) + // refactor.extract.* + { + extractions, err := getExtractCodeActions(enabled, pgf, rng, snapshot.Options()) + if err != nil { + return nil, err } + actions = append(actions, extractions...) } - // Code actions requiring type information. - if want[protocol.RefactorRewrite] || - want[protocol.RefactorInline] || - want[settings.GoAssembly] || - want[settings.GoDoc] || - want[settings.GoTest] { - pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) + if kind := settings.GoFreeSymbols; enabled(kind) && rng.End != rng.Start { + loc := protocol.Location{URI: pgf.URI, Range: rng} + cmd, err := command.NewFreeSymbolsCommand("Browse free symbols", snapshot.View().ID(), loc) if err != nil { return nil, err } - start, end, err := pgf.RangePos(rng) + // For implementation, see commandHandler.FreeSymbols. + add(cmd, kind) + } + + if kind := settings.GoplsDocFeatures; enabled(kind) { + // TODO(adonovan): after the docs are published in gopls/v0.17.0, + // use the gopls release tag instead of master. + cmd, err := command.NewClientOpenURLCommand( + "Browse gopls feature documentation", + "https://github.com/golang/tools/blob/master/gopls/doc/features/README.md") if err != nil { return nil, err } + add(cmd, kind) + } - if want[protocol.RefactorRewrite] { - rewrites, err := getRewriteCodeActions(ctx, pkg, snapshot, pgf, fh, rng, snapshot.Options()) - if err != nil { - return nil, err - } - actions = append(actions, rewrites...) + // code actions that require type information + // + // In order to keep the fast path (in particular, + // VS Code's request for just source.organizeImports + // immediately after a save) fast, avoid type checking + // if no enabled code actions need it. + // + // TODO(adonovan): design some kind of registration mechanism + // that avoids the need to keep this list up to date. + if !slices.ContainsFunc([]protocol.CodeActionKind{ + settings.RefactorRewriteRemoveUnusedParam, + settings.RefactorRewriteChangeQuote, + settings.RefactorRewriteInvertIf, + settings.RefactorRewriteSplitLines, + settings.RefactorRewriteJoinLines, + settings.RefactorRewriteFillStruct, + settings.RefactorRewriteFillSwitch, + settings.RefactorInlineCall, + settings.GoTest, + settings.GoDoc, + settings.GoAssembly, + }, enabled) { + return actions, nil + } + + // NB: update pgf, since it may not be a parse cache hit (esp. on 386). + // And update start, end, since they may have changed too. + // A framework would really make this cleaner. + pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) + if err != nil { + return nil, err + } + start, end, err = pgf.RangePos(rng) + if err != nil { + return nil, err + } + + // refactor.rewrite.* + { + rewrites, err := getRewriteCodeActions(enabled, ctx, pkg, snapshot, pgf, fh, rng, snapshot.Options()) + if err != nil { + return nil, err } + actions = append(actions, rewrites...) + } - // To avoid distraction (e.g. VS Code lightbulb), offer "inline" - // only after a selection or explicit menu operation. - if want[protocol.RefactorInline] && (trigger != protocol.CodeActionAutomatic || rng.Start != rng.End) { - rewrites, err := getInlineCodeActions(pkg, pgf, rng, snapshot.Options()) - if err != nil { - return nil, err - } - actions = append(actions, rewrites...) + // To avoid distraction (e.g. VS Code lightbulb), offer "inline" + // only after a selection or explicit menu operation. + if trigger != protocol.CodeActionAutomatic || rng.Start != rng.End { + rewrites, err := getInlineCodeActions(enabled, pkg, pgf, rng, snapshot.Options()) + if err != nil { + return nil, err } + actions = append(actions, rewrites...) + } - if want[settings.GoTest] { - fixes, err := getGoTestCodeActions(pkg, pgf, rng) - if err != nil { - return nil, err - } - actions = append(actions, fixes...) + if enabled(settings.GoTest) { + fixes, err := getGoTestCodeActions(pkg, pgf, rng) + if err != nil { + return nil, err } + actions = append(actions, fixes...) + } - if want[settings.GoDoc] { - // "Browse documentation for ..." - _, _, title := DocFragment(pkg, pgf, start, end) - loc := protocol.Location{URI: pgf.URI, Range: rng} - cmd, err := command.NewDocCommand(title, loc) - if err != nil { - return nil, err - } - actions = append(actions, protocol.CodeAction{ - Title: cmd.Title, - Kind: settings.GoDoc, - Command: &cmd, - }) + if kind := settings.GoDoc; enabled(kind) { + // "Browse documentation for ..." + _, _, title := DocFragment(pkg, pgf, start, end) + loc := protocol.Location{URI: pgf.URI, Range: rng} + cmd, err := command.NewDocCommand(title, loc) + if err != nil { + return nil, err } + add(cmd, kind) + } - if want[settings.GoAssembly] { - fixes, err := getGoAssemblyAction(snapshot.View(), pkg, pgf, rng) - if err != nil { - return nil, err - } - actions = append(actions, fixes...) + if enabled(settings.GoAssembly) { + fixes, err := getGoAssemblyAction(snapshot.View(), pkg, pgf, rng) + if err != nil { + return nil, err } + actions = append(actions, fixes...) } return actions, nil } @@ -256,63 +271,81 @@ func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) } // getExtractCodeActions returns any refactor.extract code actions for the selection. -func getExtractCodeActions(pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { +func getExtractCodeActions(enabled func(protocol.CodeActionKind) bool, pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { start, end, err := pgf.RangePos(rng) if err != nil { return nil, err } - puri := pgf.URI - var commands []protocol.Command - if _, ok, methodOk, _ := canExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok { - cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ - Fix: fixExtractFunction, - URI: puri, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err + + var actions []protocol.CodeAction + add := func(cmd protocol.Command, kind protocol.CodeActionKind) { + action := newCodeAction(cmd.Title, kind, &cmd, nil, options) + actions = append(actions, action) + } + + // extract function or method + if enabled(settings.RefactorExtractFunction) || enabled(settings.RefactorExtractMethod) { + if _, ok, methodOk, _ := canExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok { + // extract function + if kind := settings.RefactorExtractFunction; enabled(kind) { + cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ + Fix: fixExtractFunction, + URI: pgf.URI, + Range: rng, + ResolveEdits: supportsResolveEdits(options), + }) + if err != nil { + return nil, err + } + add(cmd, kind) + } + + // extract method + if kind := settings.RefactorExtractMethod; methodOk && enabled(kind) { + cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ + Fix: fixExtractMethod, + URI: pgf.URI, + Range: rng, + ResolveEdits: supportsResolveEdits(options), + }) + if err != nil { + return nil, err + } + add(cmd, kind) + } } - commands = append(commands, cmd) - if methodOk { - cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ - Fix: fixExtractMethod, - URI: puri, + } + + // extract variable + if kind := settings.RefactorExtractVariable; enabled(kind) { + if _, _, ok, _ := canExtractVariable(start, end, pgf.File); ok { + cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ + Fix: fixExtractVariable, + URI: pgf.URI, Range: rng, ResolveEdits: supportsResolveEdits(options), }) if err != nil { return nil, err } - commands = append(commands, cmd) - } - } - if _, _, ok, _ := canExtractVariable(start, end, pgf.File); ok { - cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ - Fix: fixExtractVariable, - URI: puri, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err + add(cmd, kind) } - commands = append(commands, cmd) } - if canExtractToNewFile(pgf, start, end) { - cmd, err := command.NewExtractToNewFileCommand( - "Extract declarations to new file", - protocol.Location{URI: pgf.URI, Range: rng}, - ) - if err != nil { - return nil, err + + // extract to new file + if kind := settings.RefactorExtractToNewFile; enabled(kind) { + if canExtractToNewFile(pgf, start, end) { + cmd, err := command.NewExtractToNewFileCommand( + "Extract declarations to new file", + protocol.Location{URI: pgf.URI, Range: rng}, + ) + if err != nil { + return nil, err + } + add(cmd, kind) } - commands = append(commands, cmd) - } - var actions []protocol.CodeAction - for i := range commands { - actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorExtract, &commands[i], nil, options)) } + return actions, nil } @@ -335,7 +368,7 @@ func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Com return action } -func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *cache.Snapshot, pgf *parsego.File, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { +func getRewriteCodeActions(enabled func(protocol.CodeActionKind) bool, ctx context.Context, pkg *cache.Package, snapshot *cache.Snapshot, pgf *parsego.File, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { // golang/go#61693: code actions were refactored to run outside of the // analysis framework, but as a result they lost their panic recovery. // @@ -348,9 +381,14 @@ func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *ca }() var actions []protocol.CodeAction + add := func(cmd protocol.Command, kind protocol.CodeActionKind) { + action := newCodeAction(cmd.Title, kind, &cmd, nil, options) + actions = append(actions, action) + } - if canRemoveParameter(pkg, pgf, rng) { - cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{ + // remove unused param + if kind := settings.RefactorRewriteRemoveUnusedParam; enabled(kind) && canRemoveParameter(pkg, pgf, rng) { + cmd, err := command.NewChangeSignatureCommand("Refactor: remove unused parameter", command.ChangeSignatureArgs{ RemoveParameter: protocol.Location{ URI: pgf.URI, Range: rng, @@ -360,7 +398,7 @@ func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *ca if err != nil { return nil, err } - actions = append(actions, newCodeAction("Refactor: remove unused parameter", protocol.RefactorRewrite, &cmd, nil, options)) + add(cmd, kind) } start, end, err := pgf.RangePos(rng) @@ -368,61 +406,50 @@ func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *ca return nil, err } - if action, ok := convertStringLiteral(pgf, fh, start, end); ok { - actions = append(actions, action) - } - - var commands []protocol.Command - if _, ok, _ := canInvertIfCondition(pgf.File, start, end); ok { - cmd, err := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{ - Fix: fixInvertIfCondition, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err + // change quote + if enabled(settings.RefactorRewriteChangeQuote) { + if action, ok := convertStringLiteral(pgf, fh, start, end); ok { + actions = append(actions, action) } - commands = append(commands, cmd) } - if msg, ok, _ := canSplitLines(pgf.File, pkg.FileSet(), start, end); ok { - cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ - Fix: fixSplitLines, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err + // invert if condition + if kind := settings.RefactorRewriteInvertIf; enabled(kind) { + if _, ok, _ := canInvertIfCondition(pgf.File, start, end); ok { + cmd, err := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{ + Fix: fixInvertIfCondition, + URI: pgf.URI, + Range: rng, + ResolveEdits: supportsResolveEdits(options), + }) + if err != nil { + return nil, err + } + add(cmd, kind) } - commands = append(commands, cmd) } - if msg, ok, _ := canJoinLines(pgf.File, pkg.FileSet(), start, end); ok { - cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ - Fix: fixJoinLines, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - if err != nil { - return nil, err + // split lines + if kind := settings.RefactorRewriteSplitLines; enabled(kind) { + if msg, ok, _ := canSplitLines(pgf.File, pkg.FileSet(), start, end); ok { + cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ + Fix: fixSplitLines, + URI: pgf.URI, + Range: rng, + ResolveEdits: supportsResolveEdits(options), + }) + if err != nil { + return nil, err + } + add(cmd, kind) } - commands = append(commands, cmd) } - // fillstruct.Diagnose is a lazy analyzer: all it gives us is - // the (start, end, message) of each SuggestedFix; the actual - // edit is computed only later by ApplyFix, which calls fillstruct.SuggestedFix. - for _, diag := range fillstruct.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) { - rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End) - if err != nil { - return nil, err - } - for _, fix := range diag.SuggestedFixes { - cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ - Fix: diag.Category, + // join lines + if kind := settings.RefactorRewriteJoinLines; enabled(kind) { + if msg, ok, _ := canJoinLines(pgf.File, pkg.FileSet(), start, end); ok { + cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ + Fix: fixJoinLines, URI: pgf.URI, Range: rng, ResolveEdits: supportsResolveEdits(options), @@ -430,23 +457,49 @@ func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *ca if err != nil { return nil, err } - commands = append(commands, cmd) + add(cmd, kind) } } - for _, diag := range fillswitch.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) { - changes, err := suggestedFixToDocumentChange(ctx, snapshot, pkg.FileSet(), &diag.SuggestedFixes[0]) - if err != nil { - return nil, err + // fill struct + // + // fillstruct.Diagnose is a lazy analyzer: all it gives us is + // the (start, end, message) of each SuggestedFix; the actual + // edit is computed only later by ApplyFix, which calls fillstruct.SuggestedFix. + if kind := settings.RefactorRewriteFillStruct; enabled(kind) { + for _, diag := range fillstruct.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) { + rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End) + if err != nil { + return nil, err + } + for _, fix := range diag.SuggestedFixes { + cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ + Fix: diag.Category, + URI: pgf.URI, + Range: rng, + ResolveEdits: supportsResolveEdits(options), + }) + if err != nil { + return nil, err + } + add(cmd, kind) + } } - actions = append(actions, protocol.CodeAction{ - Title: diag.Message, - Kind: protocol.RefactorRewrite, - Edit: protocol.NewWorkspaceEdit(changes...), - }) } - for i := range commands { - actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorRewrite, &commands[i], nil, options)) + + // fill switch + if kind := settings.RefactorRewriteFillSwitch; enabled(kind) { + for _, diag := range fillswitch.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) { + changes, err := suggestedFixToDocumentChange(ctx, snapshot, pkg.FileSet(), &diag.SuggestedFixes[0]) + if err != nil { + return nil, err + } + actions = append(actions, protocol.CodeAction{ + Title: diag.Message, + Kind: kind, + Edit: protocol.NewWorkspaceEdit(changes...), + }) + } } return actions, nil @@ -502,32 +555,35 @@ func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Rang } // getInlineCodeActions returns refactor.inline actions available at the specified range. -func getInlineCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err +func getInlineCodeActions(enabled func(protocol.CodeActionKind) bool, pkg *cache.Package, pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { + var actions []protocol.CodeAction + add := func(cmd protocol.Command, kind protocol.CodeActionKind) { + action := newCodeAction(cmd.Title, kind, &cmd, nil, options) + actions = append(actions, action) } - // If range is within call expression, offer to inline the call. - var commands []protocol.Command - if _, fn, err := enclosingStaticCall(pkg, pgf, start, end); err == nil { - cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ - Fix: fixInlineCall, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) + // inline call + if kind := settings.RefactorInlineCall; enabled(kind) { + start, end, err := pgf.RangePos(rng) if err != nil { return nil, err } - commands = append(commands, cmd) - } - // Convert commands to actions. - var actions []protocol.CodeAction - for i := range commands { - actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorInline, &commands[i], nil, options)) + // If range is within call expression, offer to inline the call. + if _, fn, err := enclosingStaticCall(pkg, pgf, start, end); err == nil { + cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ + Fix: fixInlineCall, + URI: pgf.URI, + Range: rng, + ResolveEdits: supportsResolveEdits(options), + }) + if err != nil { + return nil, err + } + add(cmd, kind) + } } + return actions, nil } diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go index 15f659074b1..5fc0fb6aa21 100644 --- a/gopls/internal/server/code_action.go +++ b/gopls/internal/server/code_action.go @@ -31,56 +31,72 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara } defer release() uri := fh.URI() - - // Determine the supported actions for this file kind. kind := snapshot.FileKind(fh) - supportedCodeActions, ok := snapshot.Options().SupportedCodeActions[kind] + + // Determine the supported code action kinds for this file. + // + // We interpret CodeActionKinds hierarchically, so refactor.rewrite + // subsumes refactor.rewrite.change_quote, for example. + // See ../protocol/codeactionkind.go for some code action theory. + codeActionKinds, ok := snapshot.Options().SupportedCodeActions[kind] if !ok { return nil, fmt.Errorf("no supported code actions for %v file kind", kind) } - if len(supportedCodeActions) == 0 { - return nil, nil // not an error if there are none supported - } - // The Only field of the context specifies which code actions the client wants. - // If Only is empty, assume that the client wants all of the non-explicit code actions. - want := supportedCodeActions + // The Only field of the context specifies which code actions + // the client wants. If Only is empty, assume the client wants + // all supported code actions. if len(params.Context.Only) > 0 { - want = make(map[protocol.CodeActionKind]bool) - - // Explicit Code Actions are opt-in and shouldn't be - // returned to the client unless requested using Only. - // - // This mechanim exists to avoid a distracting - // lightbulb (code action) on each Test function. - // These actions are unwanted in VS Code because it - // has Test Explorer, and in other editors because - // the UX of executeCommand is unsatisfactory for tests: - // it doesn't show the complete streaming output. - // See https://github.com/joaotavora/eglot/discussions/1402 - // for a better solution. - explicit := map[protocol.CodeActionKind]bool{ - settings.GoTest: true, + codeActionKinds = make(map[protocol.CodeActionKind]bool) + for _, kind := range params.Context.Only { + codeActionKinds[kind] = true } + } - for _, only := range params.Context.Only { - for k, v := range supportedCodeActions { - if only == k || strings.HasPrefix(string(k), string(only)+".") { - want[k] = want[k] || v - } + // enabled reports whether the specified kind of code action is required. + enabled := func(kind protocol.CodeActionKind) bool { + // Given "refactor.rewrite.foo", check for it, + // then "refactor.rewrite", "refactor". + // A false map entry prunes the search for ancestors. + for { + if v, ok := codeActionKinds[kind]; ok { + return v + } + dot := strings.LastIndexByte(string(kind), '.') + if dot < 0 { + return false + } + + // The "source.test" code action shouldn't be + // returned to the client unless requested by + // an exact match in Only. + // + // This mechanism exists to avoid a distracting + // lightbulb (code action) on each Test function. + // These actions are unwanted in VS Code because it + // has Test Explorer, and in other editors because + // the UX of executeCommand is unsatisfactory for tests: + // it doesn't show the complete streaming output. + // See https://github.com/joaotavora/eglot/discussions/1402 + // for a better solution. See also + // https://github.com/golang/go/issues/67400. + // + // TODO(adonovan): consider instead switching on + // codeActionTriggerKind. Perhaps other noisy Source + // Actions should be guarded in the same way. + if kind == settings.GoTest { + return false // don't search ancestors } - want[only] = want[only] || explicit[only] + + kind = kind[:dot] } } - if len(want) == 0 { - return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only) - } switch kind { case file.Mod: var actions []protocol.CodeAction - fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, want) + fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, enabled) if err != nil { return nil, err } @@ -117,7 +133,7 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara // Note s.codeActionsMatchingDiagnostics returns only fixes // detected during the analysis phase. golang.CodeActions computes // extra changes that can address some diagnostics. - actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, params.Context.Diagnostics, want) + actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, params.Context.Diagnostics, enabled) if err != nil { return nil, err } @@ -127,7 +143,7 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara if k := params.Context.TriggerKind; k != nil { // (some clients omit it) trigger = *k } - moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, want, trigger) + moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, enabled, trigger) if err != nil { return nil, err } @@ -206,14 +222,14 @@ func (s *server) ResolveCodeAction(ctx context.Context, ca *protocol.CodeAction) // protocol.Diagnostic.Data field or, if there were none, by creating // actions from edits associated with a matching Diagnostic from the // set of stored diagnostics for this file. -func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protocol.DocumentURI, snapshot *cache.Snapshot, pds []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { +func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protocol.DocumentURI, snapshot *cache.Snapshot, pds []protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool) ([]protocol.CodeAction, error) { var actions []protocol.CodeAction var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field for _, pd := range pds { bundled := cache.BundledLazyFixes(pd) if len(bundled) > 0 { for _, fix := range bundled { - if want[fix.Kind] { + if enabled(fix.Kind) { actions = append(actions, fix) } } @@ -225,7 +241,7 @@ func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protoco for _, pd := range unbundled { for _, sd := range s.findMatchingDiagnostics(uri, pd) { - diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, &pd, want) + diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, &pd, enabled) if err != nil { return nil, err } @@ -235,10 +251,10 @@ func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protoco return actions, nil } -func codeActionsForDiagnostic(ctx context.Context, snapshot *cache.Snapshot, sd *cache.Diagnostic, pd *protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { +func codeActionsForDiagnostic(ctx context.Context, snapshot *cache.Snapshot, sd *cache.Diagnostic, pd *protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool) ([]protocol.CodeAction, error) { var actions []protocol.CodeAction for _, fix := range sd.SuggestedFixes { - if !want[fix.ActionKind] { + if !enabled(fix.ActionKind) { continue } var changes []protocol.DocumentChange diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index 7cc13229279..e8e29d8ddb8 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go @@ -12,16 +12,28 @@ import "golang.org/x/tools/gopls/internal/protocol" // // See ../protocol/tsprotocol.go for LSP standard kinds, including // -// "quickfix" -// "refactor" -// "refactor.extract" -// "refactor.inline" -// "refactor.move" -// "refactor.rewrite" -// "source" -// "source.organizeImports" -// "source.fixAll" -// "notebook" +// quickfix +// refactor +// refactor.extract +// refactor.inline +// refactor.move +// refactor.rewrite +// source +// source.organizeImports +// source.fixAll +// notebook +// +// Kinds are hierarchical: "refactor" subsumes "refactor.inline", +// which subsumes "refactor.inline.call". The "Only" field in a +// CodeAction request may specify a category such as "refactor"; any +// matching code action will be returned. +// +// All CodeActions returned by gopls use a specific leaf kind such as +// "refactor.inline.call", except for quick fixes, which all use +// "quickfix". TODO(adonovan): perhaps quick fixes should also be +// hierarchical (e.g. quickfix.govulncheck.{reset,upgrade}). +// +// # VS Code // // The effects of CodeActionKind on the behavior of VS Code are // baffling and undocumented. Here's what we have observed. @@ -54,32 +66,33 @@ import "golang.org/x/tools/gopls/internal/protocol" // are unambiguously safe to apply so that clients may automatically // apply all actions matching this category on save. (That said, this // is not VS Code's default behavior; see editor.codeActionsOnSave.) -// -// TODO(adonovan): the intent of CodeActionKind is a hierarchy. We -// should changes gopls so that we don't create instances of the -// predefined kinds directly, but treat them as interfaces. -// -// For example, -// -// instead of: we should create: -// refactor.extract refactor.extract.const -// refactor.extract.var -// refactor.extract.func -// refactor.rewrite refactor.rewrite.fillstruct -// refactor.rewrite.unusedparam -// quickfix quickfix.govulncheck.reset -// quickfix.govulncheck.upgrade -// -// etc, so that client editors and scripts can be more specific in -// their requests. -// -// This entails that we use a segmented-path matching operator -// instead of == for CodeActionKinds throughout gopls. -// See golang/go#40438 for related discussion. const ( - GoAssembly protocol.CodeActionKind = "source.assembly" - GoDoc protocol.CodeActionKind = "source.doc" - GoFreeSymbols protocol.CodeActionKind = "source.freesymbols" - GoTest protocol.CodeActionKind = "source.test" + // source + GoAssembly protocol.CodeActionKind = "source.assembly" + GoDoc protocol.CodeActionKind = "source.doc" + GoFreeSymbols protocol.CodeActionKind = "source.freesymbols" + GoTest protocol.CodeActionKind = "source.test" + + // gopls GoplsDocFeatures protocol.CodeActionKind = "gopls.doc.features" + + // refactor.rewrite + RefactorRewriteChangeQuote protocol.CodeActionKind = "refactor.rewrite.changeQuote" + RefactorRewriteFillStruct protocol.CodeActionKind = "refactor.rewrite.fillStruct" + RefactorRewriteFillSwitch protocol.CodeActionKind = "refactor.rewrite.fillSwitch" + RefactorRewriteInvertIf protocol.CodeActionKind = "refactor.rewrite.invertIf" + RefactorRewriteJoinLines protocol.CodeActionKind = "refactor.rewrite.joinLines" + RefactorRewriteRemoveUnusedParam protocol.CodeActionKind = "refactor.rewrite.removeUnusedParam" + RefactorRewriteSplitLines protocol.CodeActionKind = "refactor.rewrite.splitLines" + + // refactor.inline + RefactorInlineCall protocol.CodeActionKind = "refactor.inline.call" + + // refactor.extract + RefactorExtractFunction protocol.CodeActionKind = "refactor.extract.function" + RefactorExtractMethod protocol.CodeActionKind = "refactor.extract.method" + RefactorExtractVariable protocol.CodeActionKind = "refactor.extract.variable" + RefactorExtractToNewFile protocol.CodeActionKind = "refactor.extract.toNewFile" + + // Note: add new kinds to the SupportedCodeActions map in defaults.go too. ) diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index 9641613cd5d..25f3eae80f5 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -43,17 +43,29 @@ func DefaultOptions(overrides ...func(*Options)) *Options { ServerOptions: ServerOptions{ SupportedCodeActions: map[file.Kind]map[protocol.CodeActionKind]bool{ file.Go: { - protocol.SourceFixAll: true, - protocol.SourceOrganizeImports: true, - protocol.QuickFix: true, - protocol.RefactorRewrite: true, - protocol.RefactorInline: true, - protocol.RefactorExtract: true, - GoAssembly: true, - GoDoc: true, - GoFreeSymbols: true, + // This should include specific leaves in the tree, + // (e.g. refactor.inline.call) not generic branches + // (e.g. refactor.inline or refactor). + protocol.SourceFixAll: true, + protocol.SourceOrganizeImports: true, + protocol.QuickFix: true, + GoAssembly: true, + GoDoc: true, + GoFreeSymbols: true, + GoplsDocFeatures: true, + RefactorRewriteChangeQuote: true, + RefactorRewriteFillStruct: true, + RefactorRewriteFillSwitch: true, + RefactorRewriteInvertIf: true, + RefactorRewriteJoinLines: true, + RefactorRewriteRemoveUnusedParam: true, + RefactorRewriteSplitLines: true, + RefactorInlineCall: true, + RefactorExtractFunction: true, + RefactorExtractMethod: true, + RefactorExtractVariable: true, + RefactorExtractToNewFile: true, // Not GoTest: it must be explicit in CodeActionParams.Context.Only - GoplsDocFeatures: true, }, file.Mod: { protocol.SourceOrganizeImports: true, diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index 981abce89e2..0ded9aa04b5 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -1026,16 +1026,6 @@ func (e *Editor) applyCodeActions(ctx context.Context, loc protocol.Location, di if action.Title == "" { return 0, fmt.Errorf("empty title for code action") } - var match bool - for _, o := range only { - if action.Kind == o { - match = true - break - } - } - if !match { - continue - } applied++ if err := e.ApplyCodeAction(ctx, action); err != nil { return 0, err diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go index 70091150a87..354921afc01 100644 --- a/gopls/internal/test/integration/misc/codeactions_test.go +++ b/gopls/internal/test/integration/misc/codeactions_test.go @@ -68,8 +68,8 @@ func g() {} settings.GoDoc, settings.GoFreeSymbols, settings.GoplsDocFeatures, - protocol.RefactorExtract, - protocol.RefactorInline) + settings.RefactorExtractVariable, + settings.RefactorInlineCall) check("gen/a.go", settings.GoAssembly, settings.GoDoc, @@ -78,7 +78,7 @@ func g() {} }) } -// Test refactor.inline is not included in automatically triggered code action +// Test refactor.inline.call is not included in automatically triggered code action // unless users want refactoring. func TestVSCodeIssue65167(t *testing.T) { const vim1 = `package main @@ -108,9 +108,9 @@ func Func() int { return 0 } actions := env.CodeAction(loc, nil, trigger) want := trigger != protocol.CodeActionAutomatic || selectedRange if got := slices.ContainsFunc(actions, func(act protocol.CodeAction) bool { - return act.Kind == protocol.RefactorInline + return act.Kind == settings.RefactorInlineCall }); got != want { - t.Errorf("got refactor.inline = %t, want %t", got, want) + t.Errorf("got refactor.inline.call = %t, want %t", got, want) } }) } diff --git a/gopls/internal/test/integration/misc/extract_test.go b/gopls/internal/test/integration/misc/extract_test.go index ec13856361e..569d53e8bba 100644 --- a/gopls/internal/test/integration/misc/extract_test.go +++ b/gopls/internal/test/integration/misc/extract_test.go @@ -7,6 +7,7 @@ package misc import ( "testing" + "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/test/compare" . "golang.org/x/tools/gopls/internal/test/integration" @@ -38,7 +39,7 @@ func Foo() int { // Find the extract function code action. var extractFunc *protocol.CodeAction for _, action := range actions { - if action.Kind == protocol.RefactorExtract && action.Title == "Extract function" { + if action.Kind == settings.RefactorExtractFunction { extractFunc = &action break } diff --git a/gopls/internal/test/integration/misc/fix_test.go b/gopls/internal/test/integration/misc/fix_test.go index acf896a9adb..5a01afe2400 100644 --- a/gopls/internal/test/integration/misc/fix_test.go +++ b/gopls/internal/test/integration/misc/fix_test.go @@ -7,6 +7,7 @@ package misc import ( "testing" + "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/test/compare" . "golang.org/x/tools/gopls/internal/test/integration" @@ -49,7 +50,7 @@ func Foo() { runner.Run(t, basic, func(t *testing.T, env *Env) { env.OpenFile("main.go") - fixes, err := env.Editor.CodeActions(env.Ctx, env.RegexpSearch("main.go", "Info{}"), nil, protocol.RefactorRewrite) + fixes, err := env.Editor.CodeActions(env.Ctx, env.RegexpSearch("main.go", "Info{}"), nil, settings.RefactorRewriteFillStruct) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index 8105fd06896..f8038f5721d 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go @@ -484,7 +484,7 @@ func checkMatch(t *testing.T, want bool, got []byte, pattern string) { } } -// codeActionByKind returns the first action of the specified kind, or an error. +// codeActionByKind returns the first action of (exactly) the specified kind, or an error. func codeActionByKind(actions []protocol.CodeAction, kind protocol.CodeActionKind) (*protocol.CodeAction, error) { for _, act := range actions { if act.Kind == kind { diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go index e71bd5ba6d2..5bcb31b46de 100644 --- a/gopls/internal/test/marker/doc.go +++ b/gopls/internal/test/marker/doc.go @@ -100,17 +100,15 @@ The following markers are supported within marker tests: completion candidate produced at the given location with provided label results in the given golden state. - - codeaction(start, end, kind, golden, ...titles): specifies a code action + - codeaction(start, end, kind, golden): specifies a code action to request for the given range. To support multi-line ranges, the range is defined to be between start.Start and end.End. The golden directory contains changed file content after the code action is applied. - If titles are provided, they are used to filter the matching code - action. TODO(rfindley): now that 'location' supports multi-line matches, replace uses of 'codeaction' with codeactionedit. - - codeactionedit(location, kind, golden, ...titles): a shorter form of + - codeactionedit(location, kind, golden): a shorter form of codeaction. Invokes a code action of the given kind for the given in-line range, and compares the resulting formatted unified *edits* (notably, not the full file content) with the golden directory. diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index 87aecfaf6ed..1896d8fb19f 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -1921,13 +1921,13 @@ func changedFiles(env *integration.Env, changes []protocol.DocumentChange) (map[ return result, nil } -func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden, titles ...string) { +func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden) { // Request the range from start.Start to end.End. loc := start loc.Range.End = end.Range.End // Apply the fix it suggests. - changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) + changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil) if err != nil { mark.errorf("codeAction failed: %v", err) return @@ -1937,8 +1937,8 @@ func codeActionMarker(mark marker, start, end protocol.Location, actionKind stri checkChangedFiles(mark, changed, g) } -func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden, titles ...string) { - changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) +func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden) { + changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil) if err != nil { mark.errorf("codeAction failed: %v", err) return @@ -1950,7 +1950,7 @@ func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr stringMatcher) { loc := start loc.Range.End = end.Range.End - _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, nil) + _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil) wantErr.checkErr(mark, err) } @@ -2034,7 +2034,7 @@ func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, g } // Apply the fix it suggests. - changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) + changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag) if err != nil { mark.errorf("suggestedfix failed: %v. (Use @suggestedfixerr for expected errors.)", err) return @@ -2054,7 +2054,7 @@ func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp } // Apply the fix it suggests. - _, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) + _, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag) wantErr.checkErr(mark, err) } @@ -2065,8 +2065,8 @@ func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp // The resulting map contains resulting file contents after the code action is // applied. Currently, this function does not support code actions that return // edits directly; it only supports code action commands. -func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) (map[string][]byte, error) { - changes, err := codeActionChanges(env, uri, rng, actionKind, diag, titles) +func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic) (map[string][]byte, error) { + changes, err := codeActionChanges(env, uri, rng, actionKind, diag) if err != nil { return nil, err } @@ -2076,8 +2076,7 @@ func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Ran // codeActionChanges executes a textDocument/codeAction request for the // specified location and kind, and captures the resulting document changes. // If diag is non-nil, it is used as the code action context. -// If titles is non-empty, the code action title must be present among the provided titles. -func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) ([]protocol.DocumentChange, error) { +func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic) ([]protocol.DocumentChange, error) { // Request all code actions that apply to the diagnostic. // (The protocol supports filtering using Context.Only={actionKind} // but we can give a better error if we don't filter.) @@ -2097,27 +2096,19 @@ func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng proto return nil, err } - // Find the sole candidates CodeAction of the specified kind (e.g. refactor.rewrite). + // Find the sole candidate CodeAction of exactly the specified kind + // (e.g. refactor.inline.call). var candidates []protocol.CodeAction for _, act := range actions { if act.Kind == protocol.CodeActionKind(actionKind) { - if len(titles) > 0 { - for _, f := range titles { - if act.Title == f { - candidates = append(candidates, act) - break - } - } - } else { - candidates = append(candidates, act) - } + candidates = append(candidates, act) } } if len(candidates) != 1 { for _, act := range actions { env.T.Logf("found CodeAction Kind=%s Title=%q", act.Kind, act.Title) } - return nil, fmt.Errorf("found %d CodeActions of kind %s matching filters %v for this diagnostic, want 1", len(candidates), actionKind, titles) + return nil, fmt.Errorf("found %d CodeActions of kind %s for this diagnostic, want 1", len(candidates), actionKind) } action := candidates[0] diff --git a/gopls/internal/test/marker/testdata/codeaction/change_quote.txt b/gopls/internal/test/marker/testdata/codeaction/change_quote.txt index 0fa144c1e56..a3b4f8d4c83 100644 --- a/gopls/internal/test/marker/testdata/codeaction/change_quote.txt +++ b/gopls/internal/test/marker/testdata/codeaction/change_quote.txt @@ -17,53 +17,53 @@ import ( func foo() { var s string - s = "hello" //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") - s = `hello` //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") - s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") - s = `hello world` //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") - s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") + s = "hello" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a1) + s = `hello` //@codeactionedit("`", "refactor.rewrite.changeQuote", a2) + s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a3) + s = `hello world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a4) + s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a5) // add a comment to avoid affect diff compute s = `hello -world` //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") - s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") - s = `hello"world` //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") - s = "hello\x1bworld" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") - s = "hello`world" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") - s = "hello\x7fworld" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") +world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a6) + s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a7) + s = `hello"world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a8) + s = "hello\x1bworld" //@codeactionerr(`"`, "", "refactor.rewrite.changeQuote", re"found 0 CodeActions") + s = "hello`world" //@codeactionerr(`"`, "", "refactor.rewrite.changeQuote", re"found 0 CodeActions") + s = "hello\x7fworld" //@codeactionerr(`"`, "", "refactor.rewrite.changeQuote", re"found 0 CodeActions") fmt.Println(s) } -- @a1/a.go -- @@ -9 +9 @@ -- s = "hello" //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") -+ s = `hello` //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") +- s = "hello" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a1) ++ s = `hello` //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a1) -- @a2/a.go -- @@ -10 +10 @@ -- s = `hello` //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") -+ s = "hello" //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") +- s = `hello` //@codeactionedit("`", "refactor.rewrite.changeQuote", a2) ++ s = "hello" //@codeactionedit("`", "refactor.rewrite.changeQuote", a2) -- @a3/a.go -- @@ -11 +11 @@ -- s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") -+ s = `hello world` //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") +- s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a3) ++ s = `hello world` //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a3) -- @a4/a.go -- @@ -12 +12 @@ -- s = `hello world` //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") -+ s = "hello\tworld" //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") +- s = `hello world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a4) ++ s = "hello\tworld" //@codeactionedit("`", "refactor.rewrite.changeQuote", a4) -- @a5/a.go -- @@ -13 +13,2 @@ -- s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") +- s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a5) + s = `hello -+world` //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") ++world` //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a5) -- @a6/a.go -- @@ -15,2 +15 @@ - s = `hello --world` //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") -+ s = "hello\nworld" //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") +-world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a6) ++ s = "hello\nworld" //@codeactionedit("`", "refactor.rewrite.changeQuote", a6) -- @a7/a.go -- @@ -17 +17 @@ -- s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") -+ s = `hello"world` //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") +- s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a7) ++ s = `hello"world` //@codeactionedit(`"`, "refactor.rewrite.changeQuote", a7) -- @a8/a.go -- @@ -18 +18 @@ -- s = `hello"world` //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") -+ s = "hello\"world" //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") +- s = `hello"world` //@codeactionedit("`", "refactor.rewrite.changeQuote", a8) ++ s = "hello\"world" //@codeactionedit("`", "refactor.rewrite.changeQuote", a8) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt index 1c9df2ec5bb..d035119bc3a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt @@ -9,7 +9,7 @@ go 1.18 -- a/a.go -- package a -//@codeactionedit(block, "refactor.extract", out, "Extract function") +//@codeactionedit(block, "refactor.extract.function", out) func _() { var logf func(string, ...any) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt index 75800504006..7cb22d1577d 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt @@ -6,18 +6,18 @@ This test exercises function and method extraction. -- basic.go -- package extract -//@codeactionedit(A_XLessThanYP, "refactor.extract", meth1, "Extract method") -//@codeactionedit(A_XLessThanYP, "refactor.extract", func1, "Extract function") -//@codeactionedit(A_AddP1, "refactor.extract", meth2, "Extract method") -//@codeactionedit(A_AddP1, "refactor.extract", func2, "Extract function") -//@codeactionedit(A_AddP2, "refactor.extract", meth3, "Extract method") -//@codeactionedit(A_AddP2, "refactor.extract", func3, "Extract function") -//@codeactionedit(A_XLessThanY, "refactor.extract", meth4, "Extract method") -//@codeactionedit(A_XLessThanY, "refactor.extract", func4, "Extract function") -//@codeactionedit(A_Add1, "refactor.extract", meth5, "Extract method") -//@codeactionedit(A_Add1, "refactor.extract", func5, "Extract function") -//@codeactionedit(A_Add2, "refactor.extract", meth6, "Extract method") -//@codeactionedit(A_Add2, "refactor.extract", func6, "Extract function") +//@codeactionedit(A_XLessThanYP, "refactor.extract.method", meth1) +//@codeactionedit(A_XLessThanYP, "refactor.extract.function", func1) +//@codeactionedit(A_AddP1, "refactor.extract.method", meth2) +//@codeactionedit(A_AddP1, "refactor.extract.function", func2) +//@codeactionedit(A_AddP2, "refactor.extract.method", meth3) +//@codeactionedit(A_AddP2, "refactor.extract.function", func3) +//@codeactionedit(A_XLessThanY, "refactor.extract.method", meth4) +//@codeactionedit(A_XLessThanY, "refactor.extract.function", func4) +//@codeactionedit(A_Add1, "refactor.extract.method", meth5) +//@codeactionedit(A_Add1, "refactor.extract.function", func5) +//@codeactionedit(A_Add2, "refactor.extract.method", meth6) +//@codeactionedit(A_Add2, "refactor.extract.function", func6) type A struct { x int @@ -162,18 +162,18 @@ import ( "testing" ) -//@codeactionedit(B_AddP, "refactor.extract", contextMeth1, "Extract method") -//@codeactionedit(B_AddP, "refactor.extract", contextFunc1, "Extract function") -//@codeactionedit(B_LongList, "refactor.extract", contextMeth2, "Extract method") -//@codeactionedit(B_LongList, "refactor.extract", contextFunc2, "Extract function") -//@codeactionedit(B_AddPWithB, "refactor.extract", contextFuncB, "Extract function") -//@codeactionedit(B_LongListWithT, "refactor.extract", contextFuncT, "Extract function") +//@codeactionedit(B_AddP, "refactor.extract.method", contextMeth1) +//@codeactionedit(B_AddP, "refactor.extract.function", contextFunc1) +//@codeactionedit(B_LongList, "refactor.extract.method", contextMeth2) +//@codeactionedit(B_LongList, "refactor.extract.function", contextFunc2) +//@codeactionedit(B_AddPWithB, "refactor.extract.function", contextFuncB) +//@codeactionedit(B_LongListWithT, "refactor.extract.function", contextFuncT) type B struct { x int y int } - + func (b *B) AddP(ctx context.Context) (int, error) { sum := b.x + b.y return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt index c3e75defc51..259b84a09a3 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable-67905.txt @@ -16,7 +16,7 @@ import ( func f() io.Reader func main() { - switch r := f().(type) { //@codeactionedit("f()", "refactor.extract", type_switch_func_call) + switch r := f().(type) { //@codeactionedit("f()", "refactor.extract.variable", type_switch_func_call) default: _ = r } @@ -24,6 +24,6 @@ func main() { -- @type_switch_func_call/extract_switch.go -- @@ -10 +10,2 @@ -- switch r := f().(type) { //@codeactionedit("f()", "refactor.extract", type_switch_func_call) +- switch r := f().(type) { //@codeactionedit("f()", "refactor.extract.variable", type_switch_func_call) + x := f() -+ switch r := x.(type) { //@codeactionedit("f()", "refactor.extract", type_switch_func_call) ++ switch r := x.(type) { //@codeactionedit("f()", "refactor.extract.variable", type_switch_func_call) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt index 685b4ff9372..8c500d02c1e 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt @@ -8,41 +8,41 @@ See extract_variable_resolve.txt for the same test with resolve support. package extract func _() { - var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) - var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) + var _ = 1 + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) + var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) } -- @basic_lit1/basic_lit.go -- @@ -4 +4,2 @@ -- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) + x := 1 -+ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) ++ var _ = x + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) -- @basic_lit2/basic_lit.go -- @@ -5 +5,2 @@ -- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) + x := 3 + 4 -+ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) ++ var _ = x //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) -- func_call.go -- package extract import "strconv" func _() { - x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) + x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) str := "1" - b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) + b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) } -- @func_call1/func_call.go -- @@ -6 +6,2 @@ -- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) + x := append([]int{}, 1) -+ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) ++ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) -- @func_call2/func_call.go -- @@ -8 +8,2 @@ -- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) + x, x1 := strconv.Atoi(str) -+ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) ++ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) -- scope.go -- package extract @@ -51,20 +51,20 @@ import "go/ast" func _() { x0 := 0 if true { - y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) + y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) } if true { - x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) + x1 := !false //@codeactionedit("!false", "refactor.extract.variable", scope2) } } -- @scope1/scope.go -- @@ -8 +8,2 @@ -- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) + x := ast.CompositeLit{} -+ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) ++ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) -- @scope2/scope.go -- @@ -11 +11,2 @@ -- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) +- x1 := !false //@codeactionedit("!false", "refactor.extract.variable", scope2) + x := !false -+ x1 := x //@codeactionedit("!false", "refactor.extract", scope2) ++ x1 := x //@codeactionedit("!false", "refactor.extract.variable", scope2) diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt index dc6ad787afb..b3a9a67059f 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt @@ -19,41 +19,41 @@ See extract_variable.txt for the same test without resolve support. package extract func _() { - var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) - var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) + var _ = 1 + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) + var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) } -- @basic_lit1/basic_lit.go -- @@ -4 +4,2 @@ -- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) + x := 1 -+ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) ++ var _ = x + 2 //@codeactionedit("1", "refactor.extract.variable", basic_lit1) -- @basic_lit2/basic_lit.go -- @@ -5 +5,2 @@ -- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) + x := 3 + 4 -+ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) ++ var _ = x //@codeactionedit("3 + 4", "refactor.extract.variable", basic_lit2) -- func_call.go -- package extract import "strconv" func _() { - x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) + x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) str := "1" - b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) + b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) } -- @func_call1/func_call.go -- @@ -6 +6,2 @@ -- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) + x := append([]int{}, 1) -+ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) ++ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract.variable", func_call1) -- @func_call2/func_call.go -- @@ -8 +8,2 @@ -- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) + x, x1 := strconv.Atoi(str) -+ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) ++ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract.variable", func_call2) -- scope.go -- package extract @@ -62,20 +62,20 @@ import "go/ast" func _() { x0 := 0 if true { - y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) + y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) } if true { - x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) + x1 := !false //@codeactionedit("!false", "refactor.extract.variable", scope2) } } -- @scope1/scope.go -- @@ -8 +8,2 @@ -- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) + x := ast.CompositeLit{} -+ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) ++ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract.variable", scope1) -- @scope2/scope.go -- @@ -11 +11,2 @@ -- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) +- x1 := !false //@codeactionedit("!false", "refactor.extract.variable", scope2) + x := !false -+ x1 := x //@codeactionedit("!false", "refactor.extract", scope2) ++ x1 := x //@codeactionedit("!false", "refactor.extract.variable", scope2) diff --git a/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt b/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt index 0226d8207d1..158a9f9a22c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt @@ -12,28 +12,28 @@ go 1.18 package main // docs -func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration) +func fn() {} //@codeactionedit("func", "refactor.extract.toNewFile", function_declaration) -func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name) +func fn2() {} //@codeactionedit("fn2", "refactor.extract.toNewFile", only_select_func_name) -func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name) +func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract.toNewFile", zero_width_selection_on_func_name) // docs -type T int //@codeactionedit("type", "refactor.extract", type_declaration) +type T int //@codeactionedit("type", "refactor.extract.toNewFile", type_declaration) // docs -var V int //@codeactionedit("var", "refactor.extract", var_declaration) +var V int //@codeactionedit("var", "refactor.extract.toNewFile", var_declaration) // docs -const K = "" //@codeactionedit("const", "refactor.extract", const_declaration) +const K = "" //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration) -const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs) +const ( //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration_multiple_specs) P = iota Q R ) -func fnA () {} //@codeaction("func", mdEnd, "refactor.extract", multiple_declarations) +func fnA () {} //@codeaction("func", mdEnd, "refactor.extract.toNewFile", multiple_declarations) // unattached comment @@ -45,13 +45,13 @@ func fnB () {} //@loc(mdEnd, "}") -- existing2.1.go -- -- b.go -- package main -func existing() {} //@codeactionedit("func", "refactor.extract", file_name_conflict) -func existing2() {} //@codeactionedit("func", "refactor.extract", file_name_conflict_again) +func existing() {} //@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict) +func existing2() {} //@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict_again) -- single_import.go -- package main import "fmt" -func F() { //@codeactionedit("func", "refactor.extract", single_import) +func F() { //@codeactionedit("func", "refactor.extract.toNewFile", single_import) fmt.Println() } @@ -65,24 +65,24 @@ import ( func init(){ log.Println() } -func F() { //@codeactionedit("func", "refactor.extract", multiple_imports) +func F() { //@codeactionedit("func", "refactor.extract.toNewFile", multiple_imports) fmt.Println() } -func g() string{ //@codeactionedit("func", "refactor.extract", renamed_import) +func g() string{ //@codeactionedit("func", "refactor.extract.toNewFile", renamed_import) return time1.Now().string() } -- blank_import.go -- package main import _ "fmt" -func F() {} //@codeactionedit("func", "refactor.extract", blank_import) +func F() {} //@codeactionedit("func", "refactor.extract.toNewFile", blank_import) -- @blank_import/blank_import.go -- @@ -3 +3 @@ --func F() {} //@codeactionedit("func", "refactor.extract", blank_import) -+//@codeactionedit("func", "refactor.extract", blank_import) +-func F() {} //@codeactionedit("func", "refactor.extract.toNewFile", blank_import) ++//@codeactionedit("func", "refactor.extract.toNewFile", blank_import) -- @blank_import/f.go -- @@ -0,0 +1,3 @@ +package main @@ -91,8 +91,8 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) -- @const_declaration/a.go -- @@ -16,2 +16 @@ -// docs --const K = "" //@codeactionedit("const", "refactor.extract", const_declaration) -+//@codeactionedit("const", "refactor.extract", const_declaration) +-const K = "" //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration) ++//@codeactionedit("const", "refactor.extract.toNewFile", const_declaration) -- @const_declaration/k.go -- @@ -0,0 +1,4 @@ +package main @@ -101,7 +101,7 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) +const K = "" -- @const_declaration_multiple_specs/a.go -- @@ -19,6 +19 @@ --const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs) +-const ( //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration_multiple_specs) - P = iota - Q - R @@ -111,15 +111,15 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) @@ -0,0 +1,7 @@ +package main + -+const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs) ++const ( //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration_multiple_specs) + P = iota + Q + R +) -- @file_name_conflict/b.go -- @@ -2 +2 @@ --func existing() {} //@codeactionedit("func", "refactor.extract", file_name_conflict) -+//@codeactionedit("func", "refactor.extract", file_name_conflict) +-func existing() {} //@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict) ++//@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict) -- @file_name_conflict/existing.1.go -- @@ -0,0 +1,3 @@ +package main @@ -127,8 +127,8 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) +func existing() {} -- @file_name_conflict_again/b.go -- @@ -3 +3 @@ --func existing2() {} //@codeactionedit("func", "refactor.extract", file_name_conflict_again) -+//@codeactionedit("func", "refactor.extract", file_name_conflict_again) +-func existing2() {} //@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict_again) ++//@codeactionedit("func", "refactor.extract.toNewFile", file_name_conflict_again) -- @file_name_conflict_again/existing2.2.go -- @@ -0,0 +1,3 @@ +package main @@ -137,8 +137,8 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) -- @function_declaration/a.go -- @@ -3,2 +3 @@ -// docs --func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration) -+//@codeactionedit("func", "refactor.extract", function_declaration) +-func fn() {} //@codeactionedit("func", "refactor.extract.toNewFile", function_declaration) ++//@codeactionedit("func", "refactor.extract.toNewFile", function_declaration) -- @function_declaration/fn.go -- @@ -0,0 +1,4 @@ +package main @@ -149,22 +149,22 @@ func F() {} //@codeactionedit("func", "refactor.extract", blank_import) package main // docs -func fn() {} //@codeactionedit("func", "refactor.extract", function_declaration) +func fn() {} //@codeactionedit("func", "refactor.extract.toNewFile", function_declaration) -func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name) +func fn2() {} //@codeactionedit("fn2", "refactor.extract.toNewFile", only_select_func_name) -func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name) +func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract.toNewFile", zero_width_selection_on_func_name) // docs -type T int //@codeactionedit("type", "refactor.extract", type_declaration) +type T int //@codeactionedit("type", "refactor.extract.toNewFile", type_declaration) // docs -var V int //@codeactionedit("var", "refactor.extract", var_declaration) +var V int //@codeactionedit("var", "refactor.extract.toNewFile", var_declaration) // docs -const K = "" //@codeactionedit("const", "refactor.extract", const_declaration) +const K = "" //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration) -const ( //@codeactionedit("const", "refactor.extract", const_declaration_multiple_specs) +const ( //@codeactionedit("const", "refactor.extract.toNewFile", const_declaration_multiple_specs) P = iota Q R @@ -176,7 +176,7 @@ const ( //@codeactionedit("const", "refactor.extract", const_declaration_multipl -- @multiple_declarations/fna.go -- package main -func fnA() {} //@codeaction("func", mdEnd, "refactor.extract", multiple_declarations) +func fnA() {} //@codeaction("func", mdEnd, "refactor.extract.toNewFile", multiple_declarations) // unattached comment @@ -189,7 +189,7 @@ func fnB() {} + "fmt" +) + -+func F() { //@codeactionedit("func", "refactor.extract", multiple_imports) ++func F() { //@codeactionedit("func", "refactor.extract.toNewFile", multiple_imports) + fmt.Println() +} -- @multiple_imports/multiple_imports.go -- @@ -197,13 +197,13 @@ func fnB() {} - "fmt" + @@ -10,3 +10 @@ --func F() { //@codeactionedit("func", "refactor.extract", multiple_imports) +-func F() { //@codeactionedit("func", "refactor.extract.toNewFile", multiple_imports) - fmt.Println() -} -- @only_select_func_name/a.go -- @@ -6 +6 @@ --func fn2() {} //@codeactionedit("fn2", "refactor.extract", only_select_func_name) -+//@codeactionedit("fn2", "refactor.extract", only_select_func_name) +-func fn2() {} //@codeactionedit("fn2", "refactor.extract.toNewFile", only_select_func_name) ++//@codeactionedit("fn2", "refactor.extract.toNewFile", only_select_func_name) -- @only_select_func_name/fn2.go -- @@ -0,0 +1,3 @@ +package main @@ -217,20 +217,20 @@ func fnB() {} + "fmt" +) + -+func F() { //@codeactionedit("func", "refactor.extract", single_import) ++func F() { //@codeactionedit("func", "refactor.extract.toNewFile", single_import) + fmt.Println() +} -- @single_import/single_import.go -- @@ -2,4 +2 @@ -import "fmt" --func F() { //@codeactionedit("func", "refactor.extract", single_import) +-func F() { //@codeactionedit("func", "refactor.extract.toNewFile", single_import) - fmt.Println() -} -- @type_declaration/a.go -- @@ -10,2 +10 @@ -// docs --type T int //@codeactionedit("type", "refactor.extract", type_declaration) -+//@codeactionedit("type", "refactor.extract", type_declaration) +-type T int //@codeactionedit("type", "refactor.extract.toNewFile", type_declaration) ++//@codeactionedit("type", "refactor.extract.toNewFile", type_declaration) -- @type_declaration/t.go -- @@ -0,0 +1,4 @@ +package main @@ -240,8 +240,8 @@ func fnB() {} -- @var_declaration/a.go -- @@ -13,2 +13 @@ -// docs --var V int //@codeactionedit("var", "refactor.extract", var_declaration) -+//@codeactionedit("var", "refactor.extract", var_declaration) +-var V int //@codeactionedit("var", "refactor.extract.toNewFile", var_declaration) ++//@codeactionedit("var", "refactor.extract.toNewFile", var_declaration) -- @var_declaration/v.go -- @@ -0,0 +1,4 @@ +package main @@ -250,8 +250,8 @@ func fnB() {} +var V int -- @zero_width_selection_on_func_name/a.go -- @@ -8 +8 @@ --func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name) -+//@codeactionedit(re`()fn3`, "refactor.extract", zero_width_selection_on_func_name) +-func fn3() {} //@codeactionedit(re`()fn3`, "refactor.extract.toNewFile", zero_width_selection_on_func_name) ++//@codeactionedit(re`()fn3`, "refactor.extract.toNewFile", zero_width_selection_on_func_name) -- @zero_width_selection_on_func_name/fn3.go -- @@ -0,0 +1,3 @@ +package main @@ -265,7 +265,7 @@ func fnB() {} + time1 "time" +) + -+func g() string { //@codeactionedit("func", "refactor.extract", renamed_import) ++func g() string { //@codeactionedit("func", "refactor.extract.toNewFile", renamed_import) + return time1.Now().string() +} -- @renamed_import/multiple_imports.go -- @@ -273,7 +273,7 @@ func fnB() {} - time1 "time" + @@ -13,4 +13 @@ --func g() string{ //@codeactionedit("func", "refactor.extract", renamed_import) +-func g() string{ //@codeactionedit("func", "refactor.extract.toNewFile", renamed_import) - return time1.Now().string() -} - diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt index deac1d78507..2b947bf8bbc 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt @@ -28,49 +28,49 @@ type basicStruct struct { foo int } -var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) type nestedStruct struct { bar string basic basicStruct } -var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) -var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +var _ = data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) -- @a1/a.go -- @@ -11 +11,3 @@ --var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +-var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) +var _ = basicStruct{ + foo: 0, -+} //@codeactionedit("}", "refactor.rewrite", a1) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) -- @a2/a.go -- @@ -18 +18,4 @@ --var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +-var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) +var _ = twoArgStruct{ + foo: 0, + bar: "", -+} //@codeactionedit("}", "refactor.rewrite", a2) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) -- @a3/a.go -- @@ -25 +25,4 @@ --var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +-var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) +var _ = nestedStruct{ + bar: "", + basic: basicStruct{}, -+} //@codeactionedit("}", "refactor.rewrite", a3) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) -- @a4/a.go -- @@ -27 +27,3 @@ --var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +-var _ = data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) +var _ = data.B{ + ExportedInt: 0, -+} //@codeactionedit("}", "refactor.rewrite", a4) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) -- a2.go -- package fillstruct @@ -82,57 +82,57 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) type funStructComplex struct { fn func(i int, s string) (string, int) } -var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) -- @a21/a2.go -- @@ -11 +11,7 @@ --var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +-var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) +var _ = typedStruct{ + m: map[string]int{}, + s: []int{}, + c: make(chan int), + c1: make(<-chan int), + a: [2]string{}, -+} //@codeactionedit("}", "refactor.rewrite", a21) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) -- @a22/a2.go -- @@ -17 +17,4 @@ --var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +-var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) +var _ = funStruct{ + fn: func(i int) int { + }, -+} //@codeactionedit("}", "refactor.rewrite", a22) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) -- @a23/a2.go -- @@ -23 +23,4 @@ --var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +-var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) +var _ = funStructComplex{ + fn: func(i int, s string) (string, int) { + }, -+} //@codeactionedit("}", "refactor.rewrite", a23) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) -- @a24/a2.go -- @@ -29 +29,4 @@ --var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +-var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) +var _ = funStructEmpty{ + fn: func() { + }, -+} //@codeactionedit("}", "refactor.rewrite", a24) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) -- a3.go -- package fillstruct @@ -150,7 +150,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +var _ = Bar{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -161,7 +161,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) type pointerBuiltinStruct struct { b *bool @@ -169,23 +169,23 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) var _ = []ast.BasicLit{ - {}, //@codeactionedit("}", "refactor.rewrite", a34) + {}, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) } -var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) -- @a31/a3.go -- @@ -17 +17,4 @@ --var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +-var _ = Bar{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) +var _ = Bar{ + X: &Foo{}, + Y: &Foo{}, -+} //@codeactionedit("}", "refactor.rewrite", a31) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) -- @a32/a3.go -- @@ -28 +28,9 @@ --var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +-var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) +var _ = importedStruct{ + m: map[*ast.CompositeLit]ast.Field{}, + s: []ast.BadExpr{}, @@ -194,31 +194,31 @@ var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) + fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { + }, + st: ast.CompositeLit{}, -+} //@codeactionedit("}", "refactor.rewrite", a32) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) -- @a33/a3.go -- @@ -36 +36,5 @@ --var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +-var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) +var _ = pointerBuiltinStruct{ + b: new(bool), + s: new(string), + i: new(int), -+} //@codeactionedit("}", "refactor.rewrite", a33) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) -- @a34/a3.go -- @@ -39 +39,5 @@ -- {}, //@codeactionedit("}", "refactor.rewrite", a34) +- {}, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) + { + ValuePos: 0, + Kind: 0, + Value: "", -+ }, //@codeactionedit("}", "refactor.rewrite", a34) ++ }, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) -- @a35/a3.go -- @@ -42 +42,5 @@ --var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +-var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) +var _ = []ast.BasicLit{{ + ValuePos: 0, + Kind: 0, + Value: "", -+}} //@codeactionedit("}", "refactor.rewrite", a35) ++}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) -- a4.go -- package fillstruct @@ -244,49 +244,49 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) + var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) var s string - var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) + var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) + var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) var node *ast.CompositeLit - var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) + var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) } -- @a41/a4.go -- @@ -25 +25,3 @@ -- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) +- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) + var _ = iStruct{ + X: x, -+ } //@codeactionedit("}", "refactor.rewrite", a41) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) -- @a42/a4.go -- @@ -28 +28,3 @@ -- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) +- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) + var _ = sStruct{ + str: s, -+ } //@codeactionedit("}", "refactor.rewrite", a42) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) -- @a43/a4.go -- @@ -35 +35,5 @@ -- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) +- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) + var _ = multiFill{ + num: n, + strin: s, + arr: []int{}, -+ } //@codeactionedit("}", "refactor.rewrite", a43) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) -- @a45/a4.go -- @@ -38 +38,3 @@ -- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) +- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) + var _ = assignStruct{ + n: node, -+ } //@codeactionedit("}", "refactor.rewrite", a45) --- fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) +-- fillStruct.go -- package fillstruct type StructA struct { @@ -306,43 +306,43 @@ type StructA3 struct { } func fill() { - a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) - b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) - c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) + a := StructA{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) + b := StructA2{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) + c := StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) if true { - _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) + _ = StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) } } --- @fill_struct1/fill_struct.go -- +-- @fillStruct1/fillStruct.go -- @@ -20 +20,7 @@ -- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) +- a := StructA{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) + a := StructA{ + unexportedIntField: 0, + ExportedIntField: 0, + MapA: map[int]string{}, + Array: []int{}, + StructB: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct1) --- @fill_struct2/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) +-- @fillStruct2/fillStruct.go -- @@ -21 +21,3 @@ -- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) +- b := StructA2{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) + b := StructA2{ + B: &StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct2) --- @fill_struct3/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) +-- @fillStruct3/fillStruct.go -- @@ -22 +22,3 @@ -- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) +- c := StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) + c := StructA3{ + B: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct3) --- @fill_struct4/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) +-- @fillStruct4/fillStruct.go -- @@ -24 +24,3 @@ -- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) +- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) + _ = StructA3{ + B: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct4) --- fill_struct_anon.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) +-- fillStruct_anon.go -- package fillstruct type StructAnon struct { @@ -355,17 +355,17 @@ type StructAnon struct { } func fill() { - _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) + _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) } --- @fill_struct_anon/fill_struct_anon.go -- +-- @fillStruct_anon/fillStruct_anon.go -- @@ -13 +13,5 @@ -- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) + _ := StructAnon{ + a: struct{}{}, + b: map[string]interface{}{}, + c: map[string]struct{d int; e bool}{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) --- fill_struct_nested.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) +-- fillStruct_nested.go -- package fillstruct type StructB struct { @@ -378,17 +378,17 @@ type StructC struct { func nested() { c := StructB{ - StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) + StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) } } --- @fill_nested/fill_struct_nested.go -- +-- @fill_nested/fillStruct_nested.go -- @@ -13 +13,3 @@ -- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) +- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) + StructC: StructC{ + unexportedInt: 0, -+ }, //@codeactionedit("}", "refactor.rewrite", fill_nested) --- fill_struct_package.go -- ++ }, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) +-- fillStruct_package.go -- package fillstruct import ( @@ -398,26 +398,26 @@ import ( ) func unexported() { - a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) - _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) + a := data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) + _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) } --- @fill_struct_package1/fill_struct_package.go -- +-- @fillStruct_package1/fillStruct_package.go -- @@ -10 +10,3 @@ -- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +- a := data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) + a := data.B{ + ExportedInt: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) --- @fill_struct_package2/fill_struct_package.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) +-- @fillStruct_package2/fillStruct_package.go -- @@ -11 +11,7 @@ -- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) + _ = h2.Client{ + Transport: nil, + CheckRedirect: func(req *h2.Request, via []*h2.Request) error { + }, + Jar: nil, + Timeout: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) --- fill_struct_partial.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) +-- fillStruct_partial.go -- package fillstruct type StructPartialA struct { @@ -434,22 +434,22 @@ type StructPartialB struct { func fill() { a := StructPartialA{ PrefilledInt: 5, - } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1) + } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_partial1) b := StructPartialB{ /* this comment should disappear */ PrefilledInt: 7, // This comment should be blown away. /* As should this one */ - } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2) + } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_partial2) } --- @fill_struct_partial1/fill_struct_partial.go -- +-- @fillStruct_partial1/fillStruct_partial.go -- @@ -16 +16,3 @@ - PrefilledInt: 5, + PrefilledInt: 5, + UnfilledInt: 0, + StructPartialB: StructPartialB{}, --- @fill_struct_partial2/fill_struct_partial.go -- +-- @fillStruct_partial2/fillStruct_partial.go -- @@ -19,4 +19,2 @@ - /* this comment should disappear */ - PrefilledInt: 7, // This comment should be blown away. @@ -457,7 +457,7 @@ func fill() { - this one */ + PrefilledInt: 7, + UnfilledInt: 0, --- fill_struct_spaces.go -- +-- fillStruct_spaces.go -- package fillstruct type StructD struct { @@ -465,16 +465,16 @@ type StructD struct { } func spaces() { - d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) + d := StructD{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) } --- @fill_struct_spaces/fill_struct_spaces.go -- +-- @fillStruct_spaces/fillStruct_spaces.go -- @@ -8 +8,3 @@ -- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +- d := StructD{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) + d := StructD{ + ExportedIntField: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) --- fill_struct_unsafe.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) +-- fillStruct_unsafe.go -- package fillstruct import "unsafe" @@ -485,16 +485,16 @@ type unsafeStruct struct { } func fill() { - _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) + _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) } --- @fill_struct_unsafe/fill_struct_unsafe.go -- +-- @fillStruct_unsafe/fillStruct_unsafe.go -- @@ -11 +11,4 @@ -- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) + _ := unsafeStruct{ + x: 0, + p: nil, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) -- typeparams.go -- package fillstruct @@ -506,59 +506,59 @@ type basicStructWithTypeParams[T any] struct { foo T } -var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) type twoArgStructWithTypeParams[F, B any] struct { foo F bar B } -var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) var _ = twoArgStructWithTypeParams[int, string]{ bar: "bar", -} //@codeactionedit("}", "refactor.rewrite", typeparams3) +} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams3) type nestedStructWithTypeParams struct { bar string basic basicStructWithTypeParams[int] } -var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) func _[T any]() { type S struct{ t T } - _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) + _ = S{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) } -- @typeparams1/typeparams.go -- @@ -11 +11,3 @@ --var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +-var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) +var _ = basicStructWithTypeParams[int]{ + foo: 0, -+} //@codeactionedit("}", "refactor.rewrite", typeparams1) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) -- @typeparams2/typeparams.go -- @@ -18 +18,4 @@ --var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +-var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) +var _ = twoArgStructWithTypeParams[string, int]{ + foo: "", + bar: 0, -+} //@codeactionedit("}", "refactor.rewrite", typeparams2) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) -- @typeparams3/typeparams.go -- @@ -21 +21 @@ + foo: 0, -- @typeparams4/typeparams.go -- @@ -29 +29,4 @@ --var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +-var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) +var _ = nestedStructWithTypeParams{ + bar: "", + basic: basicStructWithTypeParams{}, -+} //@codeactionedit("}", "refactor.rewrite", typeparams4) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) -- @typeparams5/typeparams.go -- @@ -33 +33,3 @@ -- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +- _ = S{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) + _ = S{ + t: *new(T), -+ } //@codeactionedit("}", "refactor.rewrite", typeparams5) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) -- issue63921.go -- package fillstruct @@ -571,5 +571,5 @@ type invalidStruct struct { func _() { // Note: the golden content for issue63921 is empty: fillstruct produces no // edits, but does not panic. - invalidStruct{} //@codeactionedit("}", "refactor.rewrite", issue63921) + invalidStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", issue63921) } diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt index e553d1c5993..24e7a9126e2 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt @@ -39,49 +39,49 @@ type basicStruct struct { foo int } -var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) type twoArgStruct struct { foo int bar string } -var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) type nestedStruct struct { bar string basic basicStruct } -var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) -var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +var _ = data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) -- @a1/a.go -- @@ -11 +11,3 @@ --var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +-var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) +var _ = basicStruct{ + foo: 0, -+} //@codeactionedit("}", "refactor.rewrite", a1) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a1) -- @a2/a.go -- @@ -18 +18,4 @@ --var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +-var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) +var _ = twoArgStruct{ + foo: 0, + bar: "", -+} //@codeactionedit("}", "refactor.rewrite", a2) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a2) -- @a3/a.go -- @@ -25 +25,4 @@ --var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +-var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) +var _ = nestedStruct{ + bar: "", + basic: basicStruct{}, -+} //@codeactionedit("}", "refactor.rewrite", a3) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a3) -- @a4/a.go -- @@ -27 +27,3 @@ --var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +-var _ = data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) +var _ = data.B{ + ExportedInt: 0, -+} //@codeactionedit("}", "refactor.rewrite", a4) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a4) -- a2.go -- package fillstruct @@ -93,57 +93,57 @@ type typedStruct struct { a [2]string } -var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) type funStruct struct { fn func(i int) int } -var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) type funStructComplex struct { fn func(i int, s string) (string, int) } -var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) type funStructEmpty struct { fn func() } -var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) -- @a21/a2.go -- @@ -11 +11,7 @@ --var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +-var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) +var _ = typedStruct{ + m: map[string]int{}, + s: []int{}, + c: make(chan int), + c1: make(<-chan int), + a: [2]string{}, -+} //@codeactionedit("}", "refactor.rewrite", a21) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a21) -- @a22/a2.go -- @@ -17 +17,4 @@ --var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +-var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) +var _ = funStruct{ + fn: func(i int) int { + }, -+} //@codeactionedit("}", "refactor.rewrite", a22) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a22) -- @a23/a2.go -- @@ -23 +23,4 @@ --var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +-var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) +var _ = funStructComplex{ + fn: func(i int, s string) (string, int) { + }, -+} //@codeactionedit("}", "refactor.rewrite", a23) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a23) -- @a24/a2.go -- @@ -29 +29,4 @@ --var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +-var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) +var _ = funStructEmpty{ + fn: func() { + }, -+} //@codeactionedit("}", "refactor.rewrite", a24) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a24) -- a3.go -- package fillstruct @@ -161,7 +161,7 @@ type Bar struct { Y *Foo } -var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +var _ = Bar{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) type importedStruct struct { m map[*ast.CompositeLit]ast.Field @@ -172,7 +172,7 @@ type importedStruct struct { st ast.CompositeLit } -var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) type pointerBuiltinStruct struct { b *bool @@ -180,23 +180,23 @@ type pointerBuiltinStruct struct { i *int } -var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) var _ = []ast.BasicLit{ - {}, //@codeactionedit("}", "refactor.rewrite", a34) + {}, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) } -var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) -- @a31/a3.go -- @@ -17 +17,4 @@ --var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +-var _ = Bar{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) +var _ = Bar{ + X: &Foo{}, + Y: &Foo{}, -+} //@codeactionedit("}", "refactor.rewrite", a31) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a31) -- @a32/a3.go -- @@ -28 +28,9 @@ --var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +-var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) +var _ = importedStruct{ + m: map[*ast.CompositeLit]ast.Field{}, + s: []ast.BadExpr{}, @@ -205,31 +205,31 @@ var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) + fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { + }, + st: ast.CompositeLit{}, -+} //@codeactionedit("}", "refactor.rewrite", a32) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a32) -- @a33/a3.go -- @@ -36 +36,5 @@ --var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +-var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) +var _ = pointerBuiltinStruct{ + b: new(bool), + s: new(string), + i: new(int), -+} //@codeactionedit("}", "refactor.rewrite", a33) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", a33) -- @a34/a3.go -- @@ -39 +39,5 @@ -- {}, //@codeactionedit("}", "refactor.rewrite", a34) +- {}, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) + { + ValuePos: 0, + Kind: 0, + Value: "", -+ }, //@codeactionedit("}", "refactor.rewrite", a34) ++ }, //@codeactionedit("}", "refactor.rewrite.fillStruct", a34) -- @a35/a3.go -- @@ -42 +42,5 @@ --var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +-var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) +var _ = []ast.BasicLit{{ + ValuePos: 0, + Kind: 0, + Value: "", -+}} //@codeactionedit("}", "refactor.rewrite", a35) ++}} //@codeactionedit("}", "refactor.rewrite.fillStruct", a35) -- a4.go -- package fillstruct @@ -255,49 +255,49 @@ type assignStruct struct { func fill() { var x int - var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) + var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) var s string - var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) + var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) var n int _ = []int{} if true { arr := []int{1, 2} } - var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) + var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) var node *ast.CompositeLit - var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) + var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) } -- @a41/a4.go -- @@ -25 +25,3 @@ -- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) +- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) + var _ = iStruct{ + X: x, -+ } //@codeactionedit("}", "refactor.rewrite", a41) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a41) -- @a42/a4.go -- @@ -28 +28,3 @@ -- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) +- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) + var _ = sStruct{ + str: s, -+ } //@codeactionedit("}", "refactor.rewrite", a42) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a42) -- @a43/a4.go -- @@ -35 +35,5 @@ -- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) +- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) + var _ = multiFill{ + num: n, + strin: s, + arr: []int{}, -+ } //@codeactionedit("}", "refactor.rewrite", a43) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a43) -- @a45/a4.go -- @@ -38 +38,3 @@ -- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) +- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) + var _ = assignStruct{ + n: node, -+ } //@codeactionedit("}", "refactor.rewrite", a45) --- fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", a45) +-- fillStruct.go -- package fillstruct type StructA struct { @@ -317,43 +317,43 @@ type StructA3 struct { } func fill() { - a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) - b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) - c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) + a := StructA{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) + b := StructA2{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) + c := StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) if true { - _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) + _ = StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) } } --- @fill_struct1/fill_struct.go -- +-- @fillStruct1/fillStruct.go -- @@ -20 +20,7 @@ -- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) +- a := StructA{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) + a := StructA{ + unexportedIntField: 0, + ExportedIntField: 0, + MapA: map[int]string{}, + Array: []int{}, + StructB: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct1) --- @fill_struct2/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct1) +-- @fillStruct2/fillStruct.go -- @@ -21 +21,3 @@ -- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) +- b := StructA2{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) + b := StructA2{ + B: &StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct2) --- @fill_struct3/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct2) +-- @fillStruct3/fillStruct.go -- @@ -22 +22,3 @@ -- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) +- c := StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) + c := StructA3{ + B: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct3) --- @fill_struct4/fill_struct.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct3) +-- @fillStruct4/fillStruct.go -- @@ -24 +24,3 @@ -- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) +- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) + _ = StructA3{ + B: StructB{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct4) --- fill_struct_anon.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct4) +-- fillStruct_anon.go -- package fillstruct type StructAnon struct { @@ -366,17 +366,17 @@ type StructAnon struct { } func fill() { - _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) + _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) } --- @fill_struct_anon/fill_struct_anon.go -- +-- @fillStruct_anon/fillStruct_anon.go -- @@ -13 +13,5 @@ -- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) + _ := StructAnon{ + a: struct{}{}, + b: map[string]interface{}{}, + c: map[string]struct{d int; e bool}{}, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) --- fill_struct_nested.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_anon) +-- fillStruct_nested.go -- package fillstruct type StructB struct { @@ -389,17 +389,17 @@ type StructC struct { func nested() { c := StructB{ - StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) + StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) } } --- @fill_nested/fill_struct_nested.go -- +-- @fill_nested/fillStruct_nested.go -- @@ -13 +13,3 @@ -- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) +- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) + StructC: StructC{ + unexportedInt: 0, -+ }, //@codeactionedit("}", "refactor.rewrite", fill_nested) --- fill_struct_package.go -- ++ }, //@codeactionedit("}", "refactor.rewrite.fillStruct", fill_nested) +-- fillStruct_package.go -- package fillstruct import ( @@ -409,26 +409,26 @@ import ( ) func unexported() { - a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) - _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) + a := data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) + _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) } --- @fill_struct_package1/fill_struct_package.go -- +-- @fillStruct_package1/fillStruct_package.go -- @@ -10 +10,3 @@ -- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +- a := data.B{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) + a := data.B{ + ExportedInt: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) --- @fill_struct_package2/fill_struct_package.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package1) +-- @fillStruct_package2/fillStruct_package.go -- @@ -11 +11,7 @@ -- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) + _ = h2.Client{ + Transport: nil, + CheckRedirect: func(req *h2.Request, via []*h2.Request) error { + }, + Jar: nil, + Timeout: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) --- fill_struct_partial.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_package2) +-- fillStruct_partial.go -- package fillstruct type StructPartialA struct { @@ -445,22 +445,22 @@ type StructPartialB struct { func fill() { a := StructPartialA{ PrefilledInt: 5, - } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1) + } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_partial1) b := StructPartialB{ /* this comment should disappear */ PrefilledInt: 7, // This comment should be blown away. /* As should this one */ - } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2) + } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_partial2) } --- @fill_struct_partial1/fill_struct_partial.go -- +-- @fillStruct_partial1/fillStruct_partial.go -- @@ -16 +16,3 @@ - PrefilledInt: 5, + PrefilledInt: 5, + UnfilledInt: 0, + StructPartialB: StructPartialB{}, --- @fill_struct_partial2/fill_struct_partial.go -- +-- @fillStruct_partial2/fillStruct_partial.go -- @@ -19,4 +19,2 @@ - /* this comment should disappear */ - PrefilledInt: 7, // This comment should be blown away. @@ -468,7 +468,7 @@ func fill() { - this one */ + PrefilledInt: 7, + UnfilledInt: 0, --- fill_struct_spaces.go -- +-- fillStruct_spaces.go -- package fillstruct type StructD struct { @@ -476,16 +476,16 @@ type StructD struct { } func spaces() { - d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) + d := StructD{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) } --- @fill_struct_spaces/fill_struct_spaces.go -- +-- @fillStruct_spaces/fillStruct_spaces.go -- @@ -8 +8,3 @@ -- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +- d := StructD{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) + d := StructD{ + ExportedIntField: 0, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) --- fill_struct_unsafe.go -- ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_spaces) +-- fillStruct_unsafe.go -- package fillstruct import "unsafe" @@ -496,16 +496,16 @@ type unsafeStruct struct { } func fill() { - _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) + _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) } --- @fill_struct_unsafe/fill_struct_unsafe.go -- +-- @fillStruct_unsafe/fillStruct_unsafe.go -- @@ -11 +11,4 @@ -- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) + _ := unsafeStruct{ + x: 0, + p: nil, -+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", fillStruct_unsafe) -- typeparams.go -- package fillstruct @@ -517,59 +517,59 @@ type basicStructWithTypeParams[T any] struct { foo T } -var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) type twoArgStructWithTypeParams[F, B any] struct { foo F bar B } -var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) var _ = twoArgStructWithTypeParams[int, string]{ bar: "bar", -} //@codeactionedit("}", "refactor.rewrite", typeparams3) +} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams3) type nestedStructWithTypeParams struct { bar string basic basicStructWithTypeParams[int] } -var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) func _[T any]() { type S struct{ t T } - _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) + _ = S{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) } -- @typeparams1/typeparams.go -- @@ -11 +11,3 @@ --var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +-var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) +var _ = basicStructWithTypeParams[int]{ + foo: 0, -+} //@codeactionedit("}", "refactor.rewrite", typeparams1) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams1) -- @typeparams2/typeparams.go -- @@ -18 +18,4 @@ --var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +-var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) +var _ = twoArgStructWithTypeParams[string, int]{ + foo: "", + bar: 0, -+} //@codeactionedit("}", "refactor.rewrite", typeparams2) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams2) -- @typeparams3/typeparams.go -- @@ -21 +21 @@ + foo: 0, -- @typeparams4/typeparams.go -- @@ -29 +29,4 @@ --var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +-var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) +var _ = nestedStructWithTypeParams{ + bar: "", + basic: basicStructWithTypeParams{}, -+} //@codeactionedit("}", "refactor.rewrite", typeparams4) ++} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams4) -- @typeparams5/typeparams.go -- @@ -33 +33,3 @@ -- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +- _ = S{} //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) + _ = S{ + t: *new(T), -+ } //@codeactionedit("}", "refactor.rewrite", typeparams5) ++ } //@codeactionedit("}", "refactor.rewrite.fillStruct", typeparams5) -- issue63921.go -- package fillstruct @@ -582,5 +582,5 @@ type invalidStruct struct { func _() { // Note: the golden content for issue63921 is empty: fillstruct produces no // edits, but does not panic. - invalidStruct{} //@codeactionedit("}", "refactor.rewrite", issue63921) + invalidStruct{} //@codeactionedit("}", "refactor.rewrite.fillStruct", issue63921) } diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt index 2c1b19e130c..0d92b05fc41 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt @@ -50,19 +50,19 @@ func (notificationTwo) isNotification() {} func doSwitch() { var b data.TypeB switch b { - case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) + case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a1) } var a typeA switch a { - case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) + case typeAThree: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a2) } var n notification - switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) + switch n.(type) { //@codeactionedit("{", "refactor.rewrite.fillSwitch", a3) } - switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) + switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite.fillSwitch", a4) } var s struct { @@ -70,7 +70,7 @@ func doSwitch() { } switch s.a { - case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) + case typeAThree: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a5) } } -- @a1/a.go -- diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt index 504acd6043e..84464417b81 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt @@ -61,19 +61,19 @@ func (notificationTwo) isNotification() {} func doSwitch() { var b data.TypeB switch b { - case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) + case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a1) } var a typeA switch a { - case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) + case typeAThree: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a2) } var n notification - switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) + switch n.(type) { //@codeactionedit("{", "refactor.rewrite.fillSwitch", a3) } - switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) + switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite.fillSwitch", a4) } var s struct { @@ -81,7 +81,7 @@ func doSwitch() { } switch s.a { - case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) + case typeAThree: //@codeactionedit(":", "refactor.rewrite.fillSwitch", a5) } } -- @a1/a.go -- diff --git a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt index b37009c78d9..1c65fcd2329 100644 --- a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt +++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt @@ -8,16 +8,16 @@ go 1.18 -- basic.go -- package extract -func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) - a := 1 //@codeaction("a", end, "refactor.extract", inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract.function", outer) + a := 1 //@codeaction("a", end, "refactor.extract.function", inner) _ = a + 4 //@loc(end, "4") } //@loc(closeBracket, "}") -- @inner/basic.go -- package extract -func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) - //@codeaction("a", end, "refactor.extract", inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract.function", outer) + //@codeaction("a", end, "refactor.extract.function", inner) newFunction() //@loc(end, "4") } @@ -29,8 +29,8 @@ func newFunction() { -- @outer/basic.go -- package extract -func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) - //@codeaction("a", end, "refactor.extract", inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract.function", outer) + //@codeaction("a", end, "refactor.extract.function", inner) newFunction() //@loc(end, "4") } @@ -44,7 +44,7 @@ package extract func _() bool { x := 1 - if x == 0 { //@codeaction("if", ifend, "refactor.extract", return) + if x == 0 { //@codeaction("if", ifend, "refactor.extract.function", return) return true } //@loc(ifend, "}") return false @@ -55,7 +55,7 @@ package extract func _() bool { x := 1 - //@codeaction("if", ifend, "refactor.extract", return) + //@codeaction("if", ifend, "refactor.extract.function", return) shouldReturn, returnValue := newFunction(x) if shouldReturn { return returnValue @@ -74,7 +74,7 @@ func newFunction(x int) (bool, bool) { package extract func _() bool { - x := 1 //@codeaction("x", rnnEnd, "refactor.extract", rnn) + x := 1 //@codeaction("x", rnnEnd, "refactor.extract.function", rnn) if x == 0 { return true } @@ -85,7 +85,7 @@ func _() bool { package extract func _() bool { - //@codeaction("x", rnnEnd, "refactor.extract", rnn) + //@codeaction("x", rnnEnd, "refactor.extract.function", rnn) return newFunction() //@loc(rnnEnd, "false") } @@ -105,7 +105,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - z := "bye" //@codeaction("z", rcEnd, "refactor.extract", rc) + z := "bye" //@codeaction("z", rcEnd, "refactor.extract.function", rc) if y == z { return x, y, fmt.Errorf("same") } else if false { @@ -123,7 +123,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - //@codeaction("z", rcEnd, "refactor.extract", rc) + //@codeaction("z", rcEnd, "refactor.extract.function", rc) z, shouldReturn, returnValue, returnValue1, returnValue2 := newFunction(y, x) if shouldReturn { return returnValue, returnValue1, returnValue2 @@ -150,7 +150,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - z := "bye" //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) + z := "bye" //@codeaction("z", rcnnEnd, "refactor.extract.function", rcnn) if y == z { return x, y, fmt.Errorf("same") } else if false { @@ -168,7 +168,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) + //@codeaction("z", rcnnEnd, "refactor.extract.function", rcnn) return newFunction(y, x) //@loc(rcnnEnd, "nil") } @@ -190,7 +190,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - if n == nil { //@codeaction("if", rflEnd, "refactor.extract", rfl) + if n == nil { //@codeaction("if", rflEnd, "refactor.extract.function", rfl) return true } //@loc(rflEnd, "}") return false @@ -204,7 +204,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - //@codeaction("if", rflEnd, "refactor.extract", rfl) + //@codeaction("if", rflEnd, "refactor.extract.function", rfl) shouldReturn, returnValue := newFunction(n) if shouldReturn { return returnValue @@ -227,7 +227,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - if n == nil { //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) + if n == nil { //@codeaction("if", rflnnEnd, "refactor.extract.function", rflnn) return true } return false //@loc(rflnnEnd, "false") @@ -241,7 +241,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) + //@codeaction("if", rflnnEnd, "refactor.extract.function", rflnn) return newFunction(n) //@loc(rflnnEnd, "false") }) } @@ -258,7 +258,7 @@ package extract func _() string { x := 1 - if x == 0 { //@codeaction("if", riEnd, "refactor.extract", ri) + if x == 0 { //@codeaction("if", riEnd, "refactor.extract.function", ri) x = 3 return "a" } //@loc(riEnd, "}") @@ -271,7 +271,7 @@ package extract func _() string { x := 1 - //@codeaction("if", riEnd, "refactor.extract", ri) + //@codeaction("if", riEnd, "refactor.extract.function", ri) shouldReturn, returnValue := newFunction(x) if shouldReturn { return returnValue @@ -293,7 +293,7 @@ package extract func _() string { x := 1 - if x == 0 { //@codeaction("if", rinnEnd, "refactor.extract", rinn) + if x == 0 { //@codeaction("if", rinnEnd, "refactor.extract.function", rinn) x = 3 return "a" } @@ -306,7 +306,7 @@ package extract func _() string { x := 1 - //@codeaction("if", rinnEnd, "refactor.extract", rinn) + //@codeaction("if", rinnEnd, "refactor.extract.function", rinn) return newFunction(x) //@loc(rinnEnd, "\"b\"") } @@ -324,10 +324,10 @@ package extract func _() { a := 1 - a = 5 //@codeaction("a", araend, "refactor.extract", ara) + a = 5 //@codeaction("a", araend, "refactor.extract.function", ara) a = a + 2 //@loc(araend, "2") - b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) + b := a * 2 //@codeaction("b", arbend, "refactor.extract.function", arb) _ = b + 4 //@loc(arbend, "4") } @@ -336,10 +336,10 @@ package extract func _() { a := 1 - //@codeaction("a", araend, "refactor.extract", ara) + //@codeaction("a", araend, "refactor.extract.function", ara) a = newFunction(a) //@loc(araend, "2") - b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) + b := a * 2 //@codeaction("b", arbend, "refactor.extract.function", arb) _ = b + 4 //@loc(arbend, "4") } @@ -354,10 +354,10 @@ package extract func _() { a := 1 - a = 5 //@codeaction("a", araend, "refactor.extract", ara) + a = 5 //@codeaction("a", araend, "refactor.extract.function", ara) a = a + 2 //@loc(araend, "2") - //@codeaction("b", arbend, "refactor.extract", arb) + //@codeaction("b", arbend, "refactor.extract.function", arb) newFunction(a) //@loc(arbend, "4") } @@ -371,7 +371,7 @@ package extract func _() { newFunction := 1 - a := newFunction //@codeaction("a", "newFunction", "refactor.extract", scope) + a := newFunction //@codeaction("a", "newFunction", "refactor.extract.function", scope) _ = a // avoid diagnostic } @@ -384,7 +384,7 @@ package extract func _() { newFunction := 1 - a := newFunction2(newFunction) //@codeaction("a", "newFunction", "refactor.extract", scope) + a := newFunction2(newFunction) //@codeaction("a", "newFunction", "refactor.extract.function", scope) _ = a // avoid diagnostic } @@ -402,7 +402,7 @@ package extract func _() { var a []int - a = append(a, 2) //@codeaction("a", siEnd, "refactor.extract", si) + a = append(a, 2) //@codeaction("a", siEnd, "refactor.extract.function", si) b := 4 //@loc(siEnd, "4") a = append(a, b) } @@ -412,7 +412,7 @@ package extract func _() { var a []int - //@codeaction("a", siEnd, "refactor.extract", si) + //@codeaction("a", siEnd, "refactor.extract.function", si) a, b := newFunction(a) //@loc(siEnd, "4") a = append(a, b) } @@ -429,7 +429,7 @@ package extract func _() { var b []int var a int - a = 2 //@codeaction("a", srEnd, "refactor.extract", sr) + a = 2 //@codeaction("a", srEnd, "refactor.extract.function", sr) b = []int{} b = append(b, a) //@loc(srEnd, ")") b[0] = 1 @@ -441,7 +441,7 @@ package extract func _() { var b []int var a int - //@codeaction("a", srEnd, "refactor.extract", sr) + //@codeaction("a", srEnd, "refactor.extract.function", sr) b = newFunction(a, b) //@loc(srEnd, ")") b[0] = 1 } @@ -458,7 +458,7 @@ package extract func _() { var b []int - a := 2 //@codeaction("a", upEnd, "refactor.extract", up) + a := 2 //@codeaction("a", upEnd, "refactor.extract.function", up) b = []int{} b = append(b, a) //@loc(upEnd, ")") b[0] = 1 @@ -472,7 +472,7 @@ package extract func _() { var b []int - //@codeaction("a", upEnd, "refactor.extract", up) + //@codeaction("a", upEnd, "refactor.extract.function", up) a, b := newFunction(b) //@loc(upEnd, ")") b[0] = 1 if a == 2 { @@ -491,9 +491,9 @@ func newFunction(b []int) (int, []int) { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) - // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) - _ = a + 4 //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract.function", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract.function", comment2) + _ = a + 4 //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract.function", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -504,9 +504,9 @@ package extract func _() { /* comment in the middle of a line */ - //@codeaction("a", commentEnd, "refactor.extract", comment1) - // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) - newFunction() //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) + //@codeaction("a", commentEnd, "refactor.extract.function", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract.function", comment2) + newFunction() //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract.function", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -522,9 +522,9 @@ func newFunction() { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) - // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) - newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract.function", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract.function", comment2) + newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract.function", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -538,9 +538,9 @@ func newFunction(a int) { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) - // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) - newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract.function", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract.function", comment2) + newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract.function", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -557,7 +557,7 @@ import "strconv" func _() { i, err := strconv.Atoi("1") - u, err := strconv.Atoi("2") //@codeaction("u", ")", "refactor.extract", redefine) + u, err := strconv.Atoi("2") //@codeaction("u", ")", "refactor.extract.function", redefine) if i == u || err == nil { return } @@ -570,7 +570,7 @@ import "strconv" func _() { i, err := strconv.Atoi("1") - u, err := newFunction() //@codeaction("u", ")", "refactor.extract", redefine) + u, err := newFunction() //@codeaction("u", ")", "refactor.extract.function", redefine) if i == u || err == nil { return } diff --git a/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt index cadc8e94263..aaca44d6c7a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt +++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt @@ -12,7 +12,7 @@ package extract import "fmt" func main() { - x := []rune{} //@codeaction("x", end, "refactor.extract", ext) + x := []rune{} //@codeaction("x", end, "refactor.extract.function", ext) s := "HELLO" for _, c := range s { x = append(x, c) @@ -26,7 +26,7 @@ package extract import "fmt" func main() { - //@codeaction("x", end, "refactor.extract", ext) + //@codeaction("x", end, "refactor.extract.function", ext) x := newFunction() //@loc(end, "}") fmt.Printf("%x\n", x) } diff --git a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt index 0d22e6ad483..1f14360d2e9 100644 --- a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt +++ b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt @@ -12,7 +12,7 @@ package func_arg func A( a string, b, c int64, - x int /*@codeaction("x", "x", "refactor.rewrite", func_arg)*/, + x int /*@codeaction("x", "x", "refactor.rewrite.joinLines", func_arg)*/, y int, ) (r1 string, r2, r3 int64, r4 int, r5 int) { return a, b, c, x, y @@ -21,7 +21,7 @@ func A( -- @func_arg/func_arg/func_arg.go -- package func_arg -func A(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", func_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { +func A(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite.joinLines", func_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { return a, b, c, x, y } @@ -29,7 +29,7 @@ func A(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", f package func_ret func A(a string, b, c int64, x int, y int) ( - r1 string /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, + r1 string /*@codeaction("r1", "r1", "refactor.rewrite.joinLines", func_ret)*/, r2, r3 int64, r4 int, r5 int, @@ -40,7 +40,7 @@ func A(a string, b, c int64, x int, y int) ( -- @func_ret/func_ret/func_ret.go -- package func_ret -func A(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, r2, r3 int64, r4 int, r5 int) { +func A(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite.joinLines", func_ret)*/, r2, r3 int64, r4 int, r5 int) { return a, b, c, x, y } @@ -50,20 +50,20 @@ package functype_arg type A func( a string, b, c int64, - x int /*@codeaction("x", "x", "refactor.rewrite", functype_arg)*/, + x int /*@codeaction("x", "x", "refactor.rewrite.joinLines", functype_arg)*/, y int, ) (r1 string, r2, r3 int64, r4 int, r5 int) -- @functype_arg/functype_arg/functype_arg.go -- package functype_arg -type A func(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", functype_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) +type A func(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite.joinLines", functype_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) -- functype_ret/functype_ret.go -- package functype_ret type A func(a string, b, c int64, x int, y int) ( - r1 string /*@codeaction("r1", "r1", "refactor.rewrite", functype_ret)*/, + r1 string /*@codeaction("r1", "r1", "refactor.rewrite.joinLines", functype_ret)*/, r2, r3 int64, r4 int, r5 int, @@ -72,7 +72,7 @@ type A func(a string, b, c int64, x int, y int) ( -- @functype_ret/functype_ret/functype_ret.go -- package functype_ret -type A func(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite", functype_ret)*/, r2, r3 int64, r4 int, r5 int) +type A func(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite.joinLines", functype_ret)*/, r2, r3 int64, r4 int, r5 int) -- func_call/func_call.go -- package func_call @@ -81,7 +81,7 @@ import "fmt" func a() { fmt.Println( - 1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, + 1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4), @@ -94,7 +94,7 @@ package func_call import "fmt" func a() { - fmt.Println(1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4)) + fmt.Println(1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4)) } -- indent/indent.go -- @@ -108,7 +108,7 @@ func a() { 2, 3, fmt.Sprintf( - "hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join arguments into one line")*/, + "hello %d" /*@codeaction("hello", "hello", "refactor.rewrite.joinLines", indent)*/, 4, )) } @@ -123,7 +123,7 @@ func a() { 1, 2, 3, - fmt.Sprintf("hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join arguments into one line")*/, 4)) + fmt.Sprintf("hello %d" /*@codeaction("hello", "hello", "refactor.rewrite.joinLines", indent)*/, 4)) } -- structelts/structelts.go -- @@ -137,7 +137,7 @@ type A struct{ func a() { _ = A{ a: 1, - b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/, + b: 2 /*@codeaction("b", "b", "refactor.rewrite.joinLines", structelts)*/, } } @@ -150,7 +150,7 @@ type A struct{ } func a() { - _ = A{a: 1, b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/} + _ = A{a: 1, b: 2 /*@codeaction("b", "b", "refactor.rewrite.joinLines", structelts)*/} } -- sliceelts/sliceelts.go -- @@ -158,7 +158,7 @@ package sliceelts func a() { _ = []int{ - 1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, + 1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", sliceelts)*/, 2, } } @@ -167,7 +167,7 @@ func a() { package sliceelts func a() { - _ = []int{1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, 2} + _ = []int{1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", sliceelts)*/, 2} } -- mapelts/mapelts.go -- @@ -175,7 +175,7 @@ package mapelts func a() { _ = map[string]int{ - "a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, + "a": 1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", mapelts)*/, "b": 2, } } @@ -184,14 +184,14 @@ func a() { package mapelts func a() { - _ = map[string]int{"a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, "b": 2} + _ = map[string]int{"a": 1 /*@codeaction("1", "1", "refactor.rewrite.joinLines", mapelts)*/, "b": 2} } -- starcomment/starcomment.go -- package starcomment func A( - /*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite", starcomment)*/, + /*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite.joinLines", starcomment)*/, /*4*/ y /*5*/ int /*6*/, ) (string, int) { return x, y @@ -200,7 +200,7 @@ func A( -- @starcomment/starcomment/starcomment.go -- package starcomment -func A(/*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite", starcomment)*/, /*4*/ y /*5*/ int /*6*/) (string, int) { +func A(/*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite.joinLines", starcomment)*/, /*4*/ y /*5*/ int /*6*/) (string, int) { return x, y } diff --git a/gopls/internal/test/marker/testdata/codeaction/inline.txt b/gopls/internal/test/marker/testdata/codeaction/inline.txt index 0c5bcb41658..050fe25b8ec 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline.txt @@ -1,4 +1,4 @@ -This is a minimal test of the refactor.inline code action, without resolve support. +This is a minimal test of the refactor.inline.call code action, without resolve support. See inline_resolve.txt for same test with resolve support. -- go.mod -- @@ -9,7 +9,7 @@ go 1.18 package a func _() { - println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline) + println(add(1, 2)) //@codeaction("add", ")", "refactor.inline.call", inline) } func add(x, y int) int { return x + y } @@ -18,7 +18,7 @@ func add(x, y int) int { return x + y } package a func _() { - println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline) + println(1 + 2) //@codeaction("add", ")", "refactor.inline.call", inline) } func add(x, y int) int { return x + y } diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt index 02c27e6505b..fa8476e91f6 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt @@ -1,4 +1,4 @@ -This is a minimal test of the refactor.inline code actions, with resolve support. +This is a minimal test of the refactor.inline.call code actions, with resolve support. See inline.txt for same test without resolve support. -- capabilities.json -- @@ -20,7 +20,7 @@ go 1.18 package a func _() { - println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline) + println(add(1, 2)) //@codeaction("add", ")", "refactor.inline.call", inline) } func add(x, y int) int { return x + y } @@ -29,7 +29,7 @@ func add(x, y int) int { return x + y } package a func _() { - println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline) + println(1 + 2) //@codeaction("add", ")", "refactor.inline.call", inline) } func add(x, y int) int { return x + y } diff --git a/gopls/internal/test/marker/testdata/codeaction/invertif.txt b/gopls/internal/test/marker/testdata/codeaction/invertif.txt index 57e77530844..02f856f6977 100644 --- a/gopls/internal/test/marker/testdata/codeaction/invertif.txt +++ b/gopls/internal/test/marker/testdata/codeaction/invertif.txt @@ -10,7 +10,7 @@ import ( func Boolean() { b := true - if b { //@codeactionedit("if b", "refactor.rewrite", boolean) + if b { //@codeactionedit("if b", "refactor.rewrite.invertIf", boolean) fmt.Println("A") } else { fmt.Println("B") @@ -18,7 +18,7 @@ func Boolean() { } func BooleanFn() { - if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) + if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite.invertIf", boolean_fn) fmt.Println("A") } else { fmt.Println("B") @@ -30,7 +30,7 @@ func DontRemoveParens() { a := false b := true if !(a || - b) { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) + b) { //@codeactionedit("b", "refactor.rewrite.invertIf", dont_remove_parens) fmt.Println("A") } else { fmt.Println("B") @@ -46,7 +46,7 @@ func ElseIf() { // No inversion expected for else-if, that would become unreadable if len(os.Args) > 2 { fmt.Println("A") - } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) + } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite.invertIf", else_if) fmt.Println("B") } else { fmt.Println("C") @@ -54,7 +54,7 @@ func ElseIf() { } func GreaterThan() { - if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite", greater_than) + if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite.invertIf", greater_than) fmt.Println("A") } else { fmt.Println("B") @@ -63,7 +63,7 @@ func GreaterThan() { func NotBoolean() { b := true - if !b { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) + if !b { //@codeactionedit("if !b", "refactor.rewrite.invertIf", not_boolean) fmt.Println("A") } else { fmt.Println("B") @@ -71,7 +71,7 @@ func NotBoolean() { } func RemoveElse() { - if true { //@codeactionedit("if true", "refactor.rewrite", remove_else) + if true { //@codeactionedit("if true", "refactor.rewrite.invertIf", remove_else) fmt.Println("A") } else { fmt.Println("B") @@ -83,7 +83,7 @@ func RemoveElse() { func RemoveParens() { b := true - if !(b) { //@codeactionedit("if", "refactor.rewrite", remove_parens) + if !(b) { //@codeactionedit("if", "refactor.rewrite.invertIf", remove_parens) fmt.Println("A") } else { fmt.Println("B") @@ -91,7 +91,7 @@ func RemoveParens() { } func Semicolon() { - if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite", semicolon) + if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite.invertIf", semicolon) fmt.Println("A") } else { fmt.Println("B") @@ -99,7 +99,7 @@ func Semicolon() { } func SemicolonAnd() { - if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite", semicolon_and) + if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite.invertIf", semicolon_and) fmt.Println("A") } else { fmt.Println("B") @@ -107,7 +107,7 @@ func SemicolonAnd() { } func SemicolonOr() { - if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) + if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite.invertIf", semicolon_or) fmt.Println("A") } else { fmt.Println("B") @@ -116,103 +116,103 @@ func SemicolonOr() { -- @boolean/p.go -- @@ -10,3 +10 @@ -- if b { //@codeactionedit("if b", "refactor.rewrite", boolean) +- if b { //@codeactionedit("if b", "refactor.rewrite.invertIf", boolean) - fmt.Println("A") - } else { + if !b { @@ -14 +12,2 @@ -+ } else { //@codeactionedit("if b", "refactor.rewrite", boolean) ++ } else { //@codeactionedit("if b", "refactor.rewrite.invertIf", boolean) + fmt.Println("A") -- @boolean_fn/p.go -- @@ -18,3 +18 @@ -- if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) +- if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite.invertIf", boolean_fn) - fmt.Println("A") - } else { + if !os.IsPathSeparator('X') { @@ -22 +20,2 @@ -+ } else { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) ++ } else { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite.invertIf", boolean_fn) + fmt.Println("A") -- @dont_remove_parens/p.go -- @@ -29,4 +29,2 @@ - if !(a || -- b) { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) +- b) { //@codeactionedit("b", "refactor.rewrite.invertIf", dont_remove_parens) - fmt.Println("A") - } else { + if (a || + b) { @@ -34 +32,2 @@ -+ } else { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) ++ } else { //@codeactionedit("b", "refactor.rewrite.invertIf", dont_remove_parens) + fmt.Println("A") -- @else_if/p.go -- @@ -46,3 +46 @@ -- } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) +- } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite.invertIf", else_if) - fmt.Println("B") - } else { + } else if os.Args[0] != "X" { @@ -50 +48,2 @@ -+ } else { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) ++ } else { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite.invertIf", else_if) + fmt.Println("B") -- @greater_than/p.go -- @@ -54,3 +54 @@ -- if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite", greater_than) +- if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite.invertIf", greater_than) - fmt.Println("A") - } else { + if len(os.Args) <= 2 { @@ -58 +56,2 @@ -+ } else { //@codeactionedit("i", "refactor.rewrite", greater_than) ++ } else { //@codeactionedit("i", "refactor.rewrite.invertIf", greater_than) + fmt.Println("A") -- @not_boolean/p.go -- @@ -63,3 +63 @@ -- if !b { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) +- if !b { //@codeactionedit("if !b", "refactor.rewrite.invertIf", not_boolean) - fmt.Println("A") - } else { + if b { @@ -67 +65,2 @@ -+ } else { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) ++ } else { //@codeactionedit("if !b", "refactor.rewrite.invertIf", not_boolean) + fmt.Println("A") -- @remove_else/p.go -- @@ -71,3 +71 @@ -- if true { //@codeactionedit("if true", "refactor.rewrite", remove_else) +- if true { //@codeactionedit("if true", "refactor.rewrite.invertIf", remove_else) - fmt.Println("A") - } else { + if false { @@ -78 +76,3 @@ -+ //@codeactionedit("if true", "refactor.rewrite", remove_else) ++ //@codeactionedit("if true", "refactor.rewrite.invertIf", remove_else) + fmt.Println("A") + -- @remove_parens/p.go -- @@ -83,3 +83 @@ -- if !(b) { //@codeactionedit("if", "refactor.rewrite", remove_parens) +- if !(b) { //@codeactionedit("if", "refactor.rewrite.invertIf", remove_parens) - fmt.Println("A") - } else { + if b { @@ -87 +85,2 @@ -+ } else { //@codeactionedit("if", "refactor.rewrite", remove_parens) ++ } else { //@codeactionedit("if", "refactor.rewrite.invertIf", remove_parens) + fmt.Println("A") -- @semicolon/p.go -- @@ -91,3 +91 @@ -- if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite", semicolon) +- if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite.invertIf", semicolon) - fmt.Println("A") - } else { + if _, err := fmt.Println("x"); err == nil { @@ -95 +93,2 @@ -+ } else { //@codeactionedit("if", "refactor.rewrite", semicolon) ++ } else { //@codeactionedit("if", "refactor.rewrite.invertIf", semicolon) + fmt.Println("A") -- @semicolon_and/p.go -- @@ -99,3 +99 @@ -- if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite", semicolon_and) +- if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite.invertIf", semicolon_and) - fmt.Println("A") - } else { + if n, err := fmt.Println("x"); err == nil || n <= 0 { @@ -103 +101,2 @@ -+ } else { //@codeactionedit("f", "refactor.rewrite", semicolon_and) ++ } else { //@codeactionedit("f", "refactor.rewrite.invertIf", semicolon_and) + fmt.Println("A") -- @semicolon_or/p.go -- @@ -107,3 +107 @@ -- if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) +- if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite.invertIf", semicolon_or) - fmt.Println("A") - } else { + if n, err := fmt.Println("x"); err == nil && n >= 5 { @@ -111 +109,2 @@ -+ } else { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) ++ } else { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite.invertIf", semicolon_or) + fmt.Println("A") diff --git a/gopls/internal/test/marker/testdata/codeaction/issue64558.txt b/gopls/internal/test/marker/testdata/codeaction/issue64558.txt index 59aaffba371..7ca661fbf00 100644 --- a/gopls/internal/test/marker/testdata/codeaction/issue64558.txt +++ b/gopls/internal/test/marker/testdata/codeaction/issue64558.txt @@ -8,7 +8,7 @@ go 1.18 package a func _() { - f(1, 2) //@ diag("2", re"too many arguments"), codeactionerr("f", ")", "refactor.inline", re`inlining failed \("args/params mismatch"\), likely because inputs were ill-typed`) + f(1, 2) //@ diag("2", re"too many arguments"), codeactionerr("f", ")", "refactor.inline.call", re`inlining failed \("args/params mismatch"\), likely because inputs were ill-typed`) } func f(int) {} diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt index 25ec6ae1d96..2b78b882df6 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt @@ -9,14 +9,14 @@ go 1.18 -- a/a.go -- package a -func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) return x } -- @a/a/a.go -- package a -func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) return x } @@ -99,7 +99,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite.removeUnusedParam", field) } func _() { @@ -108,7 +108,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite.removeUnusedParam", field) } func _() { @@ -117,7 +117,7 @@ func _() { -- ellipsis/ellipsis.go -- package ellipsis -func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite.removeUnusedParam", ellipsis) } func _() { @@ -138,7 +138,7 @@ func i() []any -- @ellipsis/ellipsis/ellipsis.go -- package ellipsis -func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite.removeUnusedParam", ellipsis) } func _() { @@ -162,7 +162,7 @@ func i() []any -- ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite.removeUnusedParam", ellipsis2) } func _() { @@ -176,7 +176,7 @@ func h() (int, int) -- @ellipsis2/ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite.removeUnusedParam", ellipsis2) } func _() { @@ -191,7 +191,7 @@ func h() (int, int) -- overlapping/overlapping.go -- package overlapping -func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") +func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite.removeUnusedParam", re"overlapping") return 0 } @@ -203,7 +203,7 @@ func _() { -- effects/effects.go -- package effects -func effects(x, y int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite", effects) +func effects(x, y int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite.removeUnusedParam", effects) return x } @@ -217,7 +217,7 @@ func _() { -- @effects/effects/effects.go -- package effects -func effects(x int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite", effects) +func effects(x int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite.removeUnusedParam", effects) return x } @@ -235,13 +235,13 @@ func _() { -- recursive/recursive.go -- package recursive -func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive) +func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", recursive) return Recursive(1) } -- @recursive/recursive/recursive.go -- package recursive -func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive) +func Recursive() int { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", recursive) return Recursive() } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt index 17abb98d5c9..b192d79b584 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt @@ -14,7 +14,7 @@ go 1.18 package a // A doc comment. -func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) // about to return return x // returning // just returned @@ -36,7 +36,7 @@ func one() int { package a // A doc comment. -func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) // about to return return x // returning // just returned diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt index e67e378fde3..ec8f63c34b3 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt @@ -10,7 +10,7 @@ go 1.18 -- a/a.go -- package a -func A(x, unused int) int { //@codeactionerr("unused", "unused", "refactor.rewrite", re"non-call function reference") +func A(x, unused int) int { //@codeactionerr("unused", "unused", "refactor.rewrite.removeUnusedParam", re"non-call function reference") return x } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt index 6fd4b9b6dcf..1f96d6b424c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt @@ -62,7 +62,7 @@ import "mod.test/c" var Chan chan c.C -func B(x, y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) +func B(x, y c.C) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", b) } -- c/c.go -- @@ -76,7 +76,7 @@ package d // Removing the parameter should remove this import. import "mod.test/c" -func D(x c.C) { //@codeaction("x", "x", "refactor.rewrite", d) +func D(x c.C) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", d) } func _() { @@ -148,14 +148,14 @@ import "mod.test/c" var Chan chan c.C -func B(y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) +func B(y c.C) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", b) } -- @d/d/d.go -- package d // Removing the parameter should remove this import. -func D() { //@codeaction("x", "x", "refactor.rewrite", d) +func D() { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", d) } func _() { diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_issue65217.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_issue65217.txt index 93d87d4dbec..f2ecae4ad1c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_issue65217.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_issue65217.txt @@ -27,7 +27,7 @@ func _() { _ = i } -func f(unused S, i int) int { //@codeaction("unused", "unused", "refactor.rewrite", rewrite, "Refactor: remove unused parameter"), diag("unused", re`unused`) +func f(unused S, i int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", rewrite), diag("unused", re`unused`) return i } @@ -53,6 +53,6 @@ func _() { _ = i } -func f(i int) int { //@codeaction("unused", "unused", "refactor.rewrite", rewrite, "Refactor: remove unused parameter"), diag("unused", re`unused`) +func f(i int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", rewrite), diag("unused", re`unused`) return i } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt index a62ff8cd679..174d9061927 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt @@ -18,7 +18,7 @@ package rm type Basic int -func (t Basic) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite", basic) +func (t Basic) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", basic) } func _(b Basic) { @@ -45,7 +45,7 @@ package rm type Basic int -func (t Basic) Foo() { //@codeaction("x", "x", "refactor.rewrite", basic) +func (t Basic) Foo() { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", basic) } func _(b Basic) { @@ -76,7 +76,7 @@ type Missing struct{} var r2 int -func (Missing) M(a, b, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite", missingrecv) +func (Missing) M(a, b, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite.removeUnusedParam", missingrecv) return a + c } @@ -104,7 +104,7 @@ type Missing struct{} var r2 int -func (Missing) M(a, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite", missingrecv) +func (Missing) M(a, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite.removeUnusedParam", missingrecv) return a + c } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt index c67e8a5d039..92f8d299272 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt @@ -20,14 +20,14 @@ go 1.18 -- a/a.go -- package a -func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) return x } -- @a/a/a.go -- package a -func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite.removeUnusedParam", a) return x } @@ -110,7 +110,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite.removeUnusedParam", field) } func _() { @@ -119,7 +119,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite.removeUnusedParam", field) } func _() { @@ -128,7 +128,7 @@ func _() { -- ellipsis/ellipsis.go -- package ellipsis -func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite.removeUnusedParam", ellipsis) } func _() { @@ -149,7 +149,7 @@ func i() []any -- @ellipsis/ellipsis/ellipsis.go -- package ellipsis -func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite.removeUnusedParam", ellipsis) } func _() { @@ -173,7 +173,7 @@ func i() []any -- ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite.removeUnusedParam", ellipsis2) } func _() { @@ -187,7 +187,7 @@ func h() (int, int) -- @ellipsis2/ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite.removeUnusedParam", ellipsis2) } func _() { @@ -202,7 +202,7 @@ func h() (int, int) -- overlapping/overlapping.go -- package overlapping -func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") +func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite.removeUnusedParam", re"overlapping") return 0 } @@ -214,7 +214,7 @@ func _() { -- effects/effects.go -- package effects -func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite", effects), diag("y", re"unused") +func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite.removeUnusedParam", effects), diag("y", re"unused") return x } @@ -228,7 +228,7 @@ func _() { -- @effects/effects/effects.go -- package effects -func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite", effects), diag("y", re"unused") +func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite.removeUnusedParam", effects), diag("y", re"unused") return x } @@ -246,13 +246,13 @@ func _() { -- recursive/recursive.go -- package recursive -func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive) +func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", recursive) return Recursive(1) } -- @recursive/recursive/recursive.go -- package recursive -func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive) +func Recursive() int { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", recursive) return Recursive() } diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt index 5a707001d0e..f35662e3dad 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt @@ -21,7 +21,7 @@ package rm type T int -func (t T) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite", basic) +func (t T) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", basic) } -- use/use.go -- @@ -44,7 +44,7 @@ package rm type T int -func (t T) Foo() { //@codeaction("x", "x", "refactor.rewrite", basic) +func (t T) Foo() { //@codeaction("x", "x", "refactor.rewrite.removeUnusedParam", basic) } -- @basic/use/use.go -- diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt index 60080028f0e..5b4cd37a51a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt @@ -3,7 +3,7 @@ This test checks that we can't remove parameters for packages with errors. -- p.go -- package p -func foo(unused int) { //@codeactionerr("unused", "unused", "refactor.rewrite", re"found 0") +func foo(unused int) { //@codeactionerr("unused", "unused", "refactor.rewrite.removeUnusedParam", re"found 0") } func _() { diff --git a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt index 76b8fb93c76..5600ccb777a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt +++ b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt @@ -9,7 +9,7 @@ go 1.18 -- func_arg/func_arg.go -- package func_arg -func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite", func_arg) +func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite.splitLines", func_arg) return a, b, c, x, y } @@ -21,14 +21,14 @@ func A( b, c int64, x int, y int, -) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite", func_arg) +) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite.splitLines", func_arg) return a, b, c, x, y } -- func_ret/func_ret.go -- package func_ret -func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("r1", "r1", "refactor.rewrite", func_ret) +func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("r1", "r1", "refactor.rewrite.splitLines", func_ret) return a, b, c, x, y } @@ -40,14 +40,14 @@ func A(a string, b, c int64, x int, y int) ( r2, r3 int64, r4 int, r5 int, -) { //@codeaction("r1", "r1", "refactor.rewrite", func_ret) +) { //@codeaction("r1", "r1", "refactor.rewrite.splitLines", func_ret) return a, b, c, x, y } -- functype_arg/functype_arg.go -- package functype_arg -type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite", functype_arg) +type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite.splitLines", functype_arg) -- @functype_arg/functype_arg/functype_arg.go -- package functype_arg @@ -57,12 +57,12 @@ type A func( b, c int64, x int, y int, -) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite", functype_arg) +) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite.splitLines", functype_arg) -- functype_ret/functype_ret.go -- package functype_ret -type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("r1", "r1", "refactor.rewrite", functype_ret) +type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("r1", "r1", "refactor.rewrite.splitLines", functype_ret) -- @functype_ret/functype_ret/functype_ret.go -- package functype_ret @@ -72,7 +72,7 @@ type A func(a string, b, c int64, x int, y int) ( r2, r3 int64, r4 int, r5 int, -) //@codeaction("r1", "r1", "refactor.rewrite", functype_ret) +) //@codeaction("r1", "r1", "refactor.rewrite.splitLines", functype_ret) -- func_call/func_call.go -- package func_call @@ -80,7 +80,7 @@ package func_call import "fmt" func a() { - fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", func_call) + fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite.splitLines", func_call) } -- @func_call/func_call/func_call.go -- @@ -94,7 +94,7 @@ func a() { 2, 3, fmt.Sprintf("hello %d", 4), - ) //@codeaction("1", "1", "refactor.rewrite", func_call) + ) //@codeaction("1", "1", "refactor.rewrite.splitLines", func_call) } -- indent/indent.go -- @@ -103,7 +103,7 @@ package indent import "fmt" func a() { - fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split arguments into separate lines") + fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("hello", "hello", "refactor.rewrite.splitLines", indent) } -- @indent/indent/indent.go -- @@ -115,7 +115,7 @@ func a() { fmt.Println(1, 2, 3, fmt.Sprintf( "hello %d", 4, - )) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split arguments into separate lines") + )) //@codeaction("hello", "hello", "refactor.rewrite.splitLines", indent) } -- indent2/indent2.go -- @@ -125,7 +125,7 @@ import "fmt" func a() { fmt. - Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split arguments into separate lines") + Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite.splitLines", indent2) } -- @indent2/indent2/indent2.go -- @@ -140,7 +140,7 @@ func a() { 2, 3, fmt.Sprintf("hello %d", 4), - ) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split arguments into separate lines") + ) //@codeaction("1", "1", "refactor.rewrite.splitLines", indent2) } -- structelts/structelts.go -- @@ -152,7 +152,7 @@ type A struct{ } func a() { - _ = A{a: 1, b: 2} //@codeaction("b", "b", "refactor.rewrite", structelts) + _ = A{a: 1, b: 2} //@codeaction("b", "b", "refactor.rewrite.splitLines", structelts) } -- @structelts/structelts/structelts.go -- @@ -167,14 +167,14 @@ func a() { _ = A{ a: 1, b: 2, - } //@codeaction("b", "b", "refactor.rewrite", structelts) + } //@codeaction("b", "b", "refactor.rewrite.splitLines", structelts) } -- sliceelts/sliceelts.go -- package sliceelts func a() { - _ = []int{1, 2} //@codeaction("1", "1", "refactor.rewrite", sliceelts) + _ = []int{1, 2} //@codeaction("1", "1", "refactor.rewrite.splitLines", sliceelts) } -- @sliceelts/sliceelts/sliceelts.go -- @@ -184,14 +184,14 @@ func a() { _ = []int{ 1, 2, - } //@codeaction("1", "1", "refactor.rewrite", sliceelts) + } //@codeaction("1", "1", "refactor.rewrite.splitLines", sliceelts) } -- mapelts/mapelts.go -- package mapelts func a() { - _ = map[string]int{"a": 1, "b": 2} //@codeaction("1", "1", "refactor.rewrite", mapelts) + _ = map[string]int{"a": 1, "b": 2} //@codeaction("1", "1", "refactor.rewrite.splitLines", mapelts) } -- @mapelts/mapelts/mapelts.go -- @@ -201,13 +201,13 @@ func a() { _ = map[string]int{ "a": 1, "b": 2, - } //@codeaction("1", "1", "refactor.rewrite", mapelts) + } //@codeaction("1", "1", "refactor.rewrite.splitLines", mapelts) } -- starcomment/starcomment.go -- package starcomment -func A(/*1*/ x /*2*/ string /*3*/, /*4*/ y /*5*/ int /*6*/) (string, int) { //@codeaction("x", "x", "refactor.rewrite", starcomment) +func A(/*1*/ x /*2*/ string /*3*/, /*4*/ y /*5*/ int /*6*/) (string, int) { //@codeaction("x", "x", "refactor.rewrite.splitLines", starcomment) return x, y } @@ -217,7 +217,7 @@ package starcomment func A( /*1*/ x /*2*/ string /*3*/, /*4*/ y /*5*/ int /*6*/, -) (string, int) { //@codeaction("x", "x", "refactor.rewrite", starcomment) +) (string, int) { //@codeaction("x", "x", "refactor.rewrite.splitLines", starcomment) return x, y } From 3e491913401c266a6e709646ec9be5c67a47fe20 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 10 Sep 2024 21:26:56 -0400 Subject: [PATCH 021/102] gopls/internal/telemetry/cmd/stacks: generate CodeSearch links This CL causes the stacks command to mark up each stack as a set of links to CodeSearch. In order to do that, it needs to build the gopls executable at the correct version of gopls and Go and for the right GOOS and GOARCH, read the pclntab out of the executable (which is the only authority on how to decode the symbol names that appear in the stack counter), and then construct CodeSearch URLs from (version, file, line) triples. The expensive steps are cached in /tmp/gopls-stacks so that they are paid infrequently in a typical stacks run. See https://github.com/golang/go/issues/67288 for an example of the updated output. Fixes golang/go#64654 Change-Id: If1c3e42af5550114515b47a22dfa036e8da27143 Reviewed-on: https://go-review.googlesource.com/c/tools/+/611840 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/telemetry/cmd/stacks/stacks.go | 273 ++++++++++++++++-- 1 file changed, 252 insertions(+), 21 deletions(-) diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go index e99e58915c9..5d4727a0650 100644 --- a/gopls/internal/telemetry/cmd/stacks/stacks.go +++ b/gopls/internal/telemetry/cmd/stacks/stacks.go @@ -7,8 +7,12 @@ // issue, creating new issues as needed. package main +// TODO(adonovan): create a proper package with tests. Much of this +// machinery might find wider use in other x/telemetry clients. + import ( "bytes" + "context" "encoding/base64" "encoding/json" "flag" @@ -19,8 +23,10 @@ import ( "net/http" "net/url" "os" + "os/exec" "path/filepath" "sort" + "strconv" "strings" "time" @@ -68,8 +74,8 @@ func main() { token = string(bytes.TrimSpace(content)) } - // Maps stack text to Version/GoVersion/GOOS/GOARCH string to counter. - stacks := make(map[string]map[string]int64) + // Maps stack text to Info to count. + stacks := make(map[string]map[Info]int64) var distinctStacks int // Maps stack to a telemetry URL. @@ -112,7 +118,7 @@ func main() { } sort.Strings(clients) if len(clients) > 0 { - clientSuffix = " " + strings.Join(clients, ",") + clientSuffix = strings.Join(clients, ",") } // Ignore @devel versions as they correspond to @@ -124,14 +130,18 @@ func main() { distinctStacks++ - info := fmt.Sprintf("%s@%s %s %s/%s%s", - prog.Program, prog.Version, - prog.GoVersion, prog.GOOS, prog.GOARCH, - clientSuffix) + info := Info{ + Program: prog.Program, + Version: prog.Version, + GoVersion: prog.GoVersion, + GOOS: prog.GOOS, + GOARCH: prog.GOARCH, + Client: clientSuffix, + } for stack, count := range prog.Stacks { counts := stacks[stack] if counts == nil { - counts = make(map[string]int64) + counts = make(map[Info]int64) stacks[stack] = counts } counts[info] += count @@ -176,8 +186,6 @@ func main() { } } - fmt.Printf("Found %d distinct stacks in last %v days:\n", distinctStacks, *daysFlag) - // For each stack, show existing issue or create a new one. // Aggregate stack IDs by issue summary. var ( @@ -188,23 +196,28 @@ func main() { for stack, counts := range stacks { id := stackID(stack) + var info0 Info // an arbitrary Info for this stack var total int64 - for _, count := range counts { + for info, count := range counts { + info0 = info total += count } if issue, ok := issuesByStackID[id]; ok { // existing issue + // TODO(adonovan): use ANSI tty color codes for Issue.State. summary := fmt.Sprintf("#%d: %s [%s]", issue.Number, issue.Title, issue.State) existingIssues[summary] += total } else { // new issue - title := newIssue(stack, id, stackToURL[stack], counts) + title := newIssue(stack, id, info0, stackToURL[stack], counts) summary := fmt.Sprintf("%s: %s [%s]", id, title, "new") newIssues[summary] += total } } + + fmt.Printf("Found %d distinct stacks in last %v days:\n", distinctStacks, *daysFlag) print := func(caption string, issues map[string]int64) { // Print items in descending frequency. keys := moremaps.KeySlice(issues) @@ -214,6 +227,7 @@ func main() { fmt.Printf("%s issues:\n", caption) for _, summary := range keys { count := issues[summary] + // TODO(adonovan): use ANSI tty codes to show high n values in bold. fmt.Printf("%s (n=%d)\n", summary, count) } } @@ -221,12 +235,26 @@ func main() { print("New", newIssues) } +type Info struct { + Program string // "golang.org/x/tools/gopls" + Version, GoVersion string // e.g. "gopls/v0.16.1", "go1.23" + GOOS, GOARCH string + Client string // e.g. "vscode" +} + +func (info Info) String() string { + return fmt.Sprintf("%s@%s %s %s/%s %s", + info.Program, info.Version, + info.GoVersion, info.GOOS, info.GOARCH, + info.Client) +} + // stackID returns a 32-bit identifier for a stack // suitable for use in GitHub issue titles. func stackID(stack string) string { // Encode it using base64 (6 bytes) for brevity, // as a single issue's body might contain multiple IDs - // if separate issues with same cause wre manually de-duped, + // if separate issues with same cause were manually de-duped, // e.g. "AAAAAA, BBBBBB" // // https://hbfs.wordpress.com/2012/03/30/finding-collisions: @@ -245,11 +273,14 @@ func stackID(stack string) string { // manually de-dup the issue before deciding whether to submit the form.) // // It returns the title. -func newIssue(stack, id, jsonURL string, counts map[string]int64) string { +func newIssue(stack, id string, info Info, jsonURL string, counts map[Info]int64) string { // Use a heuristic to find a suitable symbol to blame // in the title: the first public function or method // of a public type, in gopls, to appear in the stack // trace. We can always refine it later. + // + // TODO(adonovan): include in the issue a source snippet ±5 + // lines around the PC in this symbol. var symbol string for _, line := range strings.Split(stack, "\n") { // Look for: @@ -273,19 +304,92 @@ func newIssue(stack, id, jsonURL string, counts map[string]int64) string { } // Populate the form (title, body, label) - title := fmt.Sprintf("x/tools/gopls:%s bug reported by telemetry", symbol) + title := fmt.Sprintf("x/tools/gopls: bug in %s", symbol) body := new(bytes.Buffer) fmt.Fprintf(body, "This stack `%s` was [reported by telemetry](%s):\n\n", id, jsonURL) - fmt.Fprintf(body, "```\n%s\n```\n", stack) + + // Read the mapping from symbols to file/line. + pclntab, err := readPCLineTable(info) + if err != nil { + log.Fatal(err) + } + + // Parse the stack and get the symbol names out. + for _, line := range strings.Split(stack, "\n") { + // e.g. "golang.org/x/tools/gopls/foo.(*Type).Method.inlined.func3:+5" + symbol, offset, ok := strings.Cut(line, ":") + if !ok { + // Not a symbol (perhaps stack counter title: "gopls/bug"?) + fmt.Fprintf(body, "`%s`\n", line) + continue + } + fileline, ok := pclntab[symbol] + if !ok { + // objdump reports ELF symbol names, which in + // rare cases may be the Go symbols of + // runtime.CallersFrames mangled by (e.g.) the + // addition of .abi0 suffix; see + // https://github.com/golang/go/issues/69390#issuecomment-2343795920 + // So this should not be a hard error. + log.Printf("no pclntab info for symbol: %s", symbol) + fmt.Fprintf(body, "`%s`\n", line) + continue + } + if offset == "" { + log.Fatalf("missing line offset: %s", line) + } + offsetNum, err := strconv.Atoi(offset[1:]) + if err != nil { + log.Fatalf("invalid line offset: %s", line) + } + linenum := fileline.line + switch offset[0] { + case '-': + linenum -= offsetNum + case '+': + linenum += offsetNum + case '=': + linenum = offsetNum + } + + // Construct CodeSearch URL. + var url string + if firstSegment, _, _ := strings.Cut(fileline.file, "/"); !strings.Contains(firstSegment, ".") { + // std + // (First segment is a dir beneath GOROOT/src, not a module domain name.) + url = fmt.Sprintf("https://cs.opensource.google/go/go/+/%s:src/%s;l=%d", + info.GoVersion, fileline.file, linenum) + + } else if rest, ok := strings.CutPrefix(fileline.file, "golang.org/x/tools"); ok { + // in x/tools repo (tools or gopls module) + if rest[0] == '/' { + // "golang.org/x/tools/gopls" -> "gopls" + rest = rest[1:] + } else if rest[0] == '@' { + // "golang.org/x/tools@version/dir/file.go" -> "dir/file.go" + rest = rest[strings.Index(rest, "/")+1:] + } + + url = fmt.Sprintf("https://cs.opensource.google/go/x/tools/+/%s:%s;l=%d", + "gopls/"+info.Version, rest, linenum) + + } else { + // TODO(adonovan): support other module dependencies of gopls. + log.Printf("no CodeSearch URL for %q (%s:%d)", + symbol, fileline.file, linenum) + } + + // Emit stack frame. + if url != "" { + fmt.Fprintf(body, "- [`%s`](%s)\n", line, url) + } else { + fmt.Fprintf(body, "- `%s`\n", line) + } + } // Add counts, gopls version, and platform info. // This isn't very precise but should provide clues. - // - // TODO(adonovan): link each stack (ideally each frame) to source: - // https://cs.opensource.google/go/x/tools/+/gopls/VERSION:gopls/FILE;l=LINE - // (Requires parsing stack, shallow-cloning gopls module at that tag, and - // computing correct line offsets. Would be labor-saving though.) fmt.Fprintf(body, "```\n") for info, count := range counts { fmt.Fprintf(body, "%s (%d)\n", info, count) @@ -371,3 +475,130 @@ func min(x, y int) int { return y } } + +// -- pclntab -- + +type FileLine struct { + file string // "module@version/dir/file.go" or path relative to $GOROOT/src + line int +} + +// readPCLineTable builds the gopls executable specified by info, +// reads its PC-to-line-number table, and returns the file/line of +// each TEXT symbol. +func readPCLineTable(info Info) (map[string]FileLine, error) { + // The stacks dir will be a semi-durable temp directory + // (i.e. lasts for at least hours) holding source trees + // and executables we have build recently. + // + // Each subdir will hold a specific revision. + stacksDir := "/tmp/gopls-stacks" + if err := os.MkdirAll(stacksDir, 0777); err != nil { + return nil, fmt.Errorf("can't create stacks dir: %v", err) + } + + // Fetch the source for the tools repo, + // shallow-cloning just the desired revision. + // (Skip if it's already cloned.) + revDir := filepath.Join(stacksDir, info.Version) + if !fileExists(revDir) { + log.Printf("cloning tools@gopls/%s", info.Version) + if err := shallowClone(revDir, "https://go.googlesource.com/tools", "gopls/"+info.Version); err != nil { + return nil, fmt.Errorf("clone: %v", err) + } + } + + // Build the executable with the correct GOTOOLCHAIN, GOOS, GOARCH. + // Use -trimpath for normalized file names. + // (Skip if it's already built.) + exe := fmt.Sprintf("exe-%s.%s-%s", info.GoVersion, info.GOOS, info.GOARCH) + cmd := exec.Command("go", "build", "-trimpath", "-o", "../"+exe) + cmd.Dir = filepath.Join(revDir, "gopls") + cmd.Env = append(os.Environ(), + "GOTOOLCHAIN="+info.GoVersion, + "GOOS="+info.GOOS, + "GOARCH="+info.GOARCH, + ) + if !fileExists(filepath.Join(revDir, exe)) { + log.Printf("building %s@%s with %s on %s/%s", + info.Program, info.Version, info.GoVersion, info.GOOS, info.GOARCH) + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("building: %v", err) + } + } + + // Read pclntab of executable. + cmd = exec.Command("go", "tool", "objdump", exe) + cmd.Stdout = new(strings.Builder) + cmd.Stderr = os.Stderr + cmd.Dir = revDir + cmd.Env = append(os.Environ(), + "GOTOOLCHAIN="+info.GoVersion, + "GOOS="+info.GOOS, + "GOARCH="+info.GOARCH, + ) + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("reading pclntab %v", err) + } + pclntab := make(map[string]FileLine) + lines := strings.Split(fmt.Sprint(cmd.Stdout), "\n") + for i, line := range lines { + // Each function is of this form: + // + // TEXT symbol(SB) filename + // basename.go:line instruction + // ... + if !strings.HasPrefix(line, "TEXT ") { + continue + } + fields := strings.Fields(line) + if len(fields) != 3 { + continue // symbol without file (e.g. go:buildid) + } + + symbol := strings.TrimSuffix(fields[1], "(SB)") + + filename := fields[2] + + _, line, ok := strings.Cut(strings.Fields(lines[i+1])[0], ":") + if !ok { + return nil, fmt.Errorf("can't parse 'basename.go:line' from first instruction of %s:\n%s", + symbol, line) + } + linenum, err := strconv.Atoi(line) + if err != nil { + return nil, fmt.Errorf("can't parse line number of %s: %s", symbol, line) + } + pclntab[symbol] = FileLine{filename, linenum} + } + + return pclntab, nil +} + +// shallowClone performs a shallow clone of repo into dir at the given +// 'commitish' ref (any commit reference understood by git). +// +// The directory dir must not already exist. +func shallowClone(dir, repo, commitish string) error { + if err := os.Mkdir(dir, 0750); err != nil { + return fmt.Errorf("creating dir for %s: %v", repo, err) + } + + // Set a timeout for git fetch. If this proves flaky, it can be removed. + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + // Use a shallow fetch to download just the relevant commit. + shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", repo, commitish) + initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) + initCmd.Dir = dir + if output, err := initCmd.CombinedOutput(); err != nil { + return fmt.Errorf("checking out %s: %v\n%s", repo, err, output) + } + return nil +} + +func fileExists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} From 198986d2ddbacf9079af2ab61e6e24fda15631a1 Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 12 Sep 2024 12:40:51 -0700 Subject: [PATCH 022/102] go/ssa: go/ssa: disable TestTypeparamTest on wasm watchflakes reported additional test cases for TestTypeparamTest on wasm. Skipping the test when on wasm. Fixes golang/go#69410 Fixes golang/go#69409 Change-Id: Icfe5da007bf7411e9875313bc1b190751f1c1a15 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612855 Commit-Queue: Tim King Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Tim King --- go/ssa/builder_test.go | 10 +++++++--- go/ssa/interp/interp_test.go | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index 921fe51aa16..6ef8a86d728 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -777,6 +777,13 @@ func TestTypeparamTest(t *testing.T) { // Tests use a fake goroot to stub out standard libraries with declarations in // testdata/src. Decreases runtime from ~80s to ~1s. + if runtime.GOARCH == "wasm" { + // Consistent flakes on wasm (#64726, #69409, #69410). + // Needs more investigation, but more likely a wasm issue + // Disabling for now. + t.Skip("Consistent flakes on wasm (e.g. https://go.dev/issues/64726)") + } + dir := filepath.Join(build.Default.GOROOT, "test", "typeparam") // Collect all of the .go files in @@ -786,9 +793,6 @@ func TestTypeparamTest(t *testing.T) { } for _, entry := range list { - if entry.Name() == "chans.go" && runtime.GOARCH == "wasm" { - continue // https://go.dev/issues/64726 runtime: found bad pointer - } if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { continue // Consider standalone go files. } diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index c55fe36c425..52270f78b1a 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go @@ -307,6 +307,11 @@ func TestGorootTest(t *testing.T) { // in $GOROOT/test/typeparam/*.go. func TestTypeparamTest(t *testing.T) { + if runtime.GOARCH == "wasm" { + // See ssa/TestTypeparamTest. + t.Skip("Consistent flakes on wasm (e.g. https://go.dev/issues/64726)") + } + goroot := makeGoroot(t) // Skip known failures for the given reason. From 91d4bdb347baf3a1685f94c3de8054bd46e6c97a Mon Sep 17 00:00:00 2001 From: Kir Kolyshkin Date: Thu, 12 Sep 2024 11:59:03 -0700 Subject: [PATCH 023/102] gopls: rm GofumptFormat from internal/settings Since CL 612055 var GofumptFormat can never be nil, and since CL 609655 it is a very simple wrapper. Remove it, and use mvdan.cc/gofumpt/format directly in internal/golang. Note that this removes some documentation bits about gofumpt options, but those are already described in internal/golang.Format inline comments. Change-Id: Ic7d5b8412e913f2dbbc14befb978f8a4f743334a Reviewed-on: https://go-review.googlesource.com/c/tools/+/611844 LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley Reviewed-by: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/golang/format.go | 12 ++++++------ gopls/internal/settings/gofumpt.go | 21 --------------------- 2 files changed, 6 insertions(+), 27 deletions(-) delete mode 100644 gopls/internal/settings/gofumpt.go diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index 3fffbe3bfff..8f735f38cf4 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go @@ -21,12 +21,12 @@ import ( "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/tokeninternal" + gofumptFormat "mvdan.cc/gofumpt/format" ) // Format formats a file with a given range. @@ -67,7 +67,7 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr // Apply additional formatting, if any is supported. Currently, the only // supported additional formatter is gofumpt. - if format := settings.GofumptFormat; snapshot.Options().Gofumpt && format != nil { + if snapshot.Options().Gofumpt { // gofumpt can customize formatting based on language version and module // path, if available. // @@ -76,17 +76,17 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr // TODO: under which circumstances can we fail to find module information? // Can this, for example, result in inconsistent formatting across saves, // due to pending calls to packages.Load? - var langVersion, modulePath string + var opts gofumptFormat.Options meta, err := NarrowestMetadataForFile(ctx, snapshot, fh.URI()) if err == nil { if mi := meta.Module; mi != nil { if v := mi.GoVersion; v != "" { - langVersion = "go" + v + opts.LangVersion = "go" + v } - modulePath = mi.Path + opts.ModulePath = mi.Path } } - b, err := format(ctx, langVersion, modulePath, buf.Bytes()) + b, err := gofumptFormat.Source(buf.Bytes(), opts) if err != nil { return nil, err } diff --git a/gopls/internal/settings/gofumpt.go b/gopls/internal/settings/gofumpt.go deleted file mode 100644 index d9bf3109d8c..00000000000 --- a/gopls/internal/settings/gofumpt.go +++ /dev/null @@ -1,21 +0,0 @@ -// 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. - -package settings - -import ( - "context" - - "mvdan.cc/gofumpt/format" -) - -// GofumptFormat allows the gopls module to wire in a call to -// gofumpt/format.Source. langVersion and modulePath are used for some -// Gofumpt formatting rules -- see the Gofumpt documentation for details. -var GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { - return format.Source(src, format.Options{ - LangVersion: langVersion, - ModulePath: modulePath, - }) -} From 7891473c0188eacbae86ac0ddd94af6f8c407534 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 13 Sep 2024 10:17:44 -0400 Subject: [PATCH 024/102] gopls/internal/telemetry/cmd/stacks: fix two bugs 1. There were three early returns in the frame -> URL computation, though it was hard to see, and their formatting logic differed. This CL factors them, extracting the frameURL function. 2. When git clone fails (e.g. due to no SSO cert), we failed to clean up the empty dir, causing a persistently stuck failure state. Now we attempt to clean up the directory. (This won't help when the program is terminated from without.) Change-Id: I0f7e2dd26b95899ec85b3e4666def374dc8caadd Reviewed-on: https://go-review.googlesource.com/c/tools/+/613076 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- gopls/internal/telemetry/cmd/stacks/stacks.go | 140 +++++++++--------- 1 file changed, 72 insertions(+), 68 deletions(-) diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go index 5d4727a0650..dc33380e373 100644 --- a/gopls/internal/telemetry/cmd/stacks/stacks.go +++ b/gopls/internal/telemetry/cmd/stacks/stacks.go @@ -316,75 +316,11 @@ func newIssue(stack, id string, info Info, jsonURL string, counts map[Info]int64 } // Parse the stack and get the symbol names out. - for _, line := range strings.Split(stack, "\n") { - // e.g. "golang.org/x/tools/gopls/foo.(*Type).Method.inlined.func3:+5" - symbol, offset, ok := strings.Cut(line, ":") - if !ok { - // Not a symbol (perhaps stack counter title: "gopls/bug"?) - fmt.Fprintf(body, "`%s`\n", line) - continue - } - fileline, ok := pclntab[symbol] - if !ok { - // objdump reports ELF symbol names, which in - // rare cases may be the Go symbols of - // runtime.CallersFrames mangled by (e.g.) the - // addition of .abi0 suffix; see - // https://github.com/golang/go/issues/69390#issuecomment-2343795920 - // So this should not be a hard error. - log.Printf("no pclntab info for symbol: %s", symbol) - fmt.Fprintf(body, "`%s`\n", line) - continue - } - if offset == "" { - log.Fatalf("missing line offset: %s", line) - } - offsetNum, err := strconv.Atoi(offset[1:]) - if err != nil { - log.Fatalf("invalid line offset: %s", line) - } - linenum := fileline.line - switch offset[0] { - case '-': - linenum -= offsetNum - case '+': - linenum += offsetNum - case '=': - linenum = offsetNum - } - - // Construct CodeSearch URL. - var url string - if firstSegment, _, _ := strings.Cut(fileline.file, "/"); !strings.Contains(firstSegment, ".") { - // std - // (First segment is a dir beneath GOROOT/src, not a module domain name.) - url = fmt.Sprintf("https://cs.opensource.google/go/go/+/%s:src/%s;l=%d", - info.GoVersion, fileline.file, linenum) - - } else if rest, ok := strings.CutPrefix(fileline.file, "golang.org/x/tools"); ok { - // in x/tools repo (tools or gopls module) - if rest[0] == '/' { - // "golang.org/x/tools/gopls" -> "gopls" - rest = rest[1:] - } else if rest[0] == '@' { - // "golang.org/x/tools@version/dir/file.go" -> "dir/file.go" - rest = rest[strings.Index(rest, "/")+1:] - } - - url = fmt.Sprintf("https://cs.opensource.google/go/x/tools/+/%s:%s;l=%d", - "gopls/"+info.Version, rest, linenum) - - } else { - // TODO(adonovan): support other module dependencies of gopls. - log.Printf("no CodeSearch URL for %q (%s:%d)", - symbol, fileline.file, linenum) - } - - // Emit stack frame. - if url != "" { - fmt.Fprintf(body, "- [`%s`](%s)\n", line, url) + for _, frame := range strings.Split(stack, "\n") { + if url := frameURL(pclntab, info, frame); url != "" { + fmt.Fprintf(body, "- [`%s`](%s)\n", frame, url) } else { - fmt.Fprintf(body, "- `%s`\n", line) + fmt.Fprintf(body, "- `%s`\n", frame) } } @@ -411,6 +347,73 @@ func newIssue(stack, id string, info Info, jsonURL string, counts map[Info]int64 return title } +// frameURL returns the CodeSearch URL for the stack frame, if known. +func frameURL(pclntab map[string]FileLine, info Info, frame string) string { + // e.g. "golang.org/x/tools/gopls/foo.(*Type).Method.inlined.func3:+5" + symbol, offset, ok := strings.Cut(frame, ":") + if !ok { + // Not a symbol (perhaps stack counter title: "gopls/bug"?) + return "" + } + + fileline, ok := pclntab[symbol] + if !ok { + // objdump reports ELF symbol names, which in + // rare cases may be the Go symbols of + // runtime.CallersFrames mangled by (e.g.) the + // addition of .abi0 suffix; see + // https://github.com/golang/go/issues/69390#issuecomment-2343795920 + // So this should not be a hard error. + log.Printf("no pclntab info for symbol: %s", symbol) + return "" + } + + if offset == "" { + log.Fatalf("missing line offset: %s", frame) + } + offsetNum, err := strconv.Atoi(offset[1:]) + if err != nil { + log.Fatalf("invalid line offset: %s", frame) + } + linenum := fileline.line + switch offset[0] { + case '-': + linenum -= offsetNum + case '+': + linenum += offsetNum + case '=': + linenum = offsetNum + } + + // Construct CodeSearch URL. + firstSegment, _, _ := strings.Cut(fileline.file, "/") + if !strings.Contains(firstSegment, ".") { + // std + // (First segment is a dir beneath GOROOT/src, not a module domain name.) + return fmt.Sprintf("https://cs.opensource.google/go/go/+/%s:src/%s;l=%d", + info.GoVersion, fileline.file, linenum) + } + + if rest, ok := strings.CutPrefix(fileline.file, "golang.org/x/tools"); ok { + // in x/tools repo (tools or gopls module) + if rest[0] == '/' { + // "golang.org/x/tools/gopls" -> "gopls" + rest = rest[1:] + } else if rest[0] == '@' { + // "golang.org/x/tools@version/dir/file.go" -> "dir/file.go" + rest = rest[strings.Index(rest, "/")+1:] + } + + return fmt.Sprintf("https://cs.opensource.google/go/x/tools/+/%s:%s;l=%d", + "gopls/"+info.Version, rest, linenum) + } + + // TODO(adonovan): support other module dependencies of gopls. + log.Printf("no CodeSearch URL for %q (%s:%d)", + symbol, fileline.file, linenum) + return "" +} + // -- GitHub search -- // searchIssues queries the GitHub issue tracker. @@ -504,6 +507,7 @@ func readPCLineTable(info Info) (map[string]FileLine, error) { if !fileExists(revDir) { log.Printf("cloning tools@gopls/%s", info.Version) if err := shallowClone(revDir, "https://go.googlesource.com/tools", "gopls/"+info.Version); err != nil { + _ = os.RemoveAll(revDir) // ignore errors return nil, fmt.Errorf("clone: %v", err) } } From 8fcd92f832cbbe49e198c7574af755362816ed64 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 13 Sep 2024 15:38:41 -0400 Subject: [PATCH 025/102] gopls/internal/telemetry/cmd/stacks: predicate de-duplication This CL introduces an expression language for matching stacks, inspired by watchflakes. Each issue has a block at the start of its body of this form: ``` #!stacks "bug.Reportf" && "golang.Hover:+19" ``` where the expression is a sentence of this grammar: expr = "string literal" | ( expr ) | expr && expr | expr || expr | ! expr A string literal implies a substring match against a stack trace; the other forms are boolean operations. The stacks command reads all such predicates at start, and uses them to associate new stacks with existing issues. (It reports an error if a stack is claimed by two issues.) For each claim, it updates the issue by adding a comment describing all the new stacks (example: https://github.com/golang/go/issues/60890#issuecomment-2350023305) and it adds/updates the "Dups: " list on the last line of the issue body (first comment). This should greatly reduce the amount of toil in associating stacks with issues, since we can just tweak the predicates to accommodate minor variations. The GitHub auth token now needs R/W access to golang/go issues. Fixes golang/go#65963 Change-Id: I836cd89bba456826839a389271ac38745e493a54 Reviewed-on: https://go-review.googlesource.com/c/tools/+/613215 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/telemetry/cmd/stacks/stacks.go | 462 +++++++++++++++--- 1 file changed, 398 insertions(+), 64 deletions(-) diff --git a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go index dc33380e373..a0d9b3c30df 100644 --- a/gopls/internal/telemetry/cmd/stacks/stacks.go +++ b/gopls/internal/telemetry/cmd/stacks/stacks.go @@ -5,6 +5,51 @@ // The stacks command finds all gopls stack traces reported by // telemetry in the past 7 days, and reports their associated GitHub // issue, creating new issues as needed. +// +// The association of stacks with GitHub issues (labelled +// gopls/telemetry-wins) is represented in two different ways by the +// body (first comment) of the issue: +// +// 1. Each distinct stack is identified by an ID, 6-digit base64 +// string such as "TwtkSg". If a stack's ID appears anywhere +// within the issue body, the stack is associated with the issue. +// +// Some problems are highly deterministic, resulting in many +// field reports of the exact same stack. For such problems, a +// single ID in the issue body suffices to record the +// association. But most problems are exhibited in a variety of +// ways, leading to multiple field reports of similar but +// distinct stacks. +// +// 2. Each GitHub issue body may start with a code block of this form: +// +// ``` +// #!stacks +// "runtime.sigpanic" && "golang.hover:+170" +// ``` +// +// The first line indicates the purpose of the block; the +// remainder is a predicate that matches stacks. +// It is an expression defined by this grammar: +// +// > expr = "string literal" +// > | ( expr ) +// > | ! expr +// > | expr && expr +// > | expr || expr +// +// Each string literal implies a substring match on the stack; +// the other productions are boolean operations. +// +// The stacks command gathers all such predicates out of the +// labelled issues and evaluates each one against each new stack. +// If the predicate for an issue matches, the issue is considered +// to have "claimed" the stack: the stack command appends a +// comment containing the new (variant) stack to the issue, and +// appends the stack's ID to the last line of the issue body. +// +// It is an error if two issues' predicates attempt to claim the +// same stack. package main // TODO(adonovan): create a proper package with tests. Much of this @@ -17,6 +62,9 @@ import ( "encoding/json" "flag" "fmt" + "go/ast" + "go/parser" + "go/token" "hash/fnv" "io" "log" @@ -29,6 +77,7 @@ import ( "strconv" "strings" "time" + "unicode" "golang.org/x/telemetry" "golang.org/x/tools/gopls/internal/util/browser" @@ -39,7 +88,7 @@ import ( var ( daysFlag = flag.Int("days", 7, "number of previous days of telemetry data to read") - token string // optional GitHub authentication token, to relax the rate limit + authToken string // mandatory GitHub authentication token (for R/W issues access) ) func main() { @@ -51,8 +100,8 @@ func main() { // // You can create one using the flow at: GitHub > You > Settings > // Developer Settings > Personal Access Tokens > Fine-grained tokens > - // Generate New Token. Generate the token on behalf of yourself - // (not "golang" or "google"), with no special permissions. + // Generate New Token. Generate the token on behalf of golang/go + // with R/W access to "Issues". // The token is typically of the form "github_pat_XXX", with 82 hex digits. // Save it in the file, with mode 0400. // @@ -69,9 +118,9 @@ func main() { if !os.IsNotExist(err) { log.Fatalf("cannot read GitHub authentication token: %v", err) } - log.Printf("no file %s containing GitHub authentication token; continuing without authentication, which is subject to stricter rate limits (https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api).", tokenFile) + log.Fatalf("no file %s containing GitHub authentication token.", tokenFile) } - token = string(bytes.TrimSpace(content)) + authToken = string(bytes.TrimSpace(content)) } // Maps stack text to Info to count. @@ -152,36 +201,138 @@ func main() { } } - // Compute IDs of all stacks. - var stackIDs []string - for stack := range stacks { - stackIDs = append(stackIDs, stackID(stack)) + // Query GitHub for all existing GitHub issues with label:gopls/telemetry-wins. + // + // TODO(adonovan): by default GitHub returns at most 30 + // issues; we have lifted this to 100 using per_page=%d, but + // that won't work forever; use paging. + const query = "is:issue label:gopls/telemetry-wins" + res, err := searchIssues(query) + if err != nil { + log.Fatalf("GitHub issues query %q failed: %v", query, err) } - // Query GitHub for existing GitHub issues. - // (Note: there may be multiple Issue records - // for the same logical issue, i.e. Issue.Number.) - issuesByStackID := make(map[string]*Issue) - for len(stackIDs) > 0 { - // For some reason GitHub returns 422 UnprocessableEntity - // if we attempt to read more than 6 at once. - batch := stackIDs[:min(6, len(stackIDs))] - stackIDs = stackIDs[len(batch):] + // Extract and validate predicate expressions in ```#!stacks...``` code blocks. + // See the package doc comment for the grammar. + for _, issue := range res.Items { + block := findPredicateBlock(issue.Body) + if block != "" { + expr, err := parser.ParseExpr(block) + if err != nil { + log.Printf("invalid predicate in issue #%d: %v\n<<%s>>", + issue.Number, err, block) + continue + } + var validate func(ast.Expr) error + validate = func(e ast.Expr) error { + switch e := e.(type) { + case *ast.UnaryExpr: + if e.Op != token.NOT { + return fmt.Errorf("invalid op: %s", e.Op) + } + return validate(e.X) - query := "is:issue label:gopls/telemetry-wins in:body " + strings.Join(batch, " OR ") - res, err := searchIssues(query) - if err != nil { - log.Fatalf("GitHub issues query %q failed: %v", query, err) + case *ast.BinaryExpr: + if e.Op != token.LAND && e.Op != token.LOR { + return fmt.Errorf("invalid op: %s", e.Op) + } + if err := validate(e.X); err != nil { + return err + } + return validate(e.Y) + + case *ast.ParenExpr: + return validate(e.X) + + case *ast.BasicLit: + if e.Kind != token.STRING { + return fmt.Errorf("invalid literal (%s)", e.Kind) + } + if _, err := strconv.Unquote(e.Value); err != nil { + return err + } + + default: + return fmt.Errorf("syntax error (%T)", e) + } + return nil + } + if err := validate(expr); err != nil { + log.Printf("invalid predicate in issue #%d: %v\n<<%s>>", + issue.Number, err, block) + continue + } + issue.predicateText = block + issue.predicate = func(stack string) bool { + var eval func(ast.Expr) bool + eval = func(e ast.Expr) bool { + switch e := e.(type) { + case *ast.UnaryExpr: + return !eval(e.X) + + case *ast.BinaryExpr: + if e.Op == token.LAND { + return eval(e.X) && eval(e.Y) + } else { + return eval(e.X) || eval(e.Y) + } + + case *ast.ParenExpr: + return eval(e.X) + + case *ast.BasicLit: + substr, _ := strconv.Unquote(e.Value) + return strings.Contains(stack, substr) + } + panic("unreachable") + } + return eval(expr) + } } + } + + // Map each stack ID to its issue. + // + // An issue can claim a stack two ways: + // + // 1. if the issue body contains the ID of the stack. Matching + // is a little loose but base64 will rarely produce words + // that appear in the body by chance. + // + // 2. if the issue body contains a ```#!stacks``` predicate + // that matches the stack. + // + // We report an error if two different issues attempt to claim + // the same stack. + // + // This is O(new stacks x existing issues). + claimedBy := make(map[string]*Issue) + for stack := range stacks { + id := stackID(stack) for _, issue := range res.Items { - for _, id := range batch { - // Matching is a little fuzzy here - // but base64 will rarely produce - // words that appear in the body - // by chance. - if strings.Contains(issue.Body, id) { - issuesByStackID[id] = issue - } + byPredicate := false + if strings.Contains(issue.Body, id) { + // nop + } else if issue.predicate != nil && issue.predicate(stack) { + byPredicate = true + } else { + continue + } + + if prev := claimedBy[id]; prev != nil && prev != issue { + log.Printf("stack %s is claimed by issues #%d and #%d", + id, prev.Number, issue.Number) + continue + } + if false { + log.Printf("stack %s claimed by issue #%d", + id, issue.Number) + } + claimedBy[id] = issue + if byPredicate { + // The stack ID matched the predicate but was not + // found in the issue body, so this is a new stack. + issue.newStacks = append(issue.newStacks, stack) } } } @@ -196,27 +347,59 @@ func main() { for stack, counts := range stacks { id := stackID(stack) - var info0 Info // an arbitrary Info for this stack var total int64 - for info, count := range counts { - info0 = info + for _, count := range counts { total += count } - if issue, ok := issuesByStackID[id]; ok { + if issue, ok := claimedBy[id]; ok { // existing issue - // TODO(adonovan): use ANSI tty color codes for Issue.State. summary := fmt.Sprintf("#%d: %s [%s]", issue.Number, issue.Title, issue.State) existingIssues[summary] += total } else { // new issue - title := newIssue(stack, id, info0, stackToURL[stack], counts) + title := newIssue(stack, id, stackToURL[stack], counts) summary := fmt.Sprintf("%s: %s [%s]", id, title, "new") newIssues[summary] += total } } + // Update existing issues that claimed new stacks by predicate. + for _, issue := range res.Items { + if len(issue.newStacks) == 0 { + continue + } + + // Add a comment to the existing issue listing all its new stacks. + // (Save the ID of each stack for the second step.) + comment := new(bytes.Buffer) + var newStackIDs []string + for _, stack := range issue.newStacks { + id := stackID(stack) + newStackIDs = append(newStackIDs, id) + writeStackComment(comment, stack, id, stackToURL[stack], stacks[stack]) + } + if err := addIssueComment(issue.Number, comment.String()); err != nil { + log.Println(err) + continue + } + log.Printf("added comment to issue #%d", issue.Number) + + // Append to the "Dups: ID ..." list on last line of issue body. + body := strings.TrimSpace(issue.Body) + lastLineStart := strings.LastIndexByte(body, '\n') + 1 + lastLine := body[lastLineStart:] + if !strings.HasPrefix(lastLine, "Dups:") { + body += "\nDups:" + } + body += " " + strings.Join(newStackIDs, " ") + if err := updateIssueBody(issue.Number, body); err != nil { + log.Println(err) + } + log.Printf("updated body of issue #%d", issue.Number) + } + fmt.Printf("Found %d distinct stacks in last %v days:\n", distinctStacks, *daysFlag) print := func(caption string, issues map[string]int64) { // Print items in descending frequency. @@ -227,7 +410,6 @@ func main() { fmt.Printf("%s issues:\n", caption) for _, summary := range keys { count := issues[summary] - // TODO(adonovan): use ANSI tty codes to show high n values in bold. fmt.Printf("%s (n=%d)\n", summary, count) } } @@ -235,6 +417,8 @@ func main() { print("New", newIssues) } +// Info is used as a key for de-duping and aggregating. +// Do not add detail about particular records (e.g. data, telemetry URL). type Info struct { Program string // "golang.org/x/tools/gopls" Version, GoVersion string // e.g. "gopls/v0.16.1", "go1.23" @@ -273,7 +457,7 @@ func stackID(stack string) string { // manually de-dup the issue before deciding whether to submit the form.) // // It returns the title. -func newIssue(stack, id string, info Info, jsonURL string, counts map[Info]int64) string { +func newIssue(stack, id string, jsonURL string, counts map[Info]int64) string { // Use a heuristic to find a suitable symbol to blame // in the title: the first public function or method // of a public type, in gopls, to appear in the stack @@ -305,7 +489,44 @@ func newIssue(stack, id string, info Info, jsonURL string, counts map[Info]int64 // Populate the form (title, body, label) title := fmt.Sprintf("x/tools/gopls: bug in %s", symbol) + body := new(bytes.Buffer) + + // Add a placeholder ```#!stacks``` block since this is a new issue. + body.WriteString("```" + ` +#!stacks +"" +` + "```\n") + fmt.Fprintf(body, "Issue created by [stacks](https://pkg.go.dev/golang.org/x/tools/gopls/internal/telemetry/cmd/stacks).\n\n") + + writeStackComment(body, stack, id, jsonURL, counts) + + const labels = "gopls,Tools,gopls/telemetry-wins,NeedsInvestigation" + + // Report it. The user will interactively finish the task, + // since they will typically de-dup it without even creating a new issue + // by expanding the #!stacks predicate of an existing issue. + if !browser.Open("https://github.com/golang/go/issues/new?labels=" + labels + "&title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body.String())) { + log.Print("Please file a new issue at golang.org/issue/new using this template:\n\n") + log.Printf("Title: %s\n", title) + log.Printf("Labels: %s\n", labels) + log.Printf("Body: %s\n", body) + } + + return title +} + +// writeStackComment writes a stack in Markdown form, for a new GitHub +// issue or new comment on an existing one. +func writeStackComment(body *bytes.Buffer, stack, id string, jsonURL string, counts map[Info]int64) { + if len(counts) == 0 { + panic("no counts") + } + var info Info // pick an arbitrary key + for info = range counts { + break + } + fmt.Fprintf(body, "This stack `%s` was [reported by telemetry](%s):\n\n", id, jsonURL) @@ -331,20 +552,6 @@ func newIssue(stack, id string, info Info, jsonURL string, counts map[Info]int64 fmt.Fprintf(body, "%s (%d)\n", info, count) } fmt.Fprintf(body, "```\n\n") - - fmt.Fprintf(body, "Issue created by golang.org/x/tools/gopls/internal/telemetry/cmd/stacks.\n") - - const labels = "gopls,Tools,gopls/telemetry-wins,NeedsInvestigation" - - // Report it. - if !browser.Open("https://github.com/golang/go/issues/new?labels=" + labels + "&title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body.String())) { - log.Print("Please file a new issue at golang.org/issue/new using this template:\n\n") - log.Printf("Title: %s\n", title) - log.Printf("Labels: %s\n", labels) - log.Printf("Body: %s\n", body) - } - - return title } // frameURL returns the CodeSearch URL for the stack frame, if known. @@ -364,13 +571,19 @@ func frameURL(pclntab map[string]FileLine, info Info, frame string) string { // addition of .abi0 suffix; see // https://github.com/golang/go/issues/69390#issuecomment-2343795920 // So this should not be a hard error. - log.Printf("no pclntab info for symbol: %s", symbol) + if symbol != "runtime.goexit" { + log.Printf("no pclntab info for symbol: %s", symbol) + } return "" } if offset == "" { log.Fatalf("missing line offset: %s", frame) } + if unicode.IsDigit(rune(offset[0])) { + // Fix gopls/v0.14.2 legacy syntax ":%d" -> ":+%d". + offset = "+" + offset + } offsetNum, err := strconv.Atoi(offset[1:]) if err != nil { log.Fatalf("invalid line offset: %s", frame) @@ -386,16 +599,17 @@ func frameURL(pclntab map[string]FileLine, info Info, frame string) string { } // Construct CodeSearch URL. + + // std module? firstSegment, _, _ := strings.Cut(fileline.file, "/") if !strings.Contains(firstSegment, ".") { - // std // (First segment is a dir beneath GOROOT/src, not a module domain name.) return fmt.Sprintf("https://cs.opensource.google/go/go/+/%s:src/%s;l=%d", info.GoVersion, fileline.file, linenum) } + // x/tools repo (tools or gopls module)? if rest, ok := strings.CutPrefix(fileline.file, "golang.org/x/tools"); ok { - // in x/tools repo (tools or gopls module) if rest[0] == '/' { // "golang.org/x/tools/gopls" -> "gopls" rest = rest[1:] @@ -408,7 +622,17 @@ func frameURL(pclntab map[string]FileLine, info Info, frame string) string { "gopls/"+info.Version, rest, linenum) } - // TODO(adonovan): support other module dependencies of gopls. + // other x/ module dependency? + // e.g. golang.org/x/sync@v0.8.0/errgroup/errgroup.go + if rest, ok := strings.CutPrefix(fileline.file, "golang.org/x/"); ok { + if modVer, filename, ok := strings.Cut(rest, "/"); ok { + if mod, version, ok := strings.Cut(modVer, "@"); ok { + return fmt.Sprintf("https://cs.opensource.google/go/x/%s/+/%s:%s;l=%d", + mod, version, filename, linenum) + } + } + } + log.Printf("no CodeSearch URL for %q (%s:%d)", symbol, fileline.file, linenum) return "" @@ -420,34 +644,94 @@ func frameURL(pclntab map[string]FileLine, info Info, frame string) string { func searchIssues(query string) (*IssuesSearchResult, error) { q := url.QueryEscape(query) - req, err := http.NewRequest("GET", IssuesURL+"?q="+q, nil) + req, err := http.NewRequest("GET", "https://api.github.com/search/issues?q="+q+"&per_page=100", nil) if err != nil { return nil, err } - if token != "" { - req.Header.Add("Authorization", "Bearer "+token) + if authToken != "" { + req.Header.Add("Authorization", "Bearer "+authToken) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - resp.Body.Close() return nil, fmt.Errorf("search query failed: %s (body: %s)", resp.Status, body) } var result IssuesSearchResult if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - resp.Body.Close() return nil, err } - resp.Body.Close() return &result, nil } -// See https://developer.github.com/v3/search/#search-issues. +// updateIssueBody updates the body of the numbered issue. +func updateIssueBody(number int, body string) error { + // https://docs.github.com/en/rest/issues/comments#update-an-issue + var payload struct { + Body string `json:"body"` + } + payload.Body = body + data, err := json.Marshal(payload) + if err != nil { + return err + } -const IssuesURL = "https://api.github.com/search/issues" + url := fmt.Sprintf("https://api.github.com/repos/golang/go/issues/%d", number) + req, err := http.NewRequest("PATCH", url, bytes.NewReader(data)) + if err != nil { + return err + } + if authToken != "" { + req.Header.Add("Authorization", "Bearer "+authToken) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("issue update failed: %s (body: %s)", resp.Status, body) + } + return nil +} + +// addIssueComment adds a markdown comment to the numbered issue. +func addIssueComment(number int, comment string) error { + // https://docs.github.com/en/rest/issues/comments#create-an-issue-comment + var payload struct { + Body string `json:"body"` + } + payload.Body = comment + data, err := json.Marshal(payload) + if err != nil { + return err + } + + url := fmt.Sprintf("https://api.github.com/repos/golang/go/issues/%d/comments", number) + req, err := http.NewRequest("POST", url, bytes.NewReader(data)) + if err != nil { + return err + } + if authToken != "" { + req.Header.Add("Authorization", "Bearer "+authToken) + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to create issue comment: %s (body: %s)", resp.Status, body) + } + return nil +} + +// See https://developer.github.com/v3/search/#search-issues. type IssuesSearchResult struct { TotalCount int `json:"total_count"` @@ -462,6 +746,10 @@ type Issue struct { User *User CreatedAt time.Time `json:"created_at"` Body string // in Markdown format + + predicateText string // text of ```#!stacks...``` predicate block + predicate func(string) bool // matching predicate over stack text + newStacks []string // new stacks to add to existing issue (comments and IDs) } type User struct { @@ -606,3 +894,49 @@ func fileExists(filename string) bool { _, err := os.Stat(filename) return err == nil } + +// findPredicateBlock returns the content (sans "#!stacks") of the +// code block at the start of the issue body. +// Logic plundered from x/build/cmd/watchflakes/github.go. +func findPredicateBlock(body string) string { + // Extract ```-fenced or indented code block at start of issue description (body). + body = strings.ReplaceAll(body, "\r\n", "\n") + lines := strings.SplitAfter(body, "\n") + for len(lines) > 0 && strings.TrimSpace(lines[0]) == "" { + lines = lines[1:] + } + text := "" + // A code quotation is bracketed by sequence of 3+ backticks. + // (More than 3 are permitted so that one can quote 3 backticks.) + if len(lines) > 0 && strings.HasPrefix(lines[0], "```") { + marker := lines[0] + n := 0 + for n < len(marker) && marker[n] == '`' { + n++ + } + marker = marker[:n] + i := 1 + for i := 1; i < len(lines); i++ { + if strings.HasPrefix(lines[i], marker) && strings.TrimSpace(strings.TrimLeft(lines[i], "`")) == "" { + text = strings.Join(lines[1:i], "") + break + } + } + if i < len(lines) { + } + } else if strings.HasPrefix(lines[0], "\t") || strings.HasPrefix(lines[0], " ") { + i := 1 + for i < len(lines) && (strings.HasPrefix(lines[i], "\t") || strings.HasPrefix(lines[i], " ")) { + i++ + } + text = strings.Join(lines[:i], "") + } + + // Must start with #!stacks so we're sure it is for us. + hdr, rest, _ := strings.Cut(text, "\n") + hdr = strings.TrimSpace(hdr) + if hdr != "#!stacks" { + return "" + } + return rest +} From 5aac53c5ff1eeca116f7aad04dde7f9acf76e928 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 12 Sep 2024 22:55:36 -0400 Subject: [PATCH 026/102] gopls/internal/golang: Definition: jump to assembly This CL adds support for jumping to the definition of a function implemented in assembly. The first Definition query jumps to the Go declaration, as usual; this func has no body. Executing a second Definition query jumps to the assembly implementation, if found. + Test, doc, relnote Fixes golang/go#60531 Change-Id: I943a05d4a2a5b6a398450131831f49cc7c0754e4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612537 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/doc/features/navigation.md | 2 + gopls/doc/release/v0.17.0.md | 8 +++ gopls/internal/cache/load.go | 19 +++---- gopls/internal/cache/metadata/metadata.go | 3 +- gopls/internal/golang/definition.go | 52 +++++++++++++++++++ .../test/integration/misc/definition_test.go | 47 +++++++++++++++++ 6 files changed, 119 insertions(+), 12 deletions(-) diff --git a/gopls/doc/features/navigation.md b/gopls/doc/features/navigation.md index 5daac9039ff..00371341a7c 100644 --- a/gopls/doc/features/navigation.md +++ b/gopls/doc/features/navigation.md @@ -22,6 +22,8 @@ A definition query also works in these unexpected places: (like [`hover`](passive.md#hover)) the location of the linked symbol. - On a file name in a **[`go:embed` directive](https://pkg.go.dev/embed)**, it returns the location of the embedded file. +- On the declaration of a non-Go function (a `func` with no body), + it returns the location of the assembly implementation, if any, p2.init - main.main --> (main.S1).m - main.main --> p2.Foo - p2.Foo --> (p2.S2).m` + x.io/main.init --> x.io/p2.init + x.io/main.main --> (x.io/main.S1).m + x.io/main.main --> x.io/p2.Foo + x.io/p2.Foo --> (x.io/p2.S2).m` - conf := loader.Config{ - Build: fakeContext(map[string]string{"main": main, "p2": p2}), - } - conf.Import("main") - iprog, err := conf.Load() - if err != nil { - t.Fatalf("Load failed: %v", err) - } - prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics) + pkgs := testfiles.LoadPackages(t, txtar.Parse([]byte(src)), "./...") + prog, _ := ssautil.Packages(pkgs, ssa.InstantiateGenerics) prog.Build() cg := cha.CallGraph(prog) @@ -154,39 +152,35 @@ func TestCHAUnexported(t *testing.T) { } } -// Simplifying wrapper around buildutil.FakeContext for single-file packages. -func fakeContext(pkgs map[string]string) *build.Context { - pkgs2 := make(map[string]map[string]string) - for path, content := range pkgs { - pkgs2[path] = map[string]string{"x.go": content} - } - return buildutil.FakeContext(pkgs2) -} +// loadFile loads a built SSA package for a single-file "x.io/main" package. +// (Ideally all uses would be converted over to txtar files with explicit go.mod files.) +func loadFile(t testing.TB, filename string, mode ssa.BuilderMode) (*packages.Package, *ssa.Package) { + testenv.NeedsGoPackages(t) -func loadProgInfo(filename string, mode ssa.BuilderMode) (*ssa.Program, *ast.File, *ssa.Package, error) { - content, err := os.ReadFile(filename) + data, err := os.ReadFile(filename) if err != nil { - return nil, nil, nil, fmt.Errorf("couldn't read file '%s': %s", filename, err) + t.Fatal(err) } - - conf := loader.Config{ - ParserMode: parser.ParseComments, + dir := t.TempDir() + cfg := &packages.Config{ + Mode: packages.LoadAllSyntax, + Dir: dir, + Overlay: map[string][]byte{ + filepath.Join(dir, "go.mod"): []byte("module x.io\ngo 1.22"), + filepath.Join(dir, "main/main.go"): data, + }, + Env: append(os.Environ(), "GO111MODULES=on", "GOPATH=", "GOWORK=off", "GOPROXY=off"), } - f, err := conf.ParseFile(filename, content) + pkgs, err := packages.Load(cfg, "./main") if err != nil { - return nil, nil, nil, err + t.Fatal(err) } - - conf.CreateFromFiles("main", f) - iprog, err := conf.Load() - if err != nil { - return nil, nil, nil, err + if num := packages.PrintErrors(pkgs); num > 0 { + t.Fatalf("packages contained %d errors", num) } - - prog := ssautil.CreateProgram(iprog, mode) + prog, ssapkgs := ssautil.Packages(pkgs, mode) prog.Build() - - return prog, f, prog.Package(iprog.Created[0].Pkg), nil + return pkgs[0], ssapkgs[0] } // printGraph returns a string representation of cg involving only edges diff --git a/go/callgraph/cha/testdata/func.go b/go/callgraph/cha/testdata/func.go index a12f3f1fd3f..4db581d9bc9 100644 --- a/go/callgraph/cha/testdata/func.go +++ b/go/callgraph/cha/testdata/func.go @@ -1,5 +1,3 @@ -// +build ignore - package main // Test of dynamic function calls; no interfaces. diff --git a/go/callgraph/cha/testdata/generics.go b/go/callgraph/cha/testdata/generics.go index 0323c7582b6..3a63091b1bd 100644 --- a/go/callgraph/cha/testdata/generics.go +++ b/go/callgraph/cha/testdata/generics.go @@ -1,6 +1,3 @@ -//go:build ignore -// +build ignore - package main // Test of generic function calls. @@ -38,12 +35,12 @@ func f(h func(), g func(I), k func(A), a A, b B) { // (*A).Foo --> (A).Foo // (*B).Foo --> (B).Foo // f --> Bar -// f --> instantiated[main.A] -// f --> instantiated[main.A] -// f --> instantiated[main.B] +// f --> instantiated[x.io/main.A] +// f --> instantiated[x.io/main.A] +// f --> instantiated[x.io/main.B] // instantiated --> (*A).Foo // instantiated --> (*B).Foo // instantiated --> (A).Foo // instantiated --> (B).Foo -// instantiated[main.A] --> (A).Foo -// instantiated[main.B] --> (B).Foo +// instantiated[x.io/main.A] --> (A).Foo +// instantiated[x.io/main.B] --> (B).Foo diff --git a/go/callgraph/cha/testdata/iface.go b/go/callgraph/cha/testdata/iface.go index 8ca65e160aa..cd147f96d3b 100644 --- a/go/callgraph/cha/testdata/iface.go +++ b/go/callgraph/cha/testdata/iface.go @@ -1,5 +1,3 @@ -// +build ignore - package main // Test of interface calls. None of the concrete types are ever diff --git a/go/callgraph/cha/testdata/recv.go b/go/callgraph/cha/testdata/recv.go index a92255e06db..0ff16d3b34a 100644 --- a/go/callgraph/cha/testdata/recv.go +++ b/go/callgraph/cha/testdata/recv.go @@ -1,5 +1,3 @@ -// +build ignore - package main type I interface { diff --git a/go/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go index 20a7f28fea5..6e0b2dda7b5 100644 --- a/go/callgraph/rta/rta_test.go +++ b/go/callgraph/rta/rta_test.go @@ -19,10 +19,8 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/rta" - "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/testfiles" "golang.org/x/tools/txtar" ) @@ -42,7 +40,12 @@ func TestRTA(t *testing.T) { } for _, archive := range archivePaths { t.Run(archive, func(t *testing.T) { - pkgs := loadPackages(t, archive) + ar, err := txtar.ParseFile(archive) + if err != nil { + t.Fatal(err) + } + + pkgs := testfiles.LoadPackages(t, ar, "./...") // find the file which contains the expected result var f *ast.File @@ -83,42 +86,6 @@ func TestRTA(t *testing.T) { } } -// loadPackages unpacks the archive to a temporary directory and loads all packages within it. -func loadPackages(t *testing.T, archive string) []*packages.Package { - testenv.NeedsGoPackages(t) - - ar, err := txtar.ParseFile(archive) - if err != nil { - t.Fatal(err) - } - - fs, err := txtar.FS(ar) - if err != nil { - t.Fatal(err) - } - dir := testfiles.CopyToTmp(t, fs) - - var baseConfig = &packages.Config{ - Mode: packages.NeedSyntax | - packages.NeedTypesInfo | - packages.NeedDeps | - packages.NeedName | - packages.NeedFiles | - packages.NeedImports | - packages.NeedCompiledGoFiles | - packages.NeedTypes, - Dir: dir, - } - pkgs, err := packages.Load(baseConfig, "./...") - if err != nil { - t.Fatal(err) - } - if num := packages.PrintErrors(pkgs); num > 0 { - t.Fatalf("packages contained %d errors", num) - } - return pkgs -} - // check tests the RTA analysis results against the test expectations // defined by a comment starting with a line "WANT:". // diff --git a/go/callgraph/static/static_test.go b/go/callgraph/static/static_test.go index cf8392d2f7b..a0c587824d7 100644 --- a/go/callgraph/static/static_test.go +++ b/go/callgraph/static/static_test.go @@ -6,19 +6,27 @@ package static_test import ( "fmt" - "go/parser" "reflect" "sort" "testing" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/static" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/txtar" ) -const input = `package main +const input = ` + +-- go.mod -- +module x.io + +go 1.22 + +-- p/p.go -- +package main type C int func (C) f() @@ -51,7 +59,15 @@ func main() { } ` -const genericsInput = `package P +const genericsInput = ` + +-- go.mod -- +module x.io + +go 1.22 + +-- p/p.go -- +package p type I interface { F() @@ -83,50 +99,34 @@ func TestStatic(t *testing.T) { for _, e := range []struct { input string want []string - // typeparams must be true if input uses type parameters - typeparams bool }{ {input, []string{ "(*C).f -> (C).f", "f -> (C).f", "f -> f$1", "f -> g", - }, false}, + }}, {genericsInput, []string{ "(*A).F -> (A).F", "(*B).F -> (B).F", - "f -> instantiated[P.A]", - "f -> instantiated[P.B]", - "instantiated[P.A] -> (A).F", - "instantiated[P.B] -> (B).F", - }, true}, + "f -> instantiated[x.io/p.A]", + "f -> instantiated[x.io/p.B]", + "instantiated[x.io/p.A] -> (A).F", + "instantiated[x.io/p.B] -> (B).F", + }}, } { - conf := loader.Config{ParserMode: parser.ParseComments} - f, err := conf.ParseFile("P.go", e.input) - if err != nil { - t.Error(err) - continue - } - - conf.CreateFromFiles("P", f) - iprog, err := conf.Load() - if err != nil { - t.Error(err) - continue - } - - P := iprog.Created[0].Pkg - - prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics) + pkgs := testfiles.LoadPackages(t, txtar.Parse([]byte(e.input)), "./p") + prog, _ := ssautil.Packages(pkgs, ssa.InstantiateGenerics) prog.Build() + p := pkgs[0].Types cg := static.CallGraph(prog) var edges []string callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error { edges = append(edges, fmt.Sprintf("%s -> %s", - e.Caller.Func.RelString(P), - e.Callee.Func.RelString(P))) + e.Caller.Func.RelString(p), + e.Callee.Func.RelString(p))) return nil }) sort.Strings(edges) diff --git a/go/callgraph/vta/graph_test.go b/go/callgraph/vta/graph_test.go index a669a679977..ace80859e21 100644 --- a/go/callgraph/vta/graph_test.go +++ b/go/callgraph/vta/graph_test.go @@ -26,7 +26,7 @@ func TestNodeInterface(t *testing.T) { // - "foo" function // - "main" function and its // - first register instruction t0 := *gl - prog, _, err := testProg("testdata/src/simple.go", ssa.BuilderMode(0)) + prog, _, err := testProg(t, "testdata/src/simple.go", ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load testdata/src/simple.go program: %v", err) } @@ -71,8 +71,8 @@ func TestNodeInterface(t *testing.T) { {panicArg{}, "Panic", nil}, {recoverReturn{}, "Recover", nil}, } { - if test.s != test.n.String() { - t.Errorf("want %s; got %s", test.s, test.n.String()) + if removeModulePrefix(test.s) != removeModulePrefix(test.n.String()) { + t.Errorf("want %s; got %s", removeModulePrefix(test.s), removeModulePrefix(test.n.String())) } if test.t != test.n.Type() { t.Errorf("want %s; got %s", test.t, test.n.Type()) @@ -80,9 +80,15 @@ func TestNodeInterface(t *testing.T) { } } +// removeModulePrefix removes the "x.io/" module name prefix throughout s. +// (It is added by testProg.) +func removeModulePrefix(s string) string { + return strings.ReplaceAll(s, "x.io/", "") +} + func TestVtaGraph(t *testing.T) { // Get the basic type int from a real program. - prog, _, err := testProg("testdata/src/simple.go", ssa.BuilderMode(0)) + prog, _, err := testProg(t, "testdata/src/simple.go", ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load testdata/src/simple.go program: %v", err) } @@ -150,7 +156,7 @@ func vtaGraphStr(g vtaGraph) []string { } sort.Strings(succStr) entry := fmt.Sprintf("%v -> %v", n.String(), strings.Join(succStr, ", ")) - vgs = append(vgs, entry) + vgs = append(vgs, removeModulePrefix(entry)) } return vgs } @@ -196,7 +202,7 @@ func TestVTAGraphConstruction(t *testing.T) { "testdata/src/panic.go", } { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file, ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } diff --git a/go/callgraph/vta/helpers_test.go b/go/callgraph/vta/helpers_test.go index 64e26f1a479..59a9277f759 100644 --- a/go/callgraph/vta/helpers_test.go +++ b/go/callgraph/vta/helpers_test.go @@ -8,16 +8,17 @@ import ( "bytes" "fmt" "go/ast" - "go/parser" "os" + "path/filepath" "sort" "strings" "testing" "golang.org/x/tools/go/callgraph" + "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" ) @@ -37,32 +38,49 @@ func want(f *ast.File) []string { // testProg returns an ssa representation of a program at // `path`, assumed to define package "testdata," and the // test want result as list of strings. -func testProg(path string, mode ssa.BuilderMode) (*ssa.Program, []string, error) { - content, err := os.ReadFile(path) - if err != nil { - return nil, nil, err - } +func testProg(t testing.TB, path string, mode ssa.BuilderMode) (*ssa.Program, []string, error) { + // Set debug mode to exercise DebugRef instructions. + pkg, ssapkg := loadFile(t, path, mode|ssa.GlobalDebug) + return ssapkg.Prog, want(pkg.Syntax[0]), nil +} - conf := loader.Config{ - ParserMode: parser.ParseComments, - } +// loadFile loads a built SSA package for a single-file package "x.io/testdata". +// (Ideally all uses would be converted over to txtar files with explicit go.mod files.) +// +// TODO(adonovan): factor with similar loadFile in cha/cha_test.go. +func loadFile(t testing.TB, filename string, mode ssa.BuilderMode) (*packages.Package, *ssa.Package) { + testenv.NeedsGoPackages(t) - f, err := conf.ParseFile(path, content) + data, err := os.ReadFile(filename) if err != nil { - return nil, nil, err + t.Fatal(err) } - - conf.CreateFromFiles("testdata", f) - iprog, err := conf.Load() + dir := t.TempDir() + cfg := &packages.Config{ + Mode: packages.LoadAllSyntax, + Dir: dir, + Overlay: map[string][]byte{ + filepath.Join(dir, "go.mod"): fmt.Appendf(nil, "module x.io\ngo 1.%d", testenv.Go1Point()), + + filepath.Join(dir, "testdata", filepath.Base(filename)): data, + }, + } + pkgs, err := packages.Load(cfg, "./testdata") if err != nil { - return nil, nil, err + t.Fatal(err) } - - prog := ssautil.CreateProgram(iprog, mode) - // Set debug mode to exercise DebugRef instructions. - prog.Package(iprog.Created[0].Pkg).SetDebugMode(true) + if len(pkgs) != 1 { + t.Fatalf("got %d packages, want 1", len(pkgs)) + } + if len(pkgs[0].Syntax) != 1 { + t.Fatalf("got %d files, want 1", len(pkgs[0].Syntax)) + } + if num := packages.PrintErrors(pkgs); num > 0 { + t.Fatalf("packages contained %d errors", num) + } + prog, ssapkgs := ssautil.Packages(pkgs, mode) prog.Build() - return prog, want(f), nil + return pkgs[0], ssapkgs[0] } func firstRegInstr(f *ssa.Function) ssa.Value { @@ -112,7 +130,7 @@ func callGraphStr(g *callgraph.Graph) []string { sort.Strings(cs) entry := fmt.Sprintf("%v: %v", funcName(f), strings.Join(cs, "; ")) - gs = append(gs, entry) + gs = append(gs, removeModulePrefix(entry)) } return gs } diff --git a/go/callgraph/vta/testdata/src/simple.go b/go/callgraph/vta/testdata/src/simple.go index 71ddbe37163..24f2d458834 100644 --- a/go/callgraph/vta/testdata/src/simple.go +++ b/go/callgraph/vta/testdata/src/simple.go @@ -2,9 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// go:build ignore - -package testdata +package main var gl int diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go index 1780bf6568a..ce441eb7e1b 100644 --- a/go/callgraph/vta/vta_test.go +++ b/go/callgraph/vta/vta_test.go @@ -48,7 +48,7 @@ func TestVTACallGraph(t *testing.T) { for _, file := range files { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file, ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } @@ -77,7 +77,7 @@ func TestVTACallGraph(t *testing.T) { // enabled by having an arbitrary function set as input to CallGraph // instead of the whole program (i.e., ssautil.AllFunctions(prog)). func TestVTAProgVsFuncSet(t *testing.T) { - prog, want, err := testProg("testdata/src/callgraph_nested_ptr.go", ssa.BuilderMode(0)) + prog, want, err := testProg(t, "testdata/src/callgraph_nested_ptr.go", ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test `testdata/src/callgraph_nested_ptr.go`: %s", err) } @@ -154,7 +154,7 @@ func TestVTACallGraphGenerics(t *testing.T) { } for _, file := range files { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(file, ssa.InstantiateGenerics) + prog, want, err := testProg(t, file, ssa.InstantiateGenerics) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } @@ -174,7 +174,7 @@ func TestVTACallGraphGenerics(t *testing.T) { func TestVTACallGraphGo117(t *testing.T) { file := "testdata/src/go117.go" - prog, want, err := testProg(file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file, ssa.BuilderMode(0)) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } diff --git a/go/ssa/testutil_test.go b/go/ssa/testutil_test.go index f633f204c39..58680b282c6 100644 --- a/go/ssa/testutil_test.go +++ b/go/ssa/testutil_test.go @@ -105,6 +105,8 @@ func loadPackages(t testing.TB, src fs.FS, patterns ...string) []*packages.Packa // * checks that (*packages.Packages).Syntax contains one file, // * builds the *ssa.Package (and not its dependencies), and // * returns the built *ssa.Package and the loaded packages.Package. +// +// TODO(adonovan): factor with similar loadFile (2x) in cha/cha_test.go and vta/helpers_test.go. func buildPackage(t testing.TB, content string, mode ssa.BuilderMode) (*ssa.Package, *packages.Package) { name := parsePackageClause(t, content) diff --git a/internal/testfiles/testfiles.go b/internal/testfiles/testfiles.go index ec5fc7f6920..78733976b3b 100644 --- a/internal/testfiles/testfiles.go +++ b/internal/testfiles/testfiles.go @@ -14,6 +14,8 @@ import ( "strings" "testing" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" ) @@ -104,3 +106,42 @@ func ExtractTxtarFileToTmp(t testing.TB, file string) string { } return CopyToTmp(t, fs) } + +// LoadPackages loads typed syntax for all packages that match the +// patterns, interpreted relative to the archive root. +// +// The packages must be error-free. +func LoadPackages(t testing.TB, ar *txtar.Archive, patterns ...string) []*packages.Package { + testenv.NeedsGoPackages(t) + + fs, err := txtar.FS(ar) + if err != nil { + t.Fatal(err) + } + dir := CopyToTmp(t, fs) + + cfg := &packages.Config{ + Mode: packages.NeedSyntax | + packages.NeedTypesInfo | + packages.NeedDeps | + packages.NeedName | + packages.NeedFiles | + packages.NeedImports | + packages.NeedCompiledGoFiles | + packages.NeedTypes, + Dir: dir, + Env: append(os.Environ(), + "GO111MODULES=on", + "GOPATH=", + "GOWORK=off", + "GOPROXY=off"), + } + pkgs, err := packages.Load(cfg, patterns...) + if err != nil { + t.Fatal(err) + } + if num := packages.PrintErrors(pkgs); num > 0 { + t.Fatalf("packages contained %d errors", num) + } + return pkgs +} From f70550d233fd998bfdae39c066144b5bba81667c Mon Sep 17 00:00:00 2001 From: xieyuschen Date: Mon, 23 Sep 2024 11:38:19 +0800 Subject: [PATCH 055/102] go/ssa: migrate some tests in build_test.go away from loader For golang/go#69556 Change-Id: I9146de749112d046b9d27f012d2de25f9ff86382 Reviewed-on: https://go-review.googlesource.com/c/tools/+/614955 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: David Chase --- go/ssa/builder_test.go | 131 ++++------------------------------------- 1 file changed, 10 insertions(+), 121 deletions(-) diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index 0307ff7c8d4..c2e5590b2ec 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -500,24 +500,7 @@ func h(error) // t8 = phi [1: t7, 3: t4] #e // ... - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.BuilderMode(0)) - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p, _ := buildPackage(t, input, ssa.BuilderMode(0)) g := p.Func("g") phis := 0 @@ -566,24 +549,7 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) // func init func() // var init$guard bool - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.BuilderMode(0)) - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p, _ := buildPackage(t, input, ssa.BuilderMode(0)) if load := p.Func("Load"); load.Signature.TypeParams().Len() != 1 { t.Errorf("expected a single type param T for Load got %q", load.Signature) @@ -619,25 +585,8 @@ var indirect = R[int].M // var thunk func(S[int]) int // var wrapper func(R[int]) int - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - for _, mode := range []ssa.BuilderMode{ssa.BuilderMode(0), ssa.InstantiateGenerics} { - // Create and build SSA - prog := ssautil.CreateProgram(lprog, mode) - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p, _ := buildPackage(t, input, mode) for _, entry := range []struct { name string // name of the package variable @@ -800,24 +749,7 @@ func sliceMax(s []int) []int { return s[a():b():c()] } ` - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.BuilderMode(0)) - p := prog.Package(lprog.Package("p").Pkg) - p.Build() + p, _ := buildPackage(t, input, ssa.BuilderMode(0)) for _, item := range []struct { fn string @@ -1031,23 +963,8 @@ func TestSyntax(t *testing.T) { var _ = F[P] // unreferenced => not instantiated ` - // Parse - var conf loader.Config - f, err := conf.ParseFile("", input) - if err != nil { - t.Fatalf("parse: %v", err) - } - conf.CreateFromFiles("p", f) - - // Load - lprog, err := conf.Load() - if err != nil { - t.Fatalf("Load: %v", err) - } - - // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.InstantiateGenerics) - prog.Build() + p, _ := buildPackage(t, input, ssa.InstantiateGenerics) + prog := p.Prog // Collect syntax information for all of the functions. got := make(map[string]string) @@ -1120,21 +1037,7 @@ func TestLabels(t *testing.T) { func main() { _:println(1); _:println(2)}`, } for _, test := range tests { - conf := loader.Config{Fset: token.NewFileSet()} - f, err := parser.ParseFile(conf.Fset, "", test, 0) - if err != nil { - t.Errorf("parse error: %s", err) - return - } - conf.CreateFromFiles("main", f) - iprog, err := conf.Load() - if err != nil { - t.Error(err) - continue - } - prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0)) - pkg := prog.Package(iprog.Created[0].Pkg) - pkg.Build() + buildPackage(t, test, ssa.BuilderMode(0)) } } @@ -1168,22 +1071,8 @@ func TestIssue67079(t *testing.T) { // Load the package. const src = `package p; type T int; func (T) f() {}; var _ = (*T).f` - conf := loader.Config{Fset: token.NewFileSet()} - f, err := parser.ParseFile(conf.Fset, "p.go", src, 0) - if err != nil { - t.Fatal(err) - } - conf.CreateFromFiles("p", f) - iprog, err := conf.Load() - if err != nil { - t.Fatal(err) - } - pkg := iprog.Created[0].Pkg - - // Create and build SSA program. - prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0)) - prog.Build() - + spkg, ppkg := buildPackage(t, src, ssa.BuilderMode(0)) + prog := spkg.Prog var g errgroup.Group // Access bodies of all functions. @@ -1202,7 +1091,7 @@ func TestIssue67079(t *testing.T) { // Force building of wrappers. g.Go(func() error { - ptrT := types.NewPointer(pkg.Scope().Lookup("T").Type()) + ptrT := types.NewPointer(ppkg.Types.Scope().Lookup("T").Type()) ptrTf := types.NewMethodSet(ptrT).At(0) // (*T).f symbol prog.MethodValue(ptrTf) return nil From fadcea546d4a5b9f89351998c594e6f4933f4ac2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 11 Sep 2024 15:33:33 -0400 Subject: [PATCH 056/102] gopls/internal/golang: CodeAction: split into producers This CL splits the ad hoc CodeAction function and its many callees into a uniform list of "producer" functions, each responsible for a single CodeActionKind. Each producer is called only if its kind is enabled by the request, and any actions created by the producer automatically have the appropriate kind. Only producers that request type information will get it. To avoid redundant computation when two producers need the same information, the lazyInit mechanism allows them to share common results, similar to sync.Once. For example, quickFix and sourceOrganizeImports share the results of a call to allImportsFixes. Change-Id: I4034970ed9b1e872d5c9bf4b085b98a121fa7144 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612495 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/golang/change_quote.go | 27 +- gopls/internal/golang/codeaction.go | 773 ++++++++++--------- gopls/internal/golang/invertifcondition.go | 2 +- gopls/internal/protocol/command/interface.go | 2 + gopls/internal/protocol/span.go | 6 + 5 files changed, 431 insertions(+), 379 deletions(-) diff --git a/gopls/internal/golang/change_quote.go b/gopls/internal/golang/change_quote.go index 6fa56d42615..e793ec72c89 100644 --- a/gopls/internal/golang/change_quote.go +++ b/gopls/internal/golang/change_quote.go @@ -11,10 +11,7 @@ import ( "strings" "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/gopls/internal/cache/parsego" - "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" @@ -26,22 +23,22 @@ import ( // Only the following conditions are true, the action in result is valid // - [start, end) is enclosed by a string literal // - if the string is interpreted string, need check whether the convert is allowed -func convertStringLiteral(pgf *parsego.File, fh file.Handle, startPos, endPos token.Pos) (protocol.CodeAction, bool) { - path, _ := astutil.PathEnclosingInterval(pgf.File, startPos, endPos) +func convertStringLiteral(req *codeActionsRequest) { + path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end) lit, ok := path[0].(*ast.BasicLit) if !ok || lit.Kind != token.STRING { - return protocol.CodeAction{}, false + return } str, err := strconv.Unquote(lit.Value) if err != nil { - return protocol.CodeAction{}, false + return } interpreted := lit.Value[0] == '"' // Not all "..." strings can be represented as `...` strings. if interpreted && !strconv.CanBackquote(strings.ReplaceAll(str, "\n", "")) { - return protocol.CodeAction{}, false + return } var ( @@ -56,24 +53,20 @@ func convertStringLiteral(pgf *parsego.File, fh file.Handle, startPos, endPos to newText = strconv.Quote(str) } - start, end, err := safetoken.Offsets(pgf.Tok, lit.Pos(), lit.End()) + start, end, err := safetoken.Offsets(req.pgf.Tok, lit.Pos(), lit.End()) if err != nil { bug.Reportf("failed to get string literal offset by token.Pos:%v", err) - return protocol.CodeAction{}, false + return } edits := []diff.Edit{{ Start: start, End: end, New: newText, }} - textedits, err := protocol.EditsFromDiffEdits(pgf.Mapper, edits) + textedits, err := protocol.EditsFromDiffEdits(req.pgf.Mapper, edits) if err != nil { bug.Reportf("failed to convert diff.Edit to protocol.TextEdit:%v", err) - return protocol.CodeAction{}, false + return } - return protocol.CodeAction{ - Title: title, - Kind: settings.RefactorRewriteChangeQuote, - Edit: protocol.NewWorkspaceEdit(protocol.DocumentChangeEdit(fh, textedits)), - }, true + req.addEditAction(title, protocol.DocumentChangeEdit(req.fh, textedits)) } diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index b245b96eec6..6d481e7cb14 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -9,8 +9,11 @@ import ( "encoding/json" "fmt" "go/ast" + "go/token" "go/types" + "reflect" "slices" + "sort" "strings" "golang.org/x/tools/go/ast/astutil" @@ -23,7 +26,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typesinternal" @@ -38,7 +40,7 @@ import ( // See ../protocol/codeactionkind.go for some code action theory. func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool, trigger protocol.CodeActionTriggerKind) (actions []protocol.CodeAction, _ error) { - // code actions that require only a parse tree + loc := protocol.Location{URI: fh.URI(), Range: rng} pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { @@ -49,166 +51,250 @@ func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, return nil, err } - // add adds a code action to the result. - add := func(cmd *protocol.Command, kind protocol.CodeActionKind) { - action := newCodeAction(cmd.Title, kind, cmd, nil, snapshot.Options()) - actions = append(actions, action) + // Scan to see if any enabled producer needs type information. + var enabledMemo [len(codeActionProducers)]bool + needTypes := false + for i, p := range codeActionProducers { + if enabled(p.kind) { + enabledMemo[i] = true + if p.needPkg { + needTypes = true + } + } } - // Note: don't forget to update the allow-list in Server.CodeAction - // when adding new query operations like GoTest and GoDoc that - // are permitted even in generated source files. - - // Only compute quick fixes if there are any diagnostics to fix. - wantQuickFixes := len(diagnostics) > 0 && enabled(protocol.QuickFix) - if wantQuickFixes || enabled(protocol.SourceOrganizeImports) { - // Process any missing imports and pair them with the diagnostics they fix. - importEdits, importEditsPerFix, err := allImportsFixes(ctx, snapshot, pgf) + // Compute type information if needed. + // Also update pgf, start, end to be consistent with pkg. + // They may differ in case of parse cache miss. + var pkg *cache.Package + if needTypes { + var err error + pkg, pgf, err = NarrowestPackageForFile(ctx, snapshot, loc.URI) if err != nil { - event.Error(ctx, "imports fixes", err, label.File.Of(fh.URI().Path())) - importEdits = nil - importEditsPerFix = nil - } - - // Separate this into a set of codeActions per diagnostic, where - // each action is the addition, removal, or renaming of one import. - if wantQuickFixes { - for _, importFix := range importEditsPerFix { - fixed := fixedByImportFix(importFix.fix, diagnostics) - if len(fixed) == 0 { - continue - } - actions = append(actions, protocol.CodeAction{ - Title: importFixTitle(importFix.fix), - Kind: protocol.QuickFix, - Edit: protocol.NewWorkspaceEdit( - protocol.DocumentChangeEdit(fh, importFix.edits)), - Diagnostics: fixed, - }) - } + return nil, err } - - // Send all of the import edits as one code action if the file is - // being organized. - if len(importEdits) > 0 && enabled(protocol.SourceOrganizeImports) { - actions = append(actions, protocol.CodeAction{ - Title: "Organize Imports", - Kind: protocol.SourceOrganizeImports, - Edit: protocol.NewWorkspaceEdit( - protocol.DocumentChangeEdit(fh, importEdits)), - }) + start, end, err = pgf.RangePos(loc.Range) + if err != nil { + return nil, err } } - // refactor.extract.* - { - extractions, err := getExtractCodeActions(enabled, pgf, rng, snapshot.Options()) - if err != nil { + // Execute each enabled producer function. + req := &codeActionsRequest{ + actions: &actions, + lazy: make(map[reflect.Type]any), + snapshot: snapshot, + fh: fh, + pgf: pgf, + loc: loc, + start: start, + end: end, + diagnostics: diagnostics, + trigger: trigger, + pkg: pkg, + } + for i, p := range codeActionProducers { + if !enabledMemo[i] { + continue + } + req.kind = p.kind + if p.needPkg { + req.pkg = pkg + } else { + req.pkg = nil + } + if err := p.fn(ctx, req); err != nil { return nil, err } - actions = append(actions, extractions...) } - if kind := settings.GoFreeSymbols; enabled(kind) && rng.End != rng.Start { - loc := protocol.Location{URI: pgf.URI, Range: rng} - cmd := command.NewFreeSymbolsCommand("Browse free symbols", snapshot.View().ID(), loc) - // For implementation, see commandHandler.FreeSymbols. - add(cmd, kind) - } + sort.Slice(actions, func(i, j int) bool { + return actions[i].Kind < actions[j].Kind + }) - if kind := settings.GoplsDocFeatures; enabled(kind) { - // TODO(adonovan): after the docs are published in gopls/v0.17.0, - // use the gopls release tag instead of master. - cmd := command.NewClientOpenURLCommand( - "Browse gopls feature documentation", - "https://github.com/golang/tools/blob/master/gopls/doc/features/README.md") - add(cmd, kind) - } + return actions, nil +} - // code actions that require type information - // - // In order to keep the fast path (in particular, - // VS Code's request for just source.organizeImports - // immediately after a save) fast, avoid type checking - // if no enabled code actions need it. - // - // TODO(adonovan): design some kind of registration mechanism - // that avoids the need to keep this list up to date. - if !slices.ContainsFunc([]protocol.CodeActionKind{ - settings.RefactorRewriteRemoveUnusedParam, - settings.RefactorRewriteChangeQuote, - settings.RefactorRewriteInvertIf, - settings.RefactorRewriteSplitLines, - settings.RefactorRewriteJoinLines, - settings.RefactorRewriteFillStruct, - settings.RefactorRewriteFillSwitch, - settings.RefactorInlineCall, - settings.GoTest, - settings.GoDoc, - settings.GoAssembly, - }, enabled) { - return actions, nil - } - - // NB: update pgf, since it may not be a parse cache hit (esp. on 386). - // And update start, end, since they may have changed too. - // A framework would really make this cleaner. - pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) - if err != nil { - return nil, err - } - start, end, err = pgf.RangePos(rng) - if err != nil { - return nil, err - } +// A codeActionsRequest is passed to each function +// that produces code actions. +type codeActionsRequest struct { + // internal fields for use only by [CodeActions]. + actions *[]protocol.CodeAction // pointer to output slice; call addAction to populate + lazy map[reflect.Type]any // lazy construction + + // inputs to the producer function: + kind protocol.CodeActionKind + snapshot *cache.Snapshot + fh file.Handle + pgf *parsego.File + loc protocol.Location + start, end token.Pos + diagnostics []protocol.Diagnostic + trigger protocol.CodeActionTriggerKind + pkg *cache.Package // set only if producer.needPkg +} - // refactor.rewrite.* - { - rewrites, err := getRewriteCodeActions(enabled, ctx, pkg, snapshot, pgf, fh, rng, snapshot.Options()) +// addCommandAction adds a CodeAction to the result based on the provided command. +// +// If the client supports codeAction/resolve, then the command is embedded into +// the code action data field, and used to resolve edits later. Otherwise, the +// command is set as the code action operation. +func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command) *protocol.CodeAction { + act := protocol.CodeAction{ + Title: cmd.Title, + Kind: req.kind, + } + // TODO(adonovan): some commands (goFreeSymbols, goplsDoc, + // goDoc, goTest, and goAssembly) have side effects such as + // opening a browser, and so should not be eligible for lazy + // edit resolution. + if !req.resolveEdits() { + act.Command = cmd + } else { + data, err := json.Marshal(cmd) if err != nil { - return nil, err + panic("unable to marshal") } - actions = append(actions, rewrites...) + msg := json.RawMessage(data) + act.Data = &msg } + return req.addAction(act) +} - // To avoid distraction (e.g. VS Code lightbulb), offer "inline" - // only after a selection or explicit menu operation. - if trigger != protocol.CodeActionAutomatic || rng.Start != rng.End { - rewrites, err := getInlineCodeActions(enabled, pkg, pgf, rng, snapshot.Options()) - if err != nil { - return nil, err - } - actions = append(actions, rewrites...) +// addCommandAction adds an edit-based CodeAction to the result. +func (req *codeActionsRequest) addEditAction(title string, changes ...protocol.DocumentChange) *protocol.CodeAction { + return req.addAction(protocol.CodeAction{ + Title: title, + Kind: req.kind, + Edit: protocol.NewWorkspaceEdit(changes...), + }) +} + +// addAction adds a code action to the response. +// It returns an ephemeral pointer to the action in situ. +// which may be used (but only immediately) for further mutation. +func (req *codeActionsRequest) addAction(act protocol.CodeAction) *protocol.CodeAction { + *req.actions = append(*req.actions, act) + return &(*req.actions)[len(*req.actions)-1] +} + +// resolveEdits reports whether the client can resolve edits lazily. +func (req *codeActionsRequest) resolveEdits() bool { + opts := req.snapshot.Options() + return opts.CodeActionResolveOptions != nil && + slices.Contains(opts.CodeActionResolveOptions, "edit") +} + +// lazyInit[*T](ctx, req) returns a pointer to an instance of T, +// calling new(T).init(ctx.req) on the first request. +// +// It is conceptually a (generic) method of req. +func lazyInit[P interface { + init(ctx context.Context, req *codeActionsRequest) + *T +}, T any](ctx context.Context, req *codeActionsRequest) P { + t := reflect.TypeFor[T]() + v, ok := req.lazy[t].(P) + if !ok { + v = new(T) + v.init(ctx, req) + req.lazy[t] = v + } + return v +} + +// -- producers -- + +// A codeActionProducer describes a function that produces CodeActions +// of a particular kind. +// The function is only called if that kind is enabled. +type codeActionProducer struct { + kind protocol.CodeActionKind + fn func(ctx context.Context, req *codeActionsRequest) error + needPkg bool // fn needs type information (req.pkg) +} + +var codeActionProducers = [...]codeActionProducer{ + {kind: protocol.QuickFix, fn: quickFix}, + {kind: protocol.SourceOrganizeImports, fn: sourceOrganizeImports}, + {kind: settings.GoAssembly, fn: goAssembly, needPkg: true}, + {kind: settings.GoDoc, fn: goDoc, needPkg: true}, + {kind: settings.GoFreeSymbols, fn: goFreeSymbols}, + {kind: settings.GoTest, fn: goTest}, + {kind: settings.GoplsDocFeatures, fn: goplsDocFeatures}, + {kind: settings.RefactorExtractFunction, fn: refactorExtractFunction}, + {kind: settings.RefactorExtractMethod, fn: refactorExtractMethod}, + {kind: settings.RefactorExtractToNewFile, fn: refactorExtractToNewFile}, + {kind: settings.RefactorExtractVariable, fn: refactorExtractVariable}, + {kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true}, + {kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote}, + {kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true}, + {kind: settings.RefactorRewriteFillSwitch, fn: refactorRewriteFillSwitch, needPkg: true}, + {kind: settings.RefactorRewriteInvertIf, fn: refactorRewriteInvertIf}, + {kind: settings.RefactorRewriteJoinLines, fn: refactorRewriteJoinLines, needPkg: true}, + {kind: settings.RefactorRewriteRemoveUnusedParam, fn: refactorRewriteRemoveUnusedParam, needPkg: true}, + {kind: settings.RefactorRewriteSplitLines, fn: refactorRewriteSplitLines, needPkg: true}, + + // Note: don't forget to update the allow-list in Server.CodeAction + // when adding new query operations like GoTest and GoDoc that + // are permitted even in generated source files. +} + +// sourceOrganizeImports produces "Organize Imports" code actions. +func sourceOrganizeImports(ctx context.Context, req *codeActionsRequest) error { + res := lazyInit[*allImportsFixesResult](ctx, req) + + // Send all of the import edits as one code action + // if the file is being organized. + if len(res.allFixEdits) > 0 { + req.addEditAction("Organize Imports", protocol.DocumentChangeEdit(req.fh, res.allFixEdits)) } - if enabled(settings.GoTest) { - fixes, err := getGoTestCodeActions(pkg, pgf, rng) - if err != nil { - return nil, err - } - actions = append(actions, fixes...) + return nil +} + +// quickFix produces code actions that add/delete/rename imports to fix type errors. +func quickFix(ctx context.Context, req *codeActionsRequest) error { + // Only compute quick fixes if there are any diagnostics to fix. + if len(req.diagnostics) == 0 { + return nil } - if kind := settings.GoDoc; enabled(kind) { - // "Browse documentation for ..." - _, _, title := DocFragment(pkg, pgf, start, end) - loc := protocol.Location{URI: pgf.URI, Range: rng} - cmd := command.NewDocCommand(title, loc) - add(cmd, kind) + // Process any missing imports and pair them with the diagnostics they fix. + res := lazyInit[*allImportsFixesResult](ctx, req) + if res.err != nil { + return nil } - if enabled(settings.GoAssembly) { - fixes, err := getGoAssemblyAction(snapshot.View(), pkg, pgf, rng) - if err != nil { - return nil, err + // Separate this into a set of codeActions per diagnostic, where + // each action is the addition, removal, or renaming of one import. + for _, importFix := range res.editsPerFix { + fixed := fixedByImportFix(importFix.fix, req.diagnostics) + if len(fixed) == 0 { + continue } - actions = append(actions, fixes...) + act := req.addEditAction( + importFixTitle(importFix.fix), + protocol.DocumentChangeEdit(req.fh, importFix.edits)) + act.Diagnostics = fixed } - return actions, nil + + return nil +} + +// allImportsFixesResult is the result of a lazy call to allImportsFixes. +// It implements the codeActionsRequest lazyInit interface. +type allImportsFixesResult struct { + allFixEdits []protocol.TextEdit + editsPerFix []*importFix + err error } -func supportsResolveEdits(options *settings.Options) bool { - return options.CodeActionResolveOptions != nil && slices.Contains(options.CodeActionResolveOptions, "edit") +func (res *allImportsFixesResult) init(ctx context.Context, req *codeActionsRequest) { + res.allFixEdits, res.editsPerFix, res.err = allImportsFixes(ctx, req.snapshot, req.pgf) + if res.err != nil { + event.Error(ctx, "imports fixes", res.err, label.File.Of(req.loc.URI.Path())) + } } func importFixTitle(fix *imports.ImportFix) string { @@ -261,212 +347,193 @@ func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) return results } -// getExtractCodeActions returns any refactor.extract code actions for the selection. -func getExtractCodeActions(enabled func(protocol.CodeActionKind) bool, pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err +// goFreeSymbols produces "Browse free symbols" code actions. +// See [server.commandHandler.FreeSymbols] for command implementation. +func goFreeSymbols(ctx context.Context, req *codeActionsRequest) error { + if !req.loc.Empty() { + cmd := command.NewFreeSymbolsCommand("Browse free symbols", req.snapshot.View().ID(), req.loc) + req.addCommandAction(cmd) } + return nil +} - var actions []protocol.CodeAction - add := func(cmd *protocol.Command, kind protocol.CodeActionKind) { - action := newCodeAction(cmd.Title, kind, cmd, nil, options) - actions = append(actions, action) - } - - // extract function or method - if enabled(settings.RefactorExtractFunction) || enabled(settings.RefactorExtractMethod) { - if _, ok, methodOk, _ := canExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok { - // extract function - if kind := settings.RefactorExtractFunction; enabled(kind) { - cmd := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ - Fix: fixExtractFunction, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - add(cmd, kind) - } +// goplsDocFeatures produces "Browse gopls feature documentation" code actions. +// See [server.commandHandler.ClientOpenURL] for command implementation. +func goplsDocFeatures(ctx context.Context, req *codeActionsRequest) error { + // TODO(adonovan): after the docs are published in gopls/v0.17.0, + // use the gopls release tag instead of master. + cmd := command.NewClientOpenURLCommand( + "Browse gopls feature documentation", + "https://github.com/golang/tools/blob/master/gopls/doc/features/README.md") + req.addCommandAction(cmd) + return nil +} - // extract method - if kind := settings.RefactorExtractMethod; methodOk && enabled(kind) { - cmd := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ - Fix: fixExtractMethod, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - add(cmd, kind) - } - } - } +// goDoc produces "Browse documentation for X" code actions. +// See [server.commandHandler.Doc] for command implementation. +func goDoc(ctx context.Context, req *codeActionsRequest) error { + _, _, title := DocFragment(req.pkg, req.pgf, req.start, req.end) + cmd := command.NewDocCommand(title, req.loc) + req.addCommandAction(cmd) + return nil +} - // extract variable - if kind := settings.RefactorExtractVariable; enabled(kind) { - if _, _, ok, _ := canExtractVariable(start, end, pgf.File); ok { - cmd := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ - Fix: fixExtractVariable, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - add(cmd, kind) - } +// refactorExtractFunction produces "Extract function" code actions. +// See [extractFunction] for command implementation. +func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error { + if _, ok, _, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok { + cmd := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ + Fix: fixExtractFunction, + URI: req.loc.URI, + Range: req.loc.Range, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd) } + return nil +} - // extract to new file - if kind := settings.RefactorExtractToNewFile; enabled(kind) { - if canExtractToNewFile(pgf, start, end) { - cmd := command.NewExtractToNewFileCommand( - "Extract declarations to new file", - protocol.Location{URI: pgf.URI, Range: rng}, - ) - add(cmd, kind) - } +// refactorExtractMethod produces "Extract method" code actions. +// See [extractMethod] for command implementation. +func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error { + if _, ok, methodOK, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok && methodOK { + cmd := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ + Fix: fixExtractMethod, + URI: req.loc.URI, + Range: req.loc.Range, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd) } - - return actions, nil + return nil } -func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Command, diagnostics []protocol.Diagnostic, options *settings.Options) protocol.CodeAction { - action := protocol.CodeAction{ - Title: title, - Kind: kind, - Diagnostics: diagnostics, - } - if !supportsResolveEdits(options) { - action.Command = cmd - } else { - data, err := json.Marshal(cmd) - if err != nil { - panic("unable to marshal") - } - msg := json.RawMessage(data) - action.Data = &msg +// refactorExtractVariable produces "Extract variable" code actions. +// See [extractVariable] for command implementation. +func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error { + if _, _, ok, _ := canExtractVariable(req.start, req.end, req.pgf.File); ok { + cmd := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ + Fix: fixExtractVariable, + URI: req.loc.URI, + Range: req.loc.Range, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd) } - return action + return nil } -func getRewriteCodeActions(enabled func(protocol.CodeActionKind) bool, ctx context.Context, pkg *cache.Package, snapshot *cache.Snapshot, pgf *parsego.File, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { - // golang/go#61693: code actions were refactored to run outside of the - // analysis framework, but as a result they lost their panic recovery. - // - // These code actions should never fail, but put back the panic recovery as a - // defensive measure. - defer func() { - if r := recover(); r != nil { - rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r) - } - }() - - var actions []protocol.CodeAction - add := func(cmd *protocol.Command, kind protocol.CodeActionKind) { - action := newCodeAction(cmd.Title, kind, cmd, nil, options) - actions = append(actions, action) +// refactorExtractToNewFile produces "Extract declarations to new file" code actions. +// See [server.commandHandler.ExtractToNewFile] for command implementation. +func refactorExtractToNewFile(ctx context.Context, req *codeActionsRequest) error { + if canExtractToNewFile(req.pgf, req.start, req.end) { + cmd := command.NewExtractToNewFileCommand("Extract declarations to new file", req.loc) + req.addCommandAction(cmd) } + return nil +} - // remove unused param - if kind := settings.RefactorRewriteRemoveUnusedParam; enabled(kind) && canRemoveParameter(pkg, pgf, rng) { +// refactorRewriteRemoveUnusedParam produces "Remove unused parameter" code actions. +// See [server.commandHandler.ChangeSignature] for command implementation. +func refactorRewriteRemoveUnusedParam(ctx context.Context, req *codeActionsRequest) error { + if canRemoveParameter(req.pkg, req.pgf, req.loc.Range) { cmd := command.NewChangeSignatureCommand("Refactor: remove unused parameter", command.ChangeSignatureArgs{ - RemoveParameter: protocol.Location{ - URI: pgf.URI, - Range: rng, - }, - ResolveEdits: supportsResolveEdits(options), + RemoveParameter: req.loc, + ResolveEdits: req.resolveEdits(), }) - add(cmd, kind) - } - - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err + req.addCommandAction(cmd) } + return nil +} - // change quote - if enabled(settings.RefactorRewriteChangeQuote) { - if action, ok := convertStringLiteral(pgf, fh, start, end); ok { - actions = append(actions, action) - } - } +// refactorRewriteChangeQuote produces "Convert to {raw,interpreted} string literal" code actions. +func refactorRewriteChangeQuote(ctx context.Context, req *codeActionsRequest) error { + convertStringLiteral(req) + return nil +} - // invert if condition - if kind := settings.RefactorRewriteInvertIf; enabled(kind) { - if _, ok, _ := canInvertIfCondition(pgf.File, start, end); ok { - cmd := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{ - Fix: fixInvertIfCondition, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - add(cmd, kind) - } +// refactorRewriteChangeQuote produces "Invert 'if' condition" code actions. +// See [invertIfCondition] for command implementation. +func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error { + if _, ok, _ := canInvertIfCondition(req.pgf.File, req.start, req.end); ok { + cmd := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{ + Fix: fixInvertIfCondition, + URI: req.loc.URI, + Range: req.loc.Range, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd) } + return nil +} - // split lines - if kind := settings.RefactorRewriteSplitLines; enabled(kind) { - if msg, ok, _ := canSplitLines(pgf.File, pkg.FileSet(), start, end); ok { - cmd := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ - Fix: fixSplitLines, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - add(cmd, kind) - } +// refactorRewriteSplitLines produces "Split ITEMS into separate lines" code actions. +// See [splitLines] for command implementation. +func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) error { + // TODO(adonovan): opt: don't set needPkg just for FileSet. + if msg, ok, _ := canSplitLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok { + cmd := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ + Fix: fixSplitLines, + URI: req.loc.URI, + Range: req.loc.Range, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd) } + return nil +} - // join lines - if kind := settings.RefactorRewriteJoinLines; enabled(kind) { - if msg, ok, _ := canJoinLines(pgf.File, pkg.FileSet(), start, end); ok { - cmd := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ - Fix: fixJoinLines, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - add(cmd, kind) - } +// refactorRewriteJoinLines produces "Join ITEMS into one line" code actions. +// See [joinLines] for command implementation. +func refactorRewriteJoinLines(ctx context.Context, req *codeActionsRequest) error { + // TODO(adonovan): opt: don't set needPkg just for FileSet. + if msg, ok, _ := canJoinLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok { + cmd := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ + Fix: fixJoinLines, + URI: req.loc.URI, + Range: req.loc.Range, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd) } + return nil +} - // fill struct - // +// refactorRewriteFillStruct produces "Join ITEMS into one line" code actions. +// See [fillstruct.SuggestedFix] for command implementation. +func refactorRewriteFillStruct(ctx context.Context, req *codeActionsRequest) error { // fillstruct.Diagnose is a lazy analyzer: all it gives us is // the (start, end, message) of each SuggestedFix; the actual // edit is computed only later by ApplyFix, which calls fillstruct.SuggestedFix. - if kind := settings.RefactorRewriteFillStruct; enabled(kind) { - for _, diag := range fillstruct.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) { - rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End) - if err != nil { - return nil, err - } - for _, fix := range diag.SuggestedFixes { - cmd := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ - Fix: diag.Category, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - add(cmd, kind) - } + for _, diag := range fillstruct.Diagnose(req.pgf.File, req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) { + rng, err := req.pgf.Mapper.PosRange(req.pgf.Tok, diag.Pos, diag.End) + if err != nil { + return err + } + for _, fix := range diag.SuggestedFixes { + cmd := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ + Fix: diag.Category, + URI: req.loc.URI, + Range: rng, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd) } } + return nil +} - // fill switch - if kind := settings.RefactorRewriteFillSwitch; enabled(kind) { - for _, diag := range fillswitch.Diagnose(pgf.File, start, end, pkg.Types(), pkg.TypesInfo()) { - changes, err := suggestedFixToDocumentChange(ctx, snapshot, pkg.FileSet(), &diag.SuggestedFixes[0]) - if err != nil { - return nil, err - } - actions = append(actions, protocol.CodeAction{ - Title: diag.Message, - Kind: kind, - Edit: protocol.NewWorkspaceEdit(changes...), - }) +// refactorRewriteFillSwitch produces "Add cases for TYPE/ENUM" code actions. +func refactorRewriteFillSwitch(ctx context.Context, req *codeActionsRequest) error { + for _, diag := range fillswitch.Diagnose(req.pgf.File, req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) { + changes, err := suggestedFixToDocumentChange(ctx, req.snapshot, req.pkg.FileSet(), &diag.SuggestedFixes[0]) + if err != nil { + return err } + req.addEditAction(diag.Message, changes...) } - return actions, nil + return nil } // canRemoveParameter reports whether we can remove the function parameter @@ -479,6 +546,8 @@ func getRewriteCodeActions(enabled func(protocol.CodeActionKind) bool, ctx conte // // (Note that the unusedparam analyzer also computes this property, but // much more precisely, allowing it to report its findings as diagnostics.) +// +// TODO(adonovan): inline into refactorRewriteRemoveUnusedParam. func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) bool { if perrors, terrors := pkg.ParseErrors(), pkg.TypeErrors(); len(perrors) > 0 || len(terrors) > 0 { return false // can't remove parameters from packages with errors @@ -518,73 +587,61 @@ func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Rang return !used } -// getInlineCodeActions returns refactor.inline actions available at the specified range. -func getInlineCodeActions(enabled func(protocol.CodeActionKind) bool, pkg *cache.Package, pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { - var actions []protocol.CodeAction - add := func(cmd *protocol.Command, kind protocol.CodeActionKind) { - action := newCodeAction(cmd.Title, kind, cmd, nil, options) - actions = append(actions, action) +// refactorInlineCall produces "Inline call to FUNC" code actions. +// See [inlineCall] for command implementation. +func refactorInlineCall(ctx context.Context, req *codeActionsRequest) error { + // To avoid distraction (e.g. VS Code lightbulb), offer "inline" + // only after a selection or explicit menu operation. + if req.trigger == protocol.CodeActionAutomatic && req.loc.Empty() { + return nil } - // inline call - if kind := settings.RefactorInlineCall; enabled(kind) { - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err - } - - // If range is within call expression, offer to inline the call. - if _, fn, err := enclosingStaticCall(pkg, pgf, start, end); err == nil { - cmd := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ - Fix: fixInlineCall, - URI: pgf.URI, - Range: rng, - ResolveEdits: supportsResolveEdits(options), - }) - add(cmd, kind) - } + // If range is within call expression, offer to inline the call. + if _, fn, err := enclosingStaticCall(req.pkg, req.pgf, req.start, req.end); err == nil { + cmd := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ + Fix: fixInlineCall, + URI: req.loc.URI, + Range: req.loc.Range, + ResolveEdits: req.resolveEdits(), + }) + req.addCommandAction(cmd) } - - return actions, nil + return nil } -// getGoTestCodeActions returns any "run this test/benchmark" code actions for the selection. -func getGoTestCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) { - testFuncs, benchFuncs, err := testsAndBenchmarks(pkg.TypesInfo(), pgf) +// goTest produces "Run tests and benchmarks" code actions. +// See [server.commandHandler.runTests] for command implementation. +func goTest(ctx context.Context, req *codeActionsRequest) error { + testFuncs, benchFuncs, err := testsAndBenchmarks(req.pkg.TypesInfo(), req.pgf) if err != nil { - return nil, err + return err } var tests, benchmarks []string for _, fn := range testFuncs { - if protocol.Intersect(fn.rng, rng) { + if protocol.Intersect(fn.rng, req.loc.Range) { tests = append(tests, fn.name) } } for _, fn := range benchFuncs { - if protocol.Intersect(fn.rng, rng) { + if protocol.Intersect(fn.rng, req.loc.Range) { benchmarks = append(benchmarks, fn.name) } } if len(tests) == 0 && len(benchmarks) == 0 { - return nil, nil + return nil } - cmd := command.NewTestCommand("Run tests and benchmarks", pgf.URI, tests, benchmarks) - return []protocol.CodeAction{{ - Title: cmd.Title, - Kind: settings.GoTest, - Command: cmd, - }}, nil + cmd := command.NewTestCommand("Run tests and benchmarks", req.loc.URI, tests, benchmarks) + req.addCommandAction(cmd) + return nil } -// getGoAssemblyAction returns any "Browse assembly for f" code actions for the selection. -func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) { - start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err - } +// goAssembly produces "Browse ARCH assembly for FUNC" code actions. +// See [server.commandHandler.Assembly] for command implementation. +func goAssembly(ctx context.Context, req *codeActionsRequest) error { + view := req.snapshot.View() // Find the enclosing toplevel function or method, // and compute its symbol name (e.g. "pkgpath.(T).method"). @@ -606,11 +663,10 @@ func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File // directly to (say) a lambda of interest. // Perhaps we could scroll to STEXT for the innermost // enclosing nested function? - var actions []protocol.CodeAction - path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) + path, _ := astutil.PathEnclosingInterval(req.pgf.File, req.start, req.end) if len(path) >= 2 { // [... FuncDecl File] if decl, ok := path[len(path)-2].(*ast.FuncDecl); ok { - if fn, ok := pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok { + if fn, ok := req.pkg.TypesInfo().Defs[decl.Name].(*types.Func); ok { sig := fn.Signature() // Compute the linker symbol of the enclosing function. @@ -639,17 +695,12 @@ func getGoAssemblyAction(view *cache.View, pkg *cache.Package, pgf *parsego.File cmd := command.NewAssemblyCommand( fmt.Sprintf("Browse %s assembly for %s", view.GOARCH(), decl.Name), view.ID(), - string(pkg.Metadata().ID), + string(req.pkg.Metadata().ID), sym.String()) - // For handler, see commandHandler.Assembly. - actions = append(actions, protocol.CodeAction{ - Title: cmd.Title, - Kind: settings.GoAssembly, - Command: cmd, - }) + req.addCommandAction(cmd) } } } } - return actions, nil + return nil } diff --git a/gopls/internal/golang/invertifcondition.go b/gopls/internal/golang/invertifcondition.go index 16eaaa39bd2..0fb7d1e4d0a 100644 --- a/gopls/internal/golang/invertifcondition.go +++ b/gopls/internal/golang/invertifcondition.go @@ -240,7 +240,7 @@ func invertAndOr(fset *token.FileSet, expr *ast.BinaryExpr, src []byte) ([]byte, } // canInvertIfCondition reports whether we can do invert-if-condition on the -// code in the given range +// code in the given range. func canInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) { path, _ := astutil.PathEnclosingInterval(file, start, end) for _, node := range path { diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index 4ea9157d7c1..88b9dfefca4 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -323,6 +323,8 @@ type ApplyFixArgs struct { // upon by the code action and golang.ApplyFix. Fix string + // TODO(adonovan): replace URI + Range with Location + // The file URI for the document to fix. URI protocol.DocumentURI // The document range to scan for fixes. diff --git a/gopls/internal/protocol/span.go b/gopls/internal/protocol/span.go index 47d04df9d0e..213251b1adb 100644 --- a/gopls/internal/protocol/span.go +++ b/gopls/internal/protocol/span.go @@ -9,6 +9,12 @@ import ( "unicode/utf8" ) +// Empty reports whether the Range is an empty selection. +func (rng Range) Empty() bool { return rng.Start == rng.End } + +// Empty reports whether the Location is an empty selection. +func (loc Location) Empty() bool { return loc.Range.Empty() } + // CompareLocation defines a three-valued comparison over locations, // lexicographically ordered by (URI, Range). func CompareLocation(x, y Location) int { From 752860b84edb34a684ef51f11ac68c3848ab5c22 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 18 Sep 2024 16:34:39 -0400 Subject: [PATCH 057/102] gopls/internal/protocol/command: simplify ApplyFix - combine URI + Range into one Location field - factor n calls addCommand(NewApplyFixCommand). Change-Id: I01c4ff7efeaa577331253348f4816a3a82b80db0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/614157 Commit-Queue: Alan Donovan Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/errors.go | 5 +- gopls/internal/golang/codeaction.go | 112 ++++++------------- gopls/internal/protocol/command/interface.go | 7 +- gopls/internal/server/command.go | 4 +- 4 files changed, 43 insertions(+), 85 deletions(-) diff --git a/gopls/internal/cache/errors.go b/gopls/internal/cache/errors.go index 4423fb69e3e..26747a63d33 100644 --- a/gopls/internal/cache/errors.go +++ b/gopls/internal/cache/errors.go @@ -324,9 +324,8 @@ func toSourceDiagnostic(srcAnalyzer *settings.Analyzer, gobDiag *gobDiagnostic) // // The analysis.Diagnostic.Category is used as the fix name. cmd := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ - Fix: diag.Code, - URI: gobDiag.Location.URI, - Range: gobDiag.Location.Range, + Fix: diag.Code, + Location: gobDiag.Location, }) for _, kind := range kinds { fixes = append(fixes, SuggestedFixFromCommand(cmd, kind)) diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 6d481e7cb14..fb64a893d84 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -134,29 +134,39 @@ type codeActionsRequest struct { pkg *cache.Package // set only if producer.needPkg } +// addApplyFixAction adds an ApplyFix command-based CodeAction to the result. +func (req *codeActionsRequest) addApplyFixAction(title, fix string, loc protocol.Location) *protocol.CodeAction { + cmd := command.NewApplyFixCommand(title, command.ApplyFixArgs{ + Fix: fix, + Location: loc, + ResolveEdits: req.resolveEdits(), + }) + return req.addCommandAction(cmd, true) +} + // addCommandAction adds a CodeAction to the result based on the provided command. // -// If the client supports codeAction/resolve, then the command is embedded into -// the code action data field, and used to resolve edits later. Otherwise, the -// command is set as the code action operation. -func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command) *protocol.CodeAction { +// If allowResolveEdits (and the client supports codeAction/resolve) +// then the command is embedded into the code action data field so +// that the client can later ask the server to "resolve" a command +// into an edit that they can preview and apply selectively. +// Set allowResolveEdits only for actions that generate edits. +// +// Otherwise, the command is set as the code action operation. +func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command, allowResolveEdits bool) *protocol.CodeAction { act := protocol.CodeAction{ Title: cmd.Title, Kind: req.kind, } - // TODO(adonovan): some commands (goFreeSymbols, goplsDoc, - // goDoc, goTest, and goAssembly) have side effects such as - // opening a browser, and so should not be eligible for lazy - // edit resolution. - if !req.resolveEdits() { - act.Command = cmd - } else { + if allowResolveEdits && req.resolveEdits() { data, err := json.Marshal(cmd) if err != nil { panic("unable to marshal") } msg := json.RawMessage(data) act.Data = &msg + } else { + act.Command = cmd } return req.addAction(act) } @@ -352,7 +362,7 @@ func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) func goFreeSymbols(ctx context.Context, req *codeActionsRequest) error { if !req.loc.Empty() { cmd := command.NewFreeSymbolsCommand("Browse free symbols", req.snapshot.View().ID(), req.loc) - req.addCommandAction(cmd) + req.addCommandAction(cmd, false) } return nil } @@ -365,7 +375,7 @@ func goplsDocFeatures(ctx context.Context, req *codeActionsRequest) error { cmd := command.NewClientOpenURLCommand( "Browse gopls feature documentation", "https://github.com/golang/tools/blob/master/gopls/doc/features/README.md") - req.addCommandAction(cmd) + req.addCommandAction(cmd, false) return nil } @@ -374,7 +384,7 @@ func goplsDocFeatures(ctx context.Context, req *codeActionsRequest) error { func goDoc(ctx context.Context, req *codeActionsRequest) error { _, _, title := DocFragment(req.pkg, req.pgf, req.start, req.end) cmd := command.NewDocCommand(title, req.loc) - req.addCommandAction(cmd) + req.addCommandAction(cmd, false) return nil } @@ -382,13 +392,7 @@ func goDoc(ctx context.Context, req *codeActionsRequest) error { // See [extractFunction] for command implementation. func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error { if _, ok, _, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok { - cmd := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ - Fix: fixExtractFunction, - URI: req.loc.URI, - Range: req.loc.Range, - ResolveEdits: req.resolveEdits(), - }) - req.addCommandAction(cmd) + req.addApplyFixAction("Extract function", fixExtractFunction, req.loc) } return nil } @@ -397,13 +401,7 @@ func refactorExtractFunction(ctx context.Context, req *codeActionsRequest) error // See [extractMethod] for command implementation. func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error { if _, ok, methodOK, _ := canExtractFunction(req.pgf.Tok, req.start, req.end, req.pgf.Src, req.pgf.File); ok && methodOK { - cmd := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ - Fix: fixExtractMethod, - URI: req.loc.URI, - Range: req.loc.Range, - ResolveEdits: req.resolveEdits(), - }) - req.addCommandAction(cmd) + req.addApplyFixAction("Extract method", fixExtractMethod, req.loc) } return nil } @@ -412,13 +410,7 @@ func refactorExtractMethod(ctx context.Context, req *codeActionsRequest) error { // See [extractVariable] for command implementation. func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error { if _, _, ok, _ := canExtractVariable(req.start, req.end, req.pgf.File); ok { - cmd := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ - Fix: fixExtractVariable, - URI: req.loc.URI, - Range: req.loc.Range, - ResolveEdits: req.resolveEdits(), - }) - req.addCommandAction(cmd) + req.addApplyFixAction("Extract variable", fixExtractVariable, req.loc) } return nil } @@ -428,7 +420,7 @@ func refactorExtractVariable(ctx context.Context, req *codeActionsRequest) error func refactorExtractToNewFile(ctx context.Context, req *codeActionsRequest) error { if canExtractToNewFile(req.pgf, req.start, req.end) { cmd := command.NewExtractToNewFileCommand("Extract declarations to new file", req.loc) - req.addCommandAction(cmd) + req.addCommandAction(cmd, true) } return nil } @@ -441,7 +433,7 @@ func refactorRewriteRemoveUnusedParam(ctx context.Context, req *codeActionsReque RemoveParameter: req.loc, ResolveEdits: req.resolveEdits(), }) - req.addCommandAction(cmd) + req.addCommandAction(cmd, true) } return nil } @@ -456,13 +448,7 @@ func refactorRewriteChangeQuote(ctx context.Context, req *codeActionsRequest) er // See [invertIfCondition] for command implementation. func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error { if _, ok, _ := canInvertIfCondition(req.pgf.File, req.start, req.end); ok { - cmd := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{ - Fix: fixInvertIfCondition, - URI: req.loc.URI, - Range: req.loc.Range, - ResolveEdits: req.resolveEdits(), - }) - req.addCommandAction(cmd) + req.addApplyFixAction("Invert 'if' condition", fixInvertIfCondition, req.loc) } return nil } @@ -472,13 +458,7 @@ func refactorRewriteInvertIf(ctx context.Context, req *codeActionsRequest) error func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) error { // TODO(adonovan): opt: don't set needPkg just for FileSet. if msg, ok, _ := canSplitLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok { - cmd := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ - Fix: fixSplitLines, - URI: req.loc.URI, - Range: req.loc.Range, - ResolveEdits: req.resolveEdits(), - }) - req.addCommandAction(cmd) + req.addApplyFixAction(msg, fixSplitLines, req.loc) } return nil } @@ -488,13 +468,7 @@ func refactorRewriteSplitLines(ctx context.Context, req *codeActionsRequest) err func refactorRewriteJoinLines(ctx context.Context, req *codeActionsRequest) error { // TODO(adonovan): opt: don't set needPkg just for FileSet. if msg, ok, _ := canJoinLines(req.pgf.File, req.pkg.FileSet(), req.start, req.end); ok { - cmd := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ - Fix: fixJoinLines, - URI: req.loc.URI, - Range: req.loc.Range, - ResolveEdits: req.resolveEdits(), - }) - req.addCommandAction(cmd) + req.addApplyFixAction(msg, fixJoinLines, req.loc) } return nil } @@ -506,18 +480,12 @@ func refactorRewriteFillStruct(ctx context.Context, req *codeActionsRequest) err // the (start, end, message) of each SuggestedFix; the actual // edit is computed only later by ApplyFix, which calls fillstruct.SuggestedFix. for _, diag := range fillstruct.Diagnose(req.pgf.File, req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) { - rng, err := req.pgf.Mapper.PosRange(req.pgf.Tok, diag.Pos, diag.End) + loc, err := req.pgf.Mapper.PosLocation(req.pgf.Tok, diag.Pos, diag.End) if err != nil { return err } for _, fix := range diag.SuggestedFixes { - cmd := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ - Fix: diag.Category, - URI: req.loc.URI, - Range: rng, - ResolveEdits: req.resolveEdits(), - }) - req.addCommandAction(cmd) + req.addApplyFixAction(fix.Message, diag.Category, loc) } } return nil @@ -598,13 +566,7 @@ func refactorInlineCall(ctx context.Context, req *codeActionsRequest) error { // If range is within call expression, offer to inline the call. if _, fn, err := enclosingStaticCall(req.pkg, req.pgf, req.start, req.end); err == nil { - cmd := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ - Fix: fixInlineCall, - URI: req.loc.URI, - Range: req.loc.Range, - ResolveEdits: req.resolveEdits(), - }) - req.addCommandAction(cmd) + req.addApplyFixAction("Inline call to "+fn.Name(), fixInlineCall, req.loc) } return nil } @@ -634,7 +596,7 @@ func goTest(ctx context.Context, req *codeActionsRequest) error { } cmd := command.NewTestCommand("Run tests and benchmarks", req.loc.URI, tests, benchmarks) - req.addCommandAction(cmd) + req.addCommandAction(cmd, false) return nil } @@ -697,7 +659,7 @@ func goAssembly(ctx context.Context, req *codeActionsRequest) error { view.ID(), string(req.pkg.Metadata().ID), sym.String()) - req.addCommandAction(cmd) + req.addCommandAction(cmd, false) } } } diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index 88b9dfefca4..a2702f6c857 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -323,12 +323,9 @@ type ApplyFixArgs struct { // upon by the code action and golang.ApplyFix. Fix string - // TODO(adonovan): replace URI + Range with Location + // The portion of the document to fix. + Location protocol.Location - // The file URI for the document to fix. - URI protocol.DocumentURI - // The document range to scan for fixes. - Range protocol.Range // Whether to resolve and return the edits. ResolveEdits bool } diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 352008ac43b..709f0808d33 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -389,9 +389,9 @@ func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs var result *protocol.WorkspaceEdit err := c.run(ctx, commandConfig{ // Note: no progress here. Applying fixes should be quick. - forURI: args.URI, + forURI: args.Location.URI, }, func(ctx context.Context, deps commandDeps) error { - changes, err := golang.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Range) + changes, err := golang.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Location.Range) if err != nil { return err } From 31fdc78e7d731b4dd2a4938a7a1911030cbc98c4 Mon Sep 17 00:00:00 2001 From: Ethan Reesor Date: Sat, 21 Sep 2024 13:36:32 -0500 Subject: [PATCH 058/102] gopls/internal/server: allow suppression of showDocument When calling `gopls.doc`, allow suppressing the showDocument call that opens the documentation link in the browser. Include the documentation link in the result. Updates golang/go#55861 Change-Id: I18fe3bddfbed1ef12934cb6c833fa0cf78260d04 Reviewed-on: https://go-review.googlesource.com/c/tools/+/614300 Reviewed-by: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/codeaction.go | 2 +- .../internal/protocol/command/command_gen.go | 6 ++--- gopls/internal/protocol/command/interface.go | 7 +++++- gopls/internal/server/command.go | 22 +++++++++++++------ .../test/integration/misc/webserver_test.go | 2 +- 5 files changed, 26 insertions(+), 13 deletions(-) diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index fb64a893d84..f82abc6ce0d 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -383,7 +383,7 @@ func goplsDocFeatures(ctx context.Context, req *codeActionsRequest) error { // See [server.commandHandler.Doc] for command implementation. func goDoc(ctx context.Context, req *codeActionsRequest) error { _, _, title := DocFragment(req.pkg, req.pgf, req.start, req.end) - cmd := command.NewDocCommand(title, req.loc) + cmd := command.NewDocCommand(title, command.DocArgs{Location: req.loc, ShowDocument: true}) req.addCommandAction(cmd, false) return nil } diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index 73e793c087e..10c6c043a09 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go @@ -170,11 +170,11 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte } return nil, s.DiagnoseFiles(ctx, a0) case Doc: - var a0 protocol.Location + var a0 DocArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { return nil, err } - return nil, s.Doc(ctx, a0) + return s.Doc(ctx, a0) case EditGoDirective: var a0 EditGoDirectiveArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -420,7 +420,7 @@ func NewDiagnoseFilesCommand(title string, a0 DiagnoseFilesArgs) *protocol.Comma } } -func NewDocCommand(title string, a0 protocol.Location) *protocol.Command { +func NewDocCommand(title string, a0 DocArgs) *protocol.Command { return &protocol.Command{ Title: title, Command: Doc.String(), diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index a2702f6c857..98c5e6a061b 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -73,7 +73,7 @@ type Interface interface { // // Opens the Go package documentation page for the current // package in a browser. - Doc(context.Context, protocol.Location) error + Doc(context.Context, DocArgs) (protocol.URI, error) // RegenerateCgo: Regenerate cgo // @@ -310,6 +310,11 @@ type GenerateArgs struct { Recursive bool } +type DocArgs struct { + Location protocol.Location + ShowDocument bool // in addition to returning the URL, send showDocument +} + // TODO(rFindley): document the rest of these once the docgen is fleshed out. type ApplyFixArgs struct { diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 709f0808d33..4f6f24d869f 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -679,16 +679,21 @@ func (c *commandHandler) Test(ctx context.Context, uri protocol.DocumentURI, tes }) } -func (c *commandHandler) Doc(ctx context.Context, loc protocol.Location) error { - return c.run(ctx, commandConfig{ +func (c *commandHandler) Doc(ctx context.Context, args command.DocArgs) (protocol.URI, error) { + if args.Location.URI == "" { + return "", errors.New("missing location URI") + } + + var result protocol.URI + err := c.run(ctx, commandConfig{ progress: "", // the operation should be fast - forURI: loc.URI, + forURI: args.Location.URI, }, func(ctx context.Context, deps commandDeps) error { - pkg, pgf, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, loc.URI) + pkg, pgf, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, args.Location.URI) if err != nil { return err } - start, end, err := pgf.RangePos(loc.Range) + start, end, err := pgf.RangePos(args.Location.Range) if err != nil { return err } @@ -704,11 +709,14 @@ func (c *commandHandler) Doc(ctx context.Context, loc protocol.Location) error { pkgpath, fragment, _ := golang.DocFragment(pkg, pgf, start, end) // Direct the client to open the /pkg page. - url := web.PkgURL(deps.snapshot.View().ID(), pkgpath, fragment) - openClientBrowser(ctx, c.s.client, url) + result = web.PkgURL(deps.snapshot.View().ID(), pkgpath, fragment) + if args.ShowDocument { + openClientBrowser(ctx, c.s.client, result) + } return nil }) + return result, err } func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs) error { diff --git a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go index f8038f5721d..24518145721 100644 --- a/gopls/internal/test/integration/misc/webserver_test.go +++ b/gopls/internal/test/integration/misc/webserver_test.go @@ -285,7 +285,7 @@ func viewPkgDoc(t *testing.T, env *Env, loc protocol.Location) protocol.URI { Command: docAction.Command.Command, Arguments: docAction.Command.Arguments, } - var result command.DebuggingResult + var result any env.ExecuteCommand(params, &result) doc := shownDocument(t, env, "http:") From eb774f69dce5beb64849eaf1800fcb6b31af2fb4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sun, 22 Sep 2024 11:40:53 -0400 Subject: [PATCH 059/102] go/packages: fix LoadMode.String It had neglected some newer entries. Also, eliminate unnecessary tokens in the string. Also, document the Load* mode bit sets. They may be deprecated but they are useful, convenient, and widely used. Sometime remind me why they are deprecated. Updates golang/go#69555 Change-Id: Id0316cc1e9d39adf64e20db5194b867c3d19ab5d Reviewed-on: https://go-review.googlesource.com/c/tools/+/614875 Auto-Submit: Alan Donovan Commit-Queue: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Matloob --- go/packages/loadmode_string.go | 69 ++++++++++++++++------------------ go/packages/packages.go | 12 ++++++ go/packages/packages_test.go | 40 ++++++++++++-------- 3 files changed, 69 insertions(+), 52 deletions(-) diff --git a/go/packages/loadmode_string.go b/go/packages/loadmode_string.go index 5c080d21b54..5fcad6ea6db 100644 --- a/go/packages/loadmode_string.go +++ b/go/packages/loadmode_string.go @@ -9,49 +9,46 @@ import ( "strings" ) -var allModes = []LoadMode{ - NeedName, - NeedFiles, - NeedCompiledGoFiles, - NeedImports, - NeedDeps, - NeedExportFile, - NeedTypes, - NeedSyntax, - NeedTypesInfo, - NeedTypesSizes, +var modes = [...]struct { + mode LoadMode + name string +}{ + {NeedName, "NeedName"}, + {NeedFiles, "NeedFiles"}, + {NeedCompiledGoFiles, "NeedCompiledGoFiles"}, + {NeedImports, "NeedImports"}, + {NeedDeps, "NeedDeps"}, + {NeedExportFile, "NeedExportFile"}, + {NeedTypes, "NeedTypes"}, + {NeedSyntax, "NeedSyntax"}, + {NeedTypesInfo, "NeedTypesInfo"}, + {NeedTypesSizes, "NeedTypesSizes"}, + {NeedModule, "NeedModule"}, + {NeedEmbedFiles, "NeedEmbedFiles"}, + {NeedEmbedPatterns, "NeedEmbedPatterns"}, } -var modeStrings = []string{ - "NeedName", - "NeedFiles", - "NeedCompiledGoFiles", - "NeedImports", - "NeedDeps", - "NeedExportFile", - "NeedTypes", - "NeedSyntax", - "NeedTypesInfo", - "NeedTypesSizes", -} - -func (mod LoadMode) String() string { - m := mod - if m == 0 { +func (mode LoadMode) String() string { + if mode == 0 { return "LoadMode(0)" } var out []string - for i, x := range allModes { - if x > m { - break + // named bits + for _, item := range modes { + if (mode & item.mode) != 0 { + mode ^= item.mode + out = append(out, item.name) } - if (m & x) != 0 { - out = append(out, modeStrings[i]) - m = m ^ x + } + // unnamed residue + if mode != 0 { + if out == nil { + return fmt.Sprintf("LoadMode(%#x)", int(mode)) } + out = append(out, fmt.Sprintf("%#x", int(mode))) } - if m != 0 { - out = append(out, "Unknown") + if len(out) == 1 { + return out[0] } - return fmt.Sprintf("LoadMode(%s)", strings.Join(out, "|")) + return "(" + strings.Join(out, "|") + ")" } diff --git a/go/packages/packages.go b/go/packages/packages.go index 0b6bfaff808..69e11eced4a 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -103,25 +103,37 @@ const ( // NeedEmbedPatterns adds EmbedPatterns. NeedEmbedPatterns + + // Be sure to update loadmode_string.go when adding new items! ) const ( + // LoadFiles loads the name and file names for the initial packages. + // // Deprecated: LoadFiles exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadFiles = NeedName | NeedFiles | NeedCompiledGoFiles + // LoadImports loads the name, file names, and import mapping for the initial packages. + // // Deprecated: LoadImports exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadImports = LoadFiles | NeedImports + // LoadTypes loads exported type information for the initial packages. + // // Deprecated: LoadTypes exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadTypes = LoadImports | NeedTypes | NeedTypesSizes + // LoadSyntax loads typed syntax for the initial packages. + // // Deprecated: LoadSyntax exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadSyntax = LoadTypes | NeedSyntax | NeedTypesInfo + // LoadAllSyntax loads typed syntax for the initial packages and all dependencies. + // // Deprecated: LoadAllSyntax exists for historical compatibility // and should not be used. Please directly specify the needed fields using the Need values. LoadAllSyntax = LoadSyntax | NeedDeps diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 26dbc13df31..e78d3cdb881 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -2280,59 +2280,67 @@ func TestLoadModeStrings(t *testing.T) { }, { packages.NeedName, - "LoadMode(NeedName)", + "NeedName", }, { packages.NeedFiles, - "LoadMode(NeedFiles)", + "NeedFiles", }, { packages.NeedCompiledGoFiles, - "LoadMode(NeedCompiledGoFiles)", + "NeedCompiledGoFiles", }, { packages.NeedImports, - "LoadMode(NeedImports)", + "NeedImports", }, { packages.NeedDeps, - "LoadMode(NeedDeps)", + "NeedDeps", }, { packages.NeedExportFile, - "LoadMode(NeedExportFile)", + "NeedExportFile", }, { packages.NeedTypes, - "LoadMode(NeedTypes)", + "NeedTypes", }, { packages.NeedSyntax, - "LoadMode(NeedSyntax)", + "NeedSyntax", }, { packages.NeedTypesInfo, - "LoadMode(NeedTypesInfo)", + "NeedTypesInfo", }, { packages.NeedTypesSizes, - "LoadMode(NeedTypesSizes)", + "NeedTypesSizes", }, { packages.NeedName | packages.NeedExportFile, - "LoadMode(NeedName|NeedExportFile)", + "(NeedName|NeedExportFile)", }, { packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedDeps | packages.NeedExportFile | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypesSizes, - "LoadMode(NeedName|NeedFiles|NeedCompiledGoFiles|NeedImports|NeedDeps|NeedExportFile|NeedTypes|NeedSyntax|NeedTypesInfo|NeedTypesSizes)", + "(NeedName|NeedFiles|NeedCompiledGoFiles|NeedImports|NeedDeps|NeedExportFile|NeedTypes|NeedSyntax|NeedTypesInfo|NeedTypesSizes)", }, { - packages.NeedName | 8192, - "LoadMode(NeedName|Unknown)", + packages.NeedName | packages.NeedModule, + "(NeedName|NeedModule)", }, { - 4096, - "LoadMode(Unknown)", + packages.NeedName | 0x10000, // off the end (future use) + "(NeedName|0x10000)", + }, + { + packages.NeedName | 0x400, // needInternalDepsErrors + "(NeedName|0x400)", + }, + { + 0x1000, + "LoadMode(0x1000)", }, } From d911e4a8841ec46f0daeb627659620228be57319 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 24 Sep 2024 18:23:23 -0400 Subject: [PATCH 060/102] gopls: disable ast.Object resolution wherever possible And where not, document why. (Locations were found by instrumenting parser.ParseFile and running gopls' tests.) Change-Id: Iab205b1f96b4fb4b22a5d056b40fbbb326dcd7a4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/615436 Auto-Submit: Alan Donovan TryBot-Bypass: Alan Donovan Reviewed-by: Robert Findley Commit-Queue: Alan Donovan --- go/expect/extract.go | 2 +- go/packages/packages.go | 1 + gopls/internal/cache/parsego/parse.go | 2 +- gopls/internal/cache/snapshot.go | 3 ++- gopls/internal/golang/completion/completion.go | 2 +- gopls/internal/golang/extract.go | 2 +- gopls/internal/golang/freesymbols_test.go | 2 +- gopls/internal/golang/identifier_test.go | 2 +- gopls/internal/golang/stub.go | 2 +- gopls/internal/util/astutil/purge_test.go | 4 ++-- gopls/internal/util/safetoken/safetoken_test.go | 4 ++-- internal/aliases/aliases_go122.go | 2 +- internal/imports/fix.go | 3 ++- internal/imports/imports.go | 4 ++-- internal/imports/mkindex.go | 1 + 15 files changed, 20 insertions(+), 16 deletions(-) diff --git a/go/expect/extract.go b/go/expect/extract.go index c571c5ba4e9..1ca67d24958 100644 --- a/go/expect/extract.go +++ b/go/expect/extract.go @@ -42,7 +42,7 @@ func Parse(fset *token.FileSet, filename string, content []byte) ([]*Note, error // there are ways you can break the parser such that it will not add all the // comments to the ast, which may result in files where the tests are silently // not run. - file, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors) + file, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors|parser.SkipObjectResolution) if file == nil { return nil, err } diff --git a/go/packages/packages.go b/go/packages/packages.go index 69e11eced4a..0a2d40636dd 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -775,6 +775,7 @@ func newLoader(cfg *Config) *loader { // because we load source if export data is missing. if ld.ParseFile == nil { ld.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) { + // We implicitly promise to keep doing ast.Object resolution. :( const mode = parser.AllErrors | parser.ParseComments return parser.ParseFile(fset, filename, src, mode) } diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index 0143a36ab71..82f3eeebeec 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -824,7 +824,7 @@ func parseStmt(tok *token.File, pos token.Pos, src []byte) (ast.Stmt, error) { // Use ParseFile instead of ParseExpr because ParseFile has // best-effort behavior, whereas ParseExpr fails hard on any error. - fakeFile, err := parser.ParseFile(token.NewFileSet(), "", fileSrc, 0) + fakeFile, err := parser.ParseFile(token.NewFileSet(), "", fileSrc, parser.SkipObjectResolution) if fakeFile == nil { return nil, fmt.Errorf("error reading fake file source: %v", err) } diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 95f222c0eec..a52c9689c1d 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -2270,7 +2270,8 @@ func extractMagicComments(f *ast.File) []string { return results } -// BuiltinFile returns information about the special builtin package. +// BuiltinFile returns the pseudo-source file builtins.go, +// parsed with legacy ast.Object resolution. func (s *Snapshot) BuiltinFile(ctx context.Context) (*parsego.File, error) { s.AwaitInitialized(ctx) diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 5ffd4882e63..ca9ecba5a0d 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -3356,7 +3356,7 @@ func isSlice(obj types.Object) bool { // The AST position information is garbage. func forEachPackageMember(content []byte, f func(tok token.Token, id *ast.Ident, fn *ast.FuncDecl)) { purged := goplsastutil.PurgeFuncBodies(content) - file, _ := parser.ParseFile(token.NewFileSet(), "", purged, 0) + file, _ := parser.ParseFile(token.NewFileSet(), "", purged, parser.SkipObjectResolution) for _, decl := range file.Decls { switch decl := decl.(type) { case *ast.GenDecl: diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go index 3610deeead3..d1092d85409 100644 --- a/gopls/internal/golang/extract.go +++ b/gopls/internal/golang/extract.go @@ -1161,7 +1161,7 @@ func varOverridden(info *types.Info, firstUse *ast.Ident, obj types.Object, isFr // file that represents the text. func parseBlockStmt(fset *token.FileSet, src []byte) (*ast.BlockStmt, error) { text := "package main\nfunc _() { " + string(src) + " }" - extract, err := parser.ParseFile(fset, "", text, 0) + extract, err := parser.ParseFile(fset, "", text, parser.SkipObjectResolution) if err != nil { return nil, err } diff --git a/gopls/internal/golang/freesymbols_test.go b/gopls/internal/golang/freesymbols_test.go index 9ad8ca3ee6e..8885c32dbbc 100644 --- a/gopls/internal/golang/freesymbols_test.go +++ b/gopls/internal/golang/freesymbols_test.go @@ -98,7 +98,7 @@ func TestFreeRefs(t *testing.T) { test.src[startOffset+len("«"):endOffset] + " " + test.src[endOffset+len("»"):] - f, err := parser.ParseFile(fset, name, src, 0) + f, err := parser.ParseFile(fset, name, src, parser.SkipObjectResolution) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/golang/identifier_test.go b/gopls/internal/golang/identifier_test.go index b1e6d5a75a2..d78d8fe99f5 100644 --- a/gopls/internal/golang/identifier_test.go +++ b/gopls/internal/golang/identifier_test.go @@ -46,7 +46,7 @@ func TestSearchForEnclosing(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { fset := token.NewFileSet() - file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors) + file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors|parser.SkipObjectResolution) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go index db405631c9e..84299171fe4 100644 --- a/gopls/internal/golang/stub.go +++ b/gopls/internal/golang/stub.go @@ -281,7 +281,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache. // Re-parse the file. fset := token.NewFileSet() - newF, err := parser.ParseFile(fset, declPGF.URI.Path(), buf.Bytes(), parser.ParseComments) + newF, err := parser.ParseFile(fset, declPGF.URI.Path(), buf.Bytes(), parser.ParseComments|parser.SkipObjectResolution) if err != nil { return nil, nil, fmt.Errorf("could not reparse file: %w", err) } diff --git a/gopls/internal/util/astutil/purge_test.go b/gopls/internal/util/astutil/purge_test.go index c67f9039adc..757dd10a11b 100644 --- a/gopls/internal/util/astutil/purge_test.go +++ b/gopls/internal/util/astutil/purge_test.go @@ -50,7 +50,7 @@ func TestPurgeFuncBodies(t *testing.T) { fset := token.NewFileSet() // Parse then purge (reference implementation). - f1, _ := parser.ParseFile(fset, filename, content, 0) + f1, _ := parser.ParseFile(fset, filename, content, parser.SkipObjectResolution) ast.Inspect(f1, func(n ast.Node) bool { switch n := n.(type) { case *ast.FuncDecl: @@ -66,7 +66,7 @@ func TestPurgeFuncBodies(t *testing.T) { }) // Purge before parse (logic under test). - f2, _ := parser.ParseFile(fset, filename, astutil.PurgeFuncBodies(content), 0) + f2, _ := parser.ParseFile(fset, filename, astutil.PurgeFuncBodies(content), parser.SkipObjectResolution) // Compare sequence of node types. nodes1 := preorder(f1) diff --git a/gopls/internal/util/safetoken/safetoken_test.go b/gopls/internal/util/safetoken/safetoken_test.go index 4cdce7a97b9..ac3b878c6c4 100644 --- a/gopls/internal/util/safetoken/safetoken_test.go +++ b/gopls/internal/util/safetoken/safetoken_test.go @@ -23,11 +23,11 @@ func TestWorkaroundIssue57490(t *testing.T) { // syntax nodes, computed as Rbrace+len("}"), to be beyond EOF. src := `package p; func f() { var x struct` fset := token.NewFileSet() - file, _ := parser.ParseFile(fset, "a.go", src, 0) + file, _ := parser.ParseFile(fset, "a.go", src, parser.SkipObjectResolution) tf := fset.File(file.Pos()) // Add another file to the FileSet. - file2, _ := parser.ParseFile(fset, "b.go", "package q", 0) + file2, _ := parser.ParseFile(fset, "b.go", "package q", parser.SkipObjectResolution) // This is the ambiguity of #57490... if file.End() != file2.Pos() { diff --git a/internal/aliases/aliases_go122.go b/internal/aliases/aliases_go122.go index 18cf90c023f..7716a3331db 100644 --- a/internal/aliases/aliases_go122.go +++ b/internal/aliases/aliases_go122.go @@ -73,7 +73,7 @@ func Enabled() bool { // many tests. Therefore any attempt to cache the result // is just incorrect. fset := token.NewFileSet() - f, _ := parser.ParseFile(fset, "a.go", "package p; type A = int", 0) + f, _ := parser.ParseFile(fset, "a.go", "package p; type A = int", parser.SkipObjectResolution) pkg, _ := new(types.Config).Check("p", fset, []*ast.File{f}, nil) _, enabled := pkg.Scope().Lookup("A").Type().(*types.Alias) return enabled diff --git a/internal/imports/fix.go b/internal/imports/fix.go index dc7d50a7a40..c15108178ab 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go @@ -131,7 +131,7 @@ func parseOtherFiles(ctx context.Context, fset *token.FileSet, srcDir, filename continue } - f, err := parser.ParseFile(fset, filepath.Join(srcDir, fi.Name()), nil, 0) + f, err := parser.ParseFile(fset, filepath.Join(srcDir, fi.Name()), nil, parser.SkipObjectResolution) if err != nil { continue } @@ -1620,6 +1620,7 @@ func loadExportsFromFiles(ctx context.Context, env *ProcessEnv, dir string, incl } fullFile := filepath.Join(dir, fi.Name()) + // Legacy ast.Object resolution is needed here. f, err := parser.ParseFile(fset, fullFile, nil, 0) if err != nil { env.logf("error parsing %v: %v", fullFile, err) diff --git a/internal/imports/imports.go b/internal/imports/imports.go index f83465520a4..ff6b59a58a0 100644 --- a/internal/imports/imports.go +++ b/internal/imports/imports.go @@ -86,7 +86,7 @@ func ApplyFixes(fixes []*ImportFix, filename string, src []byte, opt *Options, e // Don't use parse() -- we don't care about fragments or statement lists // here, and we need to work with unparseable files. fileSet := token.NewFileSet() - parserMode := parser.Mode(0) + parserMode := parser.SkipObjectResolution if opt.Comments { parserMode |= parser.ParseComments } @@ -165,7 +165,7 @@ func formatFile(fset *token.FileSet, file *ast.File, src []byte, adjust func(ori // parse parses src, which was read from filename, // as a Go source file or statement list. func parse(fset *token.FileSet, filename string, src []byte, opt *Options) (*ast.File, func(orig, src []byte) []byte, error) { - parserMode := parser.Mode(0) + var parserMode parser.Mode // legacy ast.Object resolution is required here if opt.Comments { parserMode |= parser.ParseComments } diff --git a/internal/imports/mkindex.go b/internal/imports/mkindex.go index 2ecc9e45e9f..ff006b0cd2e 100644 --- a/internal/imports/mkindex.go +++ b/internal/imports/mkindex.go @@ -158,6 +158,7 @@ func loadExports(dir string) map[string]bool { return nil } for _, file := range buildPkg.GoFiles { + // Legacy ast.Object resolution is needed here. f, err := parser.ParseFile(fset, filepath.Join(dir, file), nil, 0) if err != nil { log.Printf("could not parse %q: %v", file, err) From 34638fc6a41e9e7ec6ee99535ecb24a570b8f222 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sun, 22 Sep 2024 12:25:27 -0400 Subject: [PATCH 061/102] cmd/{callgraph,ssadump}, gopackages: make -tags flags work Previously, the -tags flag used the obsolete buildutil.TagsFlag, which only affects the behavior of the obsolete go/loader. This change causes the -tags flag to affect go/packages in these three tools, as it should. Updates golang/go#69556 Updates golang/go#69538 Change-Id: Ie45c51c7fe04863dba00bc2f81b0a78f1c9bd0e3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/614895 TryBot-Bypass: Alan Donovan Reviewed-by: Michael Matloob Auto-Submit: Alan Donovan Commit-Queue: Alan Donovan --- cmd/callgraph/main.go | 15 ++++++--------- cmd/ssadump/main.go | 9 +++++---- go/packages/gopackages/main.go | 3 ++- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/cmd/callgraph/main.go b/cmd/callgraph/main.go index 4443f172f7e..9e440bbafb9 100644 --- a/cmd/callgraph/main.go +++ b/cmd/callgraph/main.go @@ -23,14 +23,12 @@ import ( "bytes" "flag" "fmt" - "go/build" "go/token" "io" "os" "runtime" "text/template" - "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/cha" "golang.org/x/tools/go/callgraph/rta" @@ -52,11 +50,9 @@ var ( formatFlag = flag.String("format", "{{.Caller}}\t--{{.Dynamic}}-{{.Line}}:{{.Column}}-->\t{{.Callee}}", "A template expression specifying how to format an edge") -) -func init() { - flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) -} + tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)") +) const Usage = `callgraph: display the call graph of a Go program. @@ -177,9 +173,10 @@ func doCallgraph(dir, gopath, algo, format string, tests bool, args []string) er } cfg := &packages.Config{ - Mode: packages.LoadAllSyntax, - Tests: tests, - Dir: dir, + Mode: packages.LoadAllSyntax, + BuildFlags: []string{"-tags=" + *tagsFlag}, + Tests: tests, + Dir: dir, } if gopath != "" { cfg.Env = append(os.Environ(), "GOPATH="+gopath) // to enable testing diff --git a/cmd/ssadump/main.go b/cmd/ssadump/main.go index cfb9122b24d..275e0a92aef 100644 --- a/cmd/ssadump/main.go +++ b/cmd/ssadump/main.go @@ -14,7 +14,6 @@ import ( "runtime" "runtime/pprof" - "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/interp" @@ -38,11 +37,12 @@ T [T]race execution of the program. Best for single-threaded programs! cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") args stringListValue + + tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)") ) func init() { flag.Var(&mode, "build", ssa.BuilderModeDoc) - flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) flag.Var(&args, "arg", "add argument to interpreted program") } @@ -76,8 +76,9 @@ func doMain() error { } cfg := &packages.Config{ - Mode: packages.LoadSyntax, - Tests: *testFlag, + BuildFlags: []string{"-tags=" + *tagsFlag}, + Mode: packages.LoadSyntax, + Tests: *testFlag, } // Choose types.Sizes from conf.Build. diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go index 9a0e7ad92c2..aab3362dbfd 100644 --- a/go/packages/gopackages/main.go +++ b/go/packages/gopackages/main.go @@ -37,6 +37,7 @@ type application struct { Deps bool `flag:"deps" help:"show dependencies too"` Test bool `flag:"test" help:"include any tests implied by the patterns"` Mode string `flag:"mode" help:"mode (one of files, imports, types, syntax, allsyntax)"` + Tags string `flag:"tags" help:"comma-separated list of extra build tags (see: go help buildconstraint)"` Private bool `flag:"private" help:"show non-exported declarations too (if -mode=syntax)"` PrintJSON bool `flag:"json" help:"print package in JSON form"` BuildFlags stringListValue `flag:"buildflag" help:"pass argument to underlying build system (may be repeated)"` @@ -95,7 +96,7 @@ func (app *application) Run(ctx context.Context, args ...string) error { cfg := &packages.Config{ Mode: packages.LoadSyntax, Tests: app.Test, - BuildFlags: app.BuildFlags, + BuildFlags: append([]string{"-tags=" + app.Tags}, app.BuildFlags...), Env: env, } From 83326b7c9c92f8b3e29c04902f606738084848ba Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 26 Aug 2024 19:59:50 +0000 Subject: [PATCH 062/102] gopls/internal/cache: join concurrent package batch operations One of the unsolved problems of the gopls scalability redesign described at https://go.dev/blog/gopls-scalability is that concurrent type checking operations may perform redundant work. While we avoided redundancy within the context of *one* operation, by encapsulating the type checking batch, it is possible that there will be logically distinct operations occurring concurrently, such as computing diagnostics and autocompletion. These operations could not share type information, as they were operating within distinct type checking "realms" (recall that go/types relies on the canonicalization of imports). This CL addresses that problem by refactoring the type checking batch such that it can join two distinct operations into a shared state. The typeCheckBatch becomes a queryable entity, coordinating work across ongoing package graph traversals. This required surprisingly little change to the typeCheckBatch itself, which already abstracted the notion of a package traversal. Rather, the key missing component was a future cache implementation that was (1) retryable and (2) transient, in the sense that computed values were discarded after use. The bulk of this change is the implementation and testing of such a cache. Some elements of the refactoring remain awkward: - packageHandles are collected and merged into a shared map, because they must still be computed in large batches for efficiency. - the shared importGraph remains a clumsy optimization, requiring subtle logic to pre-build a shared import graph. My intuition is that both of these problems have an elegant solution, but this work is left for a subsequent CL. In addition to reducing CPU across the board, due to less redundancy in the existing diagnostic pass, this CL will facilitate the following additions to gopls: - Pull-based diagnostics (golang/go#53275): now that diagnostic operations are naturally joined, there is less need for gopls to manage the scheduling of the diagnostic pass. - Improvements to loading behavior (golang/go#68002): being able to handle requests asynchronously allows completion requests to potentially be handled using stale metadata. Updates golang/go#53275 Updates golang/go#68002 Change-Id: Ib228cdcce2c4b6f616d6ba5b0abeb40e87f449be Reviewed-on: https://go-review.googlesource.com/c/tools/+/611843 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/check.go | 387 ++++++++++++++------------- gopls/internal/cache/future.go | 136 ++++++++++ gopls/internal/cache/future_test.go | 156 +++++++++++ gopls/internal/cache/snapshot.go | 8 + gopls/internal/server/diagnostics.go | 17 -- 5 files changed, 504 insertions(+), 200 deletions(-) create mode 100644 gopls/internal/cache/future.go create mode 100644 gopls/internal/cache/future_test.go diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index 4923c92db8d..91398323740 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -50,22 +50,49 @@ const ( type unit = struct{} // A typeCheckBatch holds data for a logical type-checking operation, which may -// type-check many unrelated packages. +// type check many unrelated packages. // // It shares state such as parsed files and imports, to optimize type-checking // for packages with overlapping dependency graphs. type typeCheckBatch struct { - syntaxIndex map[PackageID]int // requested ID -> index in ids - pre preTypeCheck - post postTypeCheck - handles map[PackageID]*packageHandle // (immutable) - parseCache *parseCache - fset *token.FileSet // describes all parsed or imported files - cpulimit chan unit // concurrency limiter for CPU-bound operations - - mu sync.Mutex - syntaxPackages map[PackageID]*futurePackage // results of processing a requested package; may hold (nil, nil) - importPackages map[PackageID]*futurePackage // package results to use for importing + // handleMu guards _handles, which must only be accessed via addHandles or + // getHandle. + // + // TODO(rfindley): refactor such that we can simply prepare the type checking + // pass by ensuring that handles are present on the Snapshot, and access them + // directly, rather than copying maps for each caller. + handleMu sync.Mutex + _handles map[PackageID]*packageHandle + + parseCache *parseCache + fset *token.FileSet // describes all parsed or imported files + cpulimit chan unit // concurrency limiter for CPU-bound operations + syntaxPackages *futureCache[PackageID, *Package] // transient cache of in-progress syntax futures + importPackages *futureCache[PackageID, *types.Package] // persistent cache of imports +} + +// addHandles is called by each goroutine joining the type check batch, to +// ensure that the batch has all inputs necessary for type checking. +func (b *typeCheckBatch) addHandles(handles map[PackageID]*packageHandle) { + b.handleMu.Lock() + defer b.handleMu.Unlock() + for id, ph := range handles { + if alt, ok := b._handles[id]; ok { + // Once handles have been reevaluated, they should not change. Therefore, + // we should only ever encounter exactly one handle instance for a given + // ID. + assert(alt == ph, "mismatching handle") + } else { + b._handles[id] = ph + } + } +} + +// getHandle retrieves the packageHandle for the given id. +func (b *typeCheckBatch) getHandle(id PackageID) *packageHandle { + b.handleMu.Lock() + defer b.handleMu.Unlock() + return b._handles[id] } // A futurePackage is a future result of type checking or importing a package, @@ -195,6 +222,10 @@ func (s *Snapshot) getImportGraph(ctx context.Context) *importGraph { // import graph. // // resolveImportGraph should only be called from getImportGraph. +// +// TODO(rfindley): resolveImportGraph can be eliminated (greatly simplifying +// things) by instead holding on to imports of open packages after each type +// checking pass. func (s *Snapshot) resolveImportGraph() (*importGraph, error) { ctx := s.backgroundCtx ctx, done := event.Start(event.Detach(ctx), "cache.resolveImportGraph") @@ -202,6 +233,7 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { s.mu.Lock() lastImportGraph := s.importGraph + g := s.meta s.mu.Unlock() openPackages := make(map[PackageID]bool) @@ -213,7 +245,6 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { // In the past, a call to MetadataForFile here caused a bunch of // unnecessary loads in multi-root workspaces (and as a result, spurious // diagnostics). - g := s.MetadataGraph() var mps []*metadata.Package for _, id := range g.IDs[fh.URI()] { mps = append(mps, g.Packages[id]) @@ -229,11 +260,6 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { openPackageIDs = append(openPackageIDs, id) } - handles, err := s.getPackageHandles(ctx, openPackageIDs) - if err != nil { - return nil, err - } - // Subtlety: we erase the upward cone of open packages from the shared import // graph, to increase reusability. // @@ -249,43 +275,48 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { // reachability. // // TODO(rfindley): this logic could use a unit test. - volatileDeps := make(map[PackageID]bool) - var isVolatile func(*packageHandle) bool - isVolatile = func(ph *packageHandle) (volatile bool) { - if v, ok := volatileDeps[ph.mp.ID]; ok { - return v - } - defer func() { - volatileDeps[ph.mp.ID] = volatile - }() - if openPackages[ph.mp.ID] { - return true - } - for _, dep := range ph.mp.DepsByPkgPath { - if isVolatile(handles[dep]) { - return true + volatile := make(map[PackageID]bool) + var isVolatile func(PackageID) bool + isVolatile = func(id PackageID) (v bool) { + v, ok := volatile[id] + if !ok { + volatile[id] = false // defensive: break cycles + for _, dep := range g.Packages[id].DepsByPkgPath { + if isVolatile(dep) { + v = true + // Keep going, to ensure that we traverse all dependencies. + } + } + if openPackages[id] { + v = true } + volatile[id] = v } - return false + return v } - for _, dep := range handles { - isVolatile(dep) + for _, id := range openPackageIDs { + _ = isVolatile(id) // populate volatile map } - for id, volatile := range volatileDeps { - if volatile { - delete(handles, id) + + var ids []PackageID + for id, v := range volatile { + if !v { + ids = append(ids, id) } } + handles, err := s.getPackageHandles(ctx, ids) + if err != nil { + return nil, err + } + // We reuse the last import graph if and only if none of the dependencies // have changed. Doing better would involve analyzing dependencies to find // subgraphs that are still valid. Not worth it, especially when in the // common case nothing has changed. - unchanged := lastImportGraph != nil && len(handles) == len(lastImportGraph.depKeys) - var ids []PackageID + unchanged := lastImportGraph != nil && len(ids) == len(lastImportGraph.depKeys) depKeys := make(map[PackageID]file.Hash) for id, ph := range handles { - ids = append(ids, id) depKeys[id] = ph.key if unchanged { prevKey, ok := lastImportGraph.depKeys[id] @@ -297,8 +328,8 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { return lastImportGraph, nil } - b, err := s.forEachPackageInternal(ctx, nil, ids, nil, nil, nil, handles) - if err != nil { + b := newTypeCheckBatch(s.view.parseCache, nil) + if err := b.query(ctx, ids, nil, nil, nil, handles); err != nil { return nil, err } @@ -307,11 +338,11 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { depKeys: depKeys, imports: make(map[PackageID]pkgOrErr), } - for id, fut := range b.importPackages { - if fut.v.pkg == nil && fut.v.err == nil { + for id, fut := range b.importPackages.cache { + if fut.v == nil && fut.err == nil { panic(fmt.Sprintf("internal error: import node %s is not evaluated", id)) } - next.imports[id] = fut.v + next.imports[id] = pkgOrErr{fut.v, fut.err} } return next, nil } @@ -385,31 +416,55 @@ func (s *Snapshot) forEachPackage(ctx context.Context, ids []PackageID, pre preT post(indexes[i], pkg) } + b, release := s.acquireTypeChecking(ctx) + defer release() + handles, err := s.getPackageHandles(ctx, needIDs) if err != nil { return err } + return b.query(ctx, nil, needIDs, pre2, post2, handles) +} - impGraph := s.getImportGraph(ctx) - _, err = s.forEachPackageInternal(ctx, impGraph, nil, needIDs, pre2, post2, handles) - return err +// acquireTypeChecking joins or starts a concurrent type checking batch. +// +// The batch may be queried for package information using [typeCheckBatch.query]. +// The second result must be called when the batch is no longer needed, to +// release the resource. +func (s *Snapshot) acquireTypeChecking(ctx context.Context) (*typeCheckBatch, func()) { + s.typeCheckMu.Lock() + defer s.typeCheckMu.Unlock() + + if s.batch == nil { + assert(s.batchRef == 0, "miscounted type checking") + impGraph := s.getImportGraph(ctx) + s.batch = newTypeCheckBatch(s.view.parseCache, impGraph) + } + s.batchRef++ + + return s.batch, func() { + s.typeCheckMu.Lock() + defer s.typeCheckMu.Unlock() + assert(s.batchRef > 0, "miscounted type checking 2") + s.batchRef-- + if s.batchRef == 0 { + s.batch = nil + } + } } -// forEachPackageInternal is used by both forEachPackage and loadImportGraph to -// type-check a graph of packages. +// newTypeCheckBatch creates a new type checking batch using the provided +// shared parseCache. // // If a non-nil importGraph is provided, imports in this graph will be reused. -func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *importGraph, importIDs, syntaxIDs []PackageID, pre preTypeCheck, post postTypeCheck, handles map[PackageID]*packageHandle) (*typeCheckBatch, error) { +func newTypeCheckBatch(parseCache *parseCache, importGraph *importGraph) *typeCheckBatch { b := &typeCheckBatch{ - pre: pre, - post: post, - handles: handles, - parseCache: s.view.parseCache, + _handles: make(map[PackageID]*packageHandle), + parseCache: parseCache, fset: fileSetWithBase(reservedForParsing), - syntaxIndex: make(map[PackageID]int), cpulimit: make(chan unit, runtime.GOMAXPROCS(0)), - syntaxPackages: make(map[PackageID]*futurePackage), - importPackages: make(map[PackageID]*futurePackage), + syntaxPackages: newFutureCache[PackageID, *Package](false), // don't persist syntax packages + importPackages: newFutureCache[PackageID, *types.Package](true), // ...but DO persist imports } if importGraph != nil { @@ -419,15 +474,28 @@ func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *impo done := make(chan unit) close(done) for id, res := range importGraph.imports { - b.importPackages[id] = &futurePackage{done, res} + b.importPackages.cache[id] = &future[*types.Package]{done: done, v: res.pkg, err: res.err} } } else { b.fset = fileSetWithBase(reservedForParsing) } + return b +} - for i, id := range syntaxIDs { - b.syntaxIndex[id] = i - } +// query executes a traversal of package information in the given typeCheckBatch. +// For each package in importIDs, the package will be loaded "for import" (sans +// syntax). +// +// For each package in syntaxIDs, the package will be handled following the +// pre- and post- traversal logic of [Snapshot.forEachPackage]. +// +// Package handles must be provided for each package in the forward transitive +// closure of either importIDs or syntaxIDs. +// +// TODO(rfindley): simplify this API by clarifying shared import graph and +// package handle logic. +func (b *typeCheckBatch) query(ctx context.Context, importIDs, syntaxIDs []PackageID, pre preTypeCheck, post postTypeCheck, handles map[PackageID]*packageHandle) error { + b.addHandles(handles) // Start a single goroutine for each requested package. // @@ -435,21 +503,17 @@ func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *impo // are not needed. var g errgroup.Group for _, id := range importIDs { - id := id g.Go(func() error { _, err := b.getImportPackage(ctx, id) return err }) } for i, id := range syntaxIDs { - i := i - id := id g.Go(func() error { - _, err := b.handleSyntaxPackage(ctx, i, id) - return err + return b.handleSyntaxPackage(ctx, i, id, pre, post) }) } - return b, g.Wait() + return g.Wait() } // TODO(rfindley): re-order the declarations below to read better from top-to-bottom. @@ -461,62 +525,31 @@ func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *impo // where id is in the set of requested IDs), a package loaded from export data, // or a package type-checked for import only. func (b *typeCheckBatch) getImportPackage(ctx context.Context, id PackageID) (pkg *types.Package, err error) { - b.mu.Lock() - f, ok := b.importPackages[id] - if ok { - b.mu.Unlock() + return b.importPackages.get(ctx, id, func(ctx context.Context) (*types.Package, error) { + ph := b.getHandle(id) - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-f.done: - return f.v.pkg, f.v.err + // "unsafe" cannot be imported or type-checked. + // + // We check PkgPath, not id, as the structure of the ID + // depends on the build system (in particular, + // Bazel+gopackagesdriver appears to use something other than + // "unsafe", though we aren't sure what; even 'go list' can + // use "p [q.test]" for testing or if PGO is enabled. + // See golang/go#60890. + if ph.mp.PkgPath == "unsafe" { + return types.Unsafe, nil } - } - - f = &futurePackage{done: make(chan unit)} - b.importPackages[id] = f - b.mu.Unlock() - defer func() { - f.v = pkgOrErr{pkg, err} - close(f.done) - }() - - if index, ok := b.syntaxIndex[id]; ok { - pkg, err := b.handleSyntaxPackage(ctx, index, id) - if err != nil { - return nil, err + data, err := filecache.Get(exportDataKind, ph.key) + if err == filecache.ErrNotFound { + // No cached export data: type-check as fast as possible. + return b.checkPackageForImport(ctx, ph) } - if pkg != nil { - return pkg, nil + if err != nil { + return nil, fmt.Errorf("failed to read cache data for %s: %v", ph.mp.ID, err) } - // type-checking was short-circuited by the pre- func. - } - - ph := b.handles[id] - - // "unsafe" cannot be imported or type-checked. - // - // We check PkgPath, not id, as the structure of the ID - // depends on the build system (in particular, - // Bazel+gopackagesdriver appears to use something other than - // "unsafe", though we aren't sure what; even 'go list' can - // use "p [q.test]" for testing or if PGO is enabled. - // See golang/go#60890. - if ph.mp.PkgPath == "unsafe" { - return types.Unsafe, nil - } - - data, err := filecache.Get(exportDataKind, ph.key) - if err == filecache.ErrNotFound { - // No cached export data: type-check as fast as possible. - return b.checkPackageForImport(ctx, ph) - } - if err != nil { - return nil, fmt.Errorf("failed to read cache data for %s: %v", ph.mp.ID, err) - } - return b.importPackage(ctx, ph.mp, data) + return b.importPackage(ctx, ph.mp, data) + }) } // handleSyntaxPackage handles one package from the ids slice. @@ -525,73 +558,61 @@ func (b *typeCheckBatch) getImportPackage(ctx context.Context, id PackageID) (pk // resulting types.Package so that it may be used for importing. // // handleSyntaxPackage returns (nil, nil) if pre returned false. -func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id PackageID) (pkg *types.Package, err error) { - b.mu.Lock() - f, ok := b.syntaxPackages[id] - if ok { - b.mu.Unlock() - <-f.done - return f.v.pkg, f.v.err - } - - f = &futurePackage{done: make(chan unit)} - b.syntaxPackages[id] = f - b.mu.Unlock() - defer func() { - f.v = pkgOrErr{pkg, err} - close(f.done) - }() - - ph := b.handles[id] - if b.pre != nil && !b.pre(i, ph) { - return nil, nil // skip: export data only - } - - // Wait for predecessors. - { - var g errgroup.Group - for _, depID := range ph.mp.DepsByPkgPath { - depID := depID - g.Go(func() error { - _, err := b.getImportPackage(ctx, depID) - return err - }) - } - if err := g.Wait(); err != nil { - // Failure to import a package should not abort the whole operation. - // Stop only if the context was cancelled, a likely cause. - // Import errors will be reported as type diagnostics. - if ctx.Err() != nil { - return nil, ctx.Err() +func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id PackageID, pre preTypeCheck, post postTypeCheck) error { + ph := b.getHandle(id) + if pre != nil && !pre(i, ph) { + return nil // skip: not needed + } + + pkg, err := b.syntaxPackages.get(ctx, id, func(ctx context.Context) (*Package, error) { + // Wait for predecessors. + { + var g errgroup.Group + for _, depID := range ph.mp.DepsByPkgPath { + g.Go(func() error { + _, err := b.getImportPackage(ctx, depID) + return err + }) + } + if err := g.Wait(); err != nil { + // Failure to import a package should not abort the whole operation. + // Stop only if the context was cancelled, a likely cause. + // Import errors will be reported as type diagnostics. + if ctx.Err() != nil { + return nil, ctx.Err() + } } } - } - // Wait to acquire a CPU token. - // - // Note: it is important to acquire this token only after awaiting - // predecessors, to avoid starvation. - select { - case <-ctx.Done(): - return nil, ctx.Err() - case b.cpulimit <- unit{}: - defer func() { - <-b.cpulimit // release CPU token - }() - } + // Wait to acquire a CPU token. + // + // Note: it is important to acquire this token only after awaiting + // predecessors, to avoid starvation. + select { + case <-ctx.Done(): + return nil, ctx.Err() + case b.cpulimit <- unit{}: + defer func() { + <-b.cpulimit // release CPU token + }() + } + + // Compute the syntax package. + p, err := b.checkPackage(ctx, ph) + if err != nil { + return nil, err // e.g. I/O error, cancelled + } - // Compute the syntax package. - p, err := b.checkPackage(ctx, ph) + // Update caches. + go storePackageResults(ctx, ph, p) // ...and write all packages to disk + return p, nil + }) if err != nil { - return nil, err + return err } - // Update caches. - go storePackageResults(ctx, ph, p) // ...and write all packages to disk - - b.post(i, p) - - return p.pkg.types, nil + post(i, pkg) + return nil } // storePackageResults serializes and writes information derived from p to the @@ -799,7 +820,7 @@ func (b *typeCheckBatch) importLookup(mp *metadata.Package) func(PackagePath) Pa impMap[depPath] = depID // TODO(rfindley): express this as an operation on the import graph // itself, rather than the set of package handles. - pending = append(pending, b.handles[depID].mp) + pending = append(pending, b.getHandle(depID).mp) if depPath == pkgPath { // Don't return early; finish processing pkg's deps. id = depID @@ -1671,7 +1692,7 @@ func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs // See TestFixImportDecl for an example. return nil, fmt.Errorf("missing metadata for import of %q", path) } - depPH := b.handles[id] + depPH := b.getHandle(id) if depPH == nil { // e.g. missing metadata for dependencies in buildPackageHandle return nil, missingPkgError(inputs.id, path, inputs.viewType) diff --git a/gopls/internal/cache/future.go b/gopls/internal/cache/future.go new file mode 100644 index 00000000000..8aa69e11fc6 --- /dev/null +++ b/gopls/internal/cache/future.go @@ -0,0 +1,136 @@ +// Copyright 2024 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 cache + +import ( + "context" + "sync" +) + +// A futureCache is a key-value store of "futures", which are values that might +// not yet be processed. By accessing values using [futureCache.get], the +// caller may share work with other goroutines that require the same key. +// +// This is a relatively common pattern, though this implementation includes the +// following two non-standard additions: +// +// 1. futures are cancellable and retryable. If the context being used to +// compute the future is cancelled, it will abort the computation. If other +// goroutes are awaiting the future, they will acquire the right to compute +// it, and start anew. +// 2. futures may be either persistent or transient. Persistent futures are +// the standard pattern: the results of the computation are preserved for +// the lifetime of the cache. However, if the cache is transient +// (persistent=false), the futures will be discarded once their value has +// been passed to all awaiting goroutines. +// +// These specific extensions are used to implement the concurrency model of the +// [typeCheckBatch], which allows multiple operations to piggy-back on top of +// an ongoing type checking operation, requesting new packages asynchronously +// without unduly increasing the in-use memory required by the type checking +// pass. +type futureCache[K comparable, V any] struct { + persistent bool + + mu sync.Mutex + cache map[K]*future[V] +} + +// newFutureCache returns a futureCache that is ready to coordinate +// computations via [futureCache.get]. +// +// If persistent is true, the results of these computations are stored for the +// lifecycle of cache. Otherwise, results are discarded after they have been +// passed to all awaiting goroutines. +func newFutureCache[K comparable, V any](persistent bool) *futureCache[K, V] { + return &futureCache[K, V]{ + persistent: persistent, + cache: make(map[K]*future[V]), + } +} + +type future[V any] struct { + // refs is the number of goroutines awaiting this future, to be used for + // cleaning up transient cache entries. + // + // Guarded by futureCache.mu. + refs int + + // done is closed when the future has been fully computed. + done chan unit + + // acquire used to select an awaiting goroutine to run the computation. + // acquire is 1-buffered, and initialized with one unit, so that the first + // requester starts a computation. If that computation is cancelled, the + // requester pushes the unit back to acquire, so that another goroutine may + // execute the computation. + acquire chan unit + + // v and err store the result of the computation, guarded by done. + v V + err error +} + +// cacheFunc is the type of a future computation function. +type cacheFunc[V any] func(context.Context) (V, error) + +// get retrieves or computes the value corresponding to k. +// +// If the cache if persistent and the value has already been computed, get +// returns the result of the previous computation. Otherwise, get either starts +// a computation or joins an ongoing computation. If that computation is +// cancelled, get will reassign the computation to a new goroutine as long as +// there are awaiters. +// +// Once the computation completes, the result is passed to all awaiting +// goroutines. If the cache is transient (persistent=false), the corresponding +// cache entry is removed, and the next call to get will execute a new +// computation. +// +// It is therefore the responsibility of the caller to ensure that the given +// compute function is safely retryable, and always returns the same value. +func (c *futureCache[K, V]) get(ctx context.Context, k K, compute cacheFunc[V]) (V, error) { + c.mu.Lock() + f, ok := c.cache[k] + if !ok { + f = &future[V]{ + done: make(chan unit), + acquire: make(chan unit, 1), + } + f.acquire <- unit{} // make available for computation + c.cache[k] = f + } + f.refs++ + c.mu.Unlock() + + defer func() { + c.mu.Lock() + defer c.mu.Unlock() + f.refs-- + if f.refs == 0 && !c.persistent { + delete(c.cache, k) + } + }() + + var zero V + select { + case <-ctx.Done(): + return zero, ctx.Err() + case <-f.done: + return f.v, f.err + case <-f.acquire: + } + + v, err := compute(ctx) + if err := ctx.Err(); err != nil { + f.acquire <- unit{} // hand off work to the next requester + return zero, err + } + + f.v = v + f.err = err + close(f.done) + return v, err +} diff --git a/gopls/internal/cache/future_test.go b/gopls/internal/cache/future_test.go new file mode 100644 index 00000000000..d96dc0f5317 --- /dev/null +++ b/gopls/internal/cache/future_test.go @@ -0,0 +1,156 @@ +// Copyright 2024 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 cache + +import ( + "context" + "fmt" + "sync/atomic" + "testing" + "time" + + "golang.org/x/sync/errgroup" +) + +func TestFutureCache_Persistent(t *testing.T) { + c := newFutureCache[int, int](true) + ctx := context.Background() + + var computed atomic.Int32 + compute := func(i int) cacheFunc[int] { + return func(context.Context) (int, error) { + computed.Add(1) + return i, ctx.Err() + } + } + + testFutureCache(t, ctx, c, compute) + + // Since this cache is persistent, we should get exactly 10 computations, + // since there are 10 distinct keys in [testFutureCache]. + if got := computed.Load(); got != 10 { + t.Errorf("computed %d times, want 10", got) + } +} + +func TestFutureCache_Ephemeral(t *testing.T) { + c := newFutureCache[int, int](false) + ctx := context.Background() + + var computed atomic.Int32 + compute := func(i int) cacheFunc[int] { + return func(context.Context) (int, error) { + time.Sleep(1 * time.Millisecond) + computed.Add(1) + return i, ctx.Err() + } + } + + testFutureCache(t, ctx, c, compute) + + // Since this cache is ephemeral, we should get at least 30 computations, + // since there are 10 distinct keys and three synchronous passes in + // [testFutureCache]. + if got := computed.Load(); got < 30 { + t.Errorf("computed %d times, want at least 30", got) + } else { + t.Logf("compute ran %d times", got) + } +} + +// testFutureCache starts 100 goroutines concurrently, indexed by j, each +// getting key j%10 from the cache. It repeats this three times, synchronizing +// after each. +// +// This is designed to exercise both concurrent and synchronous access to the +// cache. +func testFutureCache(t *testing.T, ctx context.Context, c *futureCache[int, int], compute func(int) cacheFunc[int]) { + for range 3 { + var g errgroup.Group + for j := range 100 { + mod := j % 10 + compute := compute(mod) + g.Go(func() error { + got, err := c.get(ctx, mod, compute) + if err == nil && got != mod { + t.Errorf("get() = %d, want %d", got, mod) + } + return err + }) + } + if err := g.Wait(); err != nil { + t.Fatal(err) + } + } +} + +func TestFutureCache_Retrying(t *testing.T) { + // This test verifies the retry behavior of cache entries, + // by checking that cancelled work is handed off to the next awaiter. + // + // The setup is a little tricky: 10 goroutines are started, and the first 9 + // are cancelled whereas the 10th is allowed to finish. As a result, the + // computation should always succeed with value 9. + + ctx := context.Background() + + for _, persistent := range []bool{true, false} { + t.Run(fmt.Sprintf("persistent=%t", persistent), func(t *testing.T) { + c := newFutureCache[int, int](persistent) + + var started atomic.Int32 + + // compute returns a new cacheFunc that produces the value i, after the + // provided done channel is closed. + compute := func(i int, done <-chan struct{}) cacheFunc[int] { + return func(ctx context.Context) (int, error) { + started.Add(1) + select { + case <-ctx.Done(): + return 0, ctx.Err() + case <-done: + return i, nil + } + } + } + + // goroutines are either cancelled, or allowed to complete, + // as controlled by cancels and dones. + var ( + cancels = make([]func(), 10) + dones = make([]chan struct{}, 10) + ) + + var g errgroup.Group + var lastValue atomic.Int32 // keep track of the last successfully computed value + for i := range 10 { + ctx, cancel := context.WithCancel(ctx) + done := make(chan struct{}) + cancels[i] = cancel + dones[i] = done + compute := compute(i, done) + g.Go(func() error { + v, err := c.get(ctx, 0, compute) + if err == nil { + lastValue.Store(int32(v)) + } + return nil + }) + } + for _, cancel := range cancels[:9] { + cancel() + } + defer cancels[9]() + + dones[9] <- struct{}{} + g.Wait() + + t.Logf("started %d computations", started.Load()) + if got := lastValue.Load(); got != 9 { + t.Errorf("after cancelling computation 0-8, got %d, want 9", got) + } + }) + } +} diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index a52c9689c1d..566131773fb 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -204,6 +204,14 @@ type Snapshot struct { // gcOptimizationDetails describes the packages for which we want // optimization details to be included in the diagnostics. gcOptimizationDetails map[metadata.PackageID]unit + + // Concurrent type checking: + // typeCheckMu guards the ongoing type checking batch, and reference count of + // ongoing type checking operations. + // When the batch is no longer needed (batchRef=0), it is discarded. + typeCheckMu sync.Mutex + batchRef int + batch *typeCheckBatch } var _ memoize.RefCounted = (*Snapshot)(nil) // snapshots are reference-counted diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go index a4466e2fc76..f4a32d708e2 100644 --- a/gopls/internal/server/diagnostics.go +++ b/gopls/internal/server/diagnostics.go @@ -192,17 +192,6 @@ func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot, // file modifications. // // The second phase runs after the delay, and does everything. - // - // We wait a brief delay before the first phase, to allow higher priority - // work such as autocompletion to acquire the type checking mutex (though - // typically both diagnosing changed files and performing autocompletion - // will be doing the same work: recomputing active packages). - const minDelay = 20 * time.Millisecond - select { - case <-time.After(minDelay): - case <-ctx.Done(): - return - } if len(changedURIs) > 0 { diagnostics, err := s.diagnoseChangedFiles(ctx, snapshot, changedURIs) @@ -215,12 +204,6 @@ func (s *server) diagnoseSnapshot(ctx context.Context, snapshot *cache.Snapshot, s.updateDiagnostics(ctx, snapshot, diagnostics, false) } - if delay < minDelay { - delay = 0 - } else { - delay -= minDelay - } - select { case <-time.After(delay): case <-ctx.Done(): From 06b986b1e84a607e1405e9355e86d37b7a128e3b Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 17 Sep 2024 01:14:15 +0000 Subject: [PATCH 063/102] gopls/internal/cache: add a few checks for context cancellation Now that we start diagnosing changes immediately following an edit, we should be a bit more proactive about checking for cancellation during the evaluation of a Snapshot. This CL adds a few cancellation checks at locations indicated by benchmarking. Change-Id: Iea71be2975d8343a5ca283ab9299ed5efe994bc8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/613716 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/check.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index 91398323740..de0a204c23d 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -295,6 +295,9 @@ func (s *Snapshot) resolveImportGraph() (*importGraph, error) { return v } for _, id := range openPackageIDs { + if ctx.Err() != nil { + return nil, ctx.Err() + } _ = isVolatile(id) // populate volatile map } @@ -504,12 +507,18 @@ func (b *typeCheckBatch) query(ctx context.Context, importIDs, syntaxIDs []Packa var g errgroup.Group for _, id := range importIDs { g.Go(func() error { + if ctx.Err() != nil { + return ctx.Err() + } _, err := b.getImportPackage(ctx, id) return err }) } for i, id := range syntaxIDs { g.Go(func() error { + if ctx.Err() != nil { + return ctx.Err() + } return b.handleSyntaxPackage(ctx, i, id, pre, post) }) } @@ -981,6 +990,10 @@ func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[ return n } for _, id := range ids { + if ctx.Err() != nil { + s.mu.Unlock() + return nil, ctx.Err() + } makeNode(nil, id) } s.mu.Unlock() From b577f77ea7365241868456d7a0cc210e6045087d Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 18 Sep 2024 15:28:05 +0000 Subject: [PATCH 064/102] gopls/internal/cache: remove unnecessary active package check Snapshot.TypeCheck implemented an eager check for active packages that was redundant with the same check in Snapshot.forEachPackage. Remove it. Change-Id: Ia58c85ac4e8795ddcc47c124d006c609b8ae715e Reviewed-on: https://go-review.googlesource.com/c/tools/+/614155 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/check.go | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index de0a204c23d..601a3e4fd32 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -129,30 +129,10 @@ type pkgOrErr struct { // of the potentially type-checking methods below. func (s *Snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]*Package, error) { pkgs := make([]*Package, len(ids)) - - var ( - needIDs []PackageID // ids to type-check - indexes []int // original index of requested ids - ) - - // Check for existing active packages, as any package will do. - // - // This is also done inside forEachPackage, but doing it here avoids - // unnecessary set up for type checking (e.g. assembling the package handle - // graph). - for i, id := range ids { - if pkg := s.getActivePackage(id); pkg != nil { - pkgs[i] = pkg - } else { - needIDs = append(needIDs, id) - indexes = append(indexes, i) - } - } - post := func(i int, pkg *Package) { - pkgs[indexes[i]] = pkg + pkgs[i] = pkg } - return pkgs, s.forEachPackage(ctx, needIDs, nil, post) + return pkgs, s.forEachPackage(ctx, ids, nil, post) } // getImportGraph returns a shared import graph use for this snapshot, or nil. From 54110aa1994bf24eedb96ac20ac0c99fd270c0b6 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Wed, 11 Sep 2024 08:05:40 -0400 Subject: [PATCH 065/102] internal/modindex: package for indexing GOMODCACHE This CL contains the first part of a package for maintaining an on-disk index of the module cache. The index is stored as text. Eventually it will consist of a header, followed by groups of lines, one for each import path, and sorted by package name. The groups of lines start with a header containing the package name, import path, name of the directory, and semantic version, followed (but not in this first CL) by lines, each of which contains information about one exported symbol. This CL only contains the code for computing and updating the information about directories and import paths, and reading the index. It does not compute anything about exported symbols, which will be in the next CL, and hence it does not present an API for looking up information about completion of selectors. There is a test that among directories with the same import path it can find the one with the largest semantic version. Change-Id: I0883ea732cf34f6700f5495e6dfd594e8f286af9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/612355 TryBot-Bypass: Peter Weinberger Reviewed-by: Robert Findley --- internal/modindex/dir_test.go | 127 +++++++++++++++ internal/modindex/directories.go | 137 +++++++++++++++++ internal/modindex/index.go | 256 +++++++++++++++++++++++++++++++ internal/modindex/modindex.go | 148 ++++++++++++++++++ internal/modindex/types.go | 25 +++ 5 files changed, 693 insertions(+) create mode 100644 internal/modindex/dir_test.go create mode 100644 internal/modindex/directories.go create mode 100644 internal/modindex/index.go create mode 100644 internal/modindex/modindex.go create mode 100644 internal/modindex/types.go diff --git a/internal/modindex/dir_test.go b/internal/modindex/dir_test.go new file mode 100644 index 00000000000..862d111ea42 --- /dev/null +++ b/internal/modindex/dir_test.go @@ -0,0 +1,127 @@ +// Copyright 2024 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 modindex + +import ( + "os" + "path/filepath" + "testing" +) + +type id struct { + importPath string + best int // which of the dirs is the one that should have been chosen + dirs []string +} + +var idtests = []id{ + { // get one right + importPath: "cloud.google.com/go/longrunning", + best: 2, + dirs: []string{ + "cloud.google.com/go/longrunning@v0.3.0", + "cloud.google.com/go/longrunning@v0.4.1", + "cloud.google.com/go@v0.104.0/longrunning", + "cloud.google.com/go@v0.94.0/longrunning", + }, + }, + { // make sure we can run more than one test + importPath: "cloud.google.com/go/compute/metadata", + best: 2, + dirs: []string{ + "cloud.google.com/go/compute/metadata@v0.2.1", + "cloud.google.com/go/compute/metadata@v0.2.3", + "cloud.google.com/go/compute@v1.7.0/metadata", + "cloud.google.com/go@v0.94.0/compute/metadata", + }, + }, + { //m test bizarre characters in directory name + importPath: "bad,guy.com/go", + best: 0, + dirs: []string{"bad,guy.com/go@v0.1.0"}, + }, +} + +func testModCache(t *testing.T) string { + t.Helper() + dir := t.TempDir() + IndexDir = func() (string, error) { return dir, nil } + return dir +} + +func TestDirsSinglePath(t *testing.T) { + for _, itest := range idtests { + t.Run(itest.importPath, func(t *testing.T) { + // create a new fake GOMODCACHE + dir := testModCache(t) + for _, d := range itest.dirs { + if err := os.MkdirAll(filepath.Join(dir, d), 0755); err != nil { + t.Fatal(err) + } + // gopathwalk wants to see .go files + err := os.WriteFile(filepath.Join(dir, d, "main.go"), []byte("package main\nfunc main() {}"), 0600) + if err != nil { + t.Fatal(err) + } + } + // build and check the index + if err := IndexModCache(dir, false); err != nil { + t.Fatal(err) + } + ix, err := ReadIndex(dir) + if err != nil { + t.Fatal(err) + } + if len(ix.Entries) != 1 { + t.Fatalf("got %d entries, wanted 1", len(ix.Entries)) + } + if ix.Entries[0].ImportPath != itest.importPath { + t.Fatalf("got %s import path, wanted %s", ix.Entries[0].ImportPath, itest.importPath) + } + if ix.Entries[0].Dir != Relpath(itest.dirs[itest.best]) { + t.Fatalf("got dir %s, wanted %s", ix.Entries[0].Dir, itest.dirs[itest.best]) + } + }) + } +} + +/* more data for tests + +directories.go:169: WEIRD cloud.google.com/go/iam/admin/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0/admin/apiv1 +cloud.google.com/go/iam@v0.13.0/admin/apiv1 +cloud.google.com/go/iam@v0.3.0/admin/apiv1 +cloud.google.com/go/iam@v0.7.0/admin/apiv1 +cloud.google.com/go/iam@v1.0.1/admin/apiv1 +cloud.google.com/go@v0.94.0/iam/admin/apiv1] +directories.go:169: WEIRD cloud.google.com/go/iam +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0 cloud.google.com/go/iam@v0.13.0 +cloud.google.com/go/iam@v0.3.0 cloud.google.com/go/iam@v0.7.0 +cloud.google.com/go/iam@v1.0.1 cloud.google.com/go@v0.94.0/iam] +directories.go:169: WEIRD cloud.google.com/go/compute/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/compute:4]: +[cloud.google.com/go/compute@v1.12.1/apiv1 +cloud.google.com/go/compute@v1.18.0/apiv1 +cloud.google.com/go/compute@v1.19.0/apiv1 +cloud.google.com/go/compute@v1.7.0/apiv1 +cloud.google.com/go@v0.94.0/compute/apiv1] +directories.go:169: WEIRD cloud.google.com/go/longrunning/autogen +map[cloud.google.com/go:2 cloud.google.com/go/longrunning:2]: +[cloud.google.com/go/longrunning@v0.3.0/autogen +cloud.google.com/go/longrunning@v0.4.1/autogen +cloud.google.com/go@v0.104.0/longrunning/autogen +cloud.google.com/go@v0.94.0/longrunning/autogen] +directories.go:169: WEIRD cloud.google.com/go/iam/credentials/apiv1 +map[cloud.google.com/go:1 cloud.google.com/go/iam:5]: +[cloud.google.com/go/iam@v0.12.0/credentials/apiv1 +cloud.google.com/go/iam@v0.13.0/credentials/apiv1 +cloud.google.com/go/iam@v0.3.0/credentials/apiv1 +cloud.google.com/go/iam@v0.7.0/credentials/apiv1 +cloud.google.com/go/iam@v1.0.1/credentials/apiv1 +cloud.google.com/go@v0.94.0/iam/credentials/apiv1] + +*/ diff --git a/internal/modindex/directories.go b/internal/modindex/directories.go new file mode 100644 index 00000000000..b8aab3b736e --- /dev/null +++ b/internal/modindex/directories.go @@ -0,0 +1,137 @@ +// Copyright 2024 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 modindex + +import ( + "fmt" + "log" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "sync" + "time" + + "golang.org/x/mod/semver" + "golang.org/x/tools/internal/gopathwalk" +) + +type directory struct { + path Relpath + importPath string + version string // semantic version +} + +// filterDirs groups the directories by import path, +// sorting the ones with the same import path by semantic version, +// most recent first. +func byImportPath(dirs []Relpath) (map[string][]*directory, error) { + ans := make(map[string][]*directory) // key is import path + for _, d := range dirs { + ip, sv, err := DirToImportPathVersion(d) + if err != nil { + return nil, err + } + ans[ip] = append(ans[ip], &directory{ + path: d, + importPath: ip, + version: sv, + }) + } + for k, v := range ans { + semanticSort(v) + ans[k] = v + } + return ans, nil +} + +// sort the directories by semantic version, lates first +func semanticSort(v []*directory) { + slices.SortFunc(v, func(l, r *directory) int { + if n := semver.Compare(l.version, r.version); n != 0 { + return -n // latest first + } + return strings.Compare(string(l.path), string(r.path)) + }) +} + +// modCacheRegexp splits a relpathpath into module, module version, and package. +var modCacheRegexp = regexp.MustCompile(`(.*)@([^/\\]*)(.*)`) + +// DirToImportPathVersion computes import path and semantic version +func DirToImportPathVersion(dir Relpath) (string, string, error) { + m := modCacheRegexp.FindStringSubmatch(string(dir)) + // m[1] is the module path + // m[2] is the version major.minor.patch(-
 that contains the name
+// of the current index. We believe writing that short file is atomic.
+// ReadIndex reads that file to get the file name of the index.
+// WriteIndex writes an index with a unique name and then
+// writes that name into a new version of index-name-.
+// ( stands for the CurrentVersion of the index format.)
+package modindex
+
+import (
+	"log"
+	"path/filepath"
+	"slices"
+	"strings"
+	"time"
+
+	"golang.org/x/mod/semver"
+)
+
+// Modindex writes an index current as of when it is called.
+// If clear is true the index is constructed from all of GOMODCACHE
+// otherwise the index is constructed from the last previous index
+// and the updates to the cache.
+func IndexModCache(cachedir string, clear bool) error {
+	cachedir, err := filepath.Abs(cachedir)
+	if err != nil {
+		return err
+	}
+	cd := Abspath(cachedir)
+	future := time.Now().Add(24 * time.Hour) // safely in the future
+	err = modindexTimed(future, cd, clear)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// modindexTimed writes an index current as of onlyBefore.
+// If clear is true the index is constructed from all of GOMODCACHE
+// otherwise the index is constructed from the last previous index
+// and all the updates to the cache before onlyBefore.
+// (this is useful for testing; perhaps it should not be exported)
+func modindexTimed(onlyBefore time.Time, cachedir Abspath, clear bool) error {
+	var curIndex *Index
+	if !clear {
+		var err error
+		curIndex, err = ReadIndex(string(cachedir))
+		if clear && err != nil {
+			return err
+		}
+		// TODO(pjw): check that most of those directorie still exist
+	}
+	cfg := &work{
+		onlyBefore: onlyBefore,
+		oldIndex:   curIndex,
+		cacheDir:   cachedir,
+	}
+	if curIndex != nil {
+		cfg.onlyAfter = curIndex.Changed
+	}
+	if err := cfg.buildIndex(); err != nil {
+		return err
+	}
+	if err := cfg.writeIndex(); err != nil {
+		return err
+	}
+	return nil
+}
+
+type work struct {
+	onlyBefore time.Time // do not use directories later than this
+	onlyAfter  time.Time // only interested in directories after this
+	// directories from before onlyAfter come from oldIndex
+	oldIndex *Index
+	newIndex *Index
+	cacheDir Abspath
+}
+
+func (w *work) buildIndex() error {
+	// The effective date of the new index should be at least
+	// slightly earlier than when the directories are scanned
+	// so set it now.
+	w.newIndex = &Index{Changed: time.Now(), Cachedir: w.cacheDir}
+	dirs := findDirs(string(w.cacheDir), w.onlyAfter, w.onlyBefore)
+	newdirs, err := byImportPath(dirs)
+	if err != nil {
+		return err
+	}
+	log.Printf("%d dirs, %d ips", len(dirs), len(newdirs))
+	// for each import path it might occur only in newdirs,
+	// only in w.oldIndex, or in both.
+	// If it occurs in both, use the semantically later one
+	if w.oldIndex != nil {
+		killed := 0
+		for _, e := range w.oldIndex.Entries {
+			found, ok := newdirs[e.ImportPath]
+			if !ok {
+				continue
+			}
+			if semver.Compare(found[0].version, e.Version) > 0 {
+				// the new one is better, disable the old one
+				e.ImportPath = ""
+				killed++
+			} else {
+				// use the old one, forget the new one
+				delete(newdirs, e.ImportPath)
+			}
+		}
+		log.Printf("%d killed, %d ips", killed, len(newdirs))
+	}
+	// Build the skeleton of the new index using newdirs,
+	// and include the surviving parts of the old index
+	if w.oldIndex != nil {
+		for _, e := range w.oldIndex.Entries {
+			if e.ImportPath != "" {
+				w.newIndex.Entries = append(w.newIndex.Entries, e)
+			}
+		}
+	}
+	for k, v := range newdirs {
+		d := v[0]
+		entry := Entry{
+			Dir:        d.path,
+			ImportPath: k,
+			Version:    d.version,
+		}
+		w.newIndex.Entries = append(w.newIndex.Entries, entry)
+	}
+	// find symbols for the incomplete entries
+	log.Print("not finding any symbols yet")
+	// sort the entries in the new index
+	slices.SortFunc(w.newIndex.Entries, func(l, r Entry) int {
+		if n := strings.Compare(l.PkgName, r.PkgName); n != 0 {
+			return n
+		}
+		return strings.Compare(l.ImportPath, r.ImportPath)
+	})
+	return nil
+}
+
+func (w *work) writeIndex() error {
+	return writeIndex(w.cacheDir, w.newIndex)
+}
diff --git a/internal/modindex/types.go b/internal/modindex/types.go
new file mode 100644
index 00000000000..ece44886309
--- /dev/null
+++ b/internal/modindex/types.go
@@ -0,0 +1,25 @@
+// Copyright 2024 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 modindex
+
+import (
+	"strings"
+)
+
+// some special types to avoid confusions
+
+// distinguish various types of directory names. It's easy to get confused.
+type Abspath string // absolute paths
+type Relpath string // paths with GOMODCACHE prefix removed
+
+func toRelpath(cachedir Abspath, s string) Relpath {
+	if strings.HasPrefix(s, string(cachedir)) {
+		if s == string(cachedir) {
+			return Relpath("")
+		}
+		return Relpath(s[len(cachedir)+1:])
+	}
+	return Relpath(s)
+}

From 81a4242b11cb7a2a89ea4fbf7536132e462c67ad Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Wed, 25 Sep 2024 11:11:01 -0700
Subject: [PATCH 066/102] internal/gcimporter: update TestIssueAliases
 expectation

Fixes golang/go#69627

Change-Id: I96460aa3c4b2863ae50fefd09a85e670d166b773
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615855
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Robert Findley 
Reviewed-by: Alan Donovan 
---
 internal/gcimporter/gcimporter_test.go | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go
index 1942386a602..f23cc14d7fd 100644
--- a/internal/gcimporter/gcimporter_test.go
+++ b/internal/gcimporter/gcimporter_test.go
@@ -1009,7 +1009,7 @@ func TestIssueAliases(t *testing.T) {
 		"X : testdata/b.B[int]",
 		"Y : testdata/c.c[string]",
 		"Z : testdata/c.c[int]",
-		"c : testdata/c.c",
+		"c : testdata/c.c[V any]",
 	}, ",")
 	if got := strings.Join(objs, ","); got != want {
 		t.Errorf("got imports %v for package c. wanted %v", objs, want)

From faf6e2869e8e491d9feb58d0f18db6f2a209b808 Mon Sep 17 00:00:00 2001
From: xieyuschen 
Date: Mon, 23 Sep 2024 17:30:05 +0800
Subject: [PATCH 067/102] go/ssa: migrate TestGenericAliases away from loader

Updates golang/go#69556

Change-Id: I50ac457a379afa9ceccd0031397fca7b38e7f689
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615015
Reviewed-by: Tim King 
Reviewed-by: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
---
 go/ssa/builder_test.go | 21 ++++-----------------
 1 file changed, 4 insertions(+), 17 deletions(-)

diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go
index c2e5590b2ec..cb309cf3188 100644
--- a/go/ssa/builder_test.go
+++ b/go/ssa/builder_test.go
@@ -1132,10 +1132,10 @@ func TestGenericAliases(t *testing.T) {
 }
 
 func testGenericAliases(t *testing.T) {
-	t.Setenv("GOEXPERIMENT", "aliastypeparams=1")
+	testenv.NeedsGoExperiment(t, "aliastypeparams")
 
 	const source = `
-package P
+package p
 
 type A = uint8
 type B[T any] = [4]T
@@ -1167,22 +1167,9 @@ func f[S any]() {
 }
 `
 
-	conf := loader.Config{Fset: token.NewFileSet()}
-	f, err := parser.ParseFile(conf.Fset, "p.go", source, 0)
-	if err != nil {
-		t.Fatal(err)
-	}
-	conf.CreateFromFiles("p", f)
-	iprog, err := conf.Load()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	// Create and build SSA program.
-	prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics)
-	prog.Build()
+	p, _ := buildPackage(t, source, ssa.InstantiateGenerics)
 
-	probes := callsTo(ssautil.AllFunctions(prog), "print")
+	probes := callsTo(ssautil.AllFunctions(p.Prog), "print")
 	if got, want := len(probes), 3*4*2; got != want {
 		t.Errorf("Found %v probes, expected %v", got, want)
 	}

From 7bb384dcf895120f692ab71f688d1ff31d193e78 Mon Sep 17 00:00:00 2001
From: Rob Findley 
Date: Thu, 19 Sep 2024 14:31:38 +0000
Subject: [PATCH 068/102] gopls/internal/test/integration/bench: add an IWL
 test that opens files

The existing BenchmarkInitialWorkspaceLoad does not open files, which
means it measures only the speed of loading and verifying the workspace
state, with hot caches. It does not measure the memory or time consumed
when starting to actually work on a file.

Fix this by opening a file.

Also, add -profile.block to gopls and -gopls_blockprofile to gopls
benchmarks, for better diagnosis of contention.

Change-Id: I63ef7c9a26ca71ddd9b6895369921655eaa4f090
Reviewed-on: https://go-review.googlesource.com/c/tools/+/614163
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Alan Donovan 
---
 gopls/internal/cmd/usage/usage-v.hlp          |  2 +
 gopls/internal/cmd/usage/usage.hlp            |  2 +
 .../test/integration/bench/bench_test.go      |  4 ++
 gopls/internal/test/integration/bench/doc.go  |  5 ++-
 .../test/integration/bench/iwl_test.go        | 40 ++++++++++++++++---
 internal/tool/tool.go                         | 25 +++++++++++-
 6 files changed, 69 insertions(+), 9 deletions(-)

diff --git a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp
index 21c124eef97..64f99a3387e 100644
--- a/gopls/internal/cmd/usage/usage-v.hlp
+++ b/gopls/internal/cmd/usage/usage-v.hlp
@@ -67,6 +67,8 @@ flags:
     	port on which to run gopls for debugging purposes
   -profile.alloc=string
     	write alloc profile to this file
+  -profile.block=string
+    	write block profile to this file
   -profile.cpu=string
     	write CPU profile to this file
   -profile.mem=string
diff --git a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp
index 9ee0077ac95..c801a467626 100644
--- a/gopls/internal/cmd/usage/usage.hlp
+++ b/gopls/internal/cmd/usage/usage.hlp
@@ -64,6 +64,8 @@ flags:
     	port on which to run gopls for debugging purposes
   -profile.alloc=string
     	write alloc profile to this file
+  -profile.block=string
+    	write block profile to this file
   -profile.cpu=string
     	write CPU profile to this file
   -profile.mem=string
diff --git a/gopls/internal/test/integration/bench/bench_test.go b/gopls/internal/test/integration/bench/bench_test.go
index 5de6804c03b..3e163d11127 100644
--- a/gopls/internal/test/integration/bench/bench_test.go
+++ b/gopls/internal/test/integration/bench/bench_test.go
@@ -42,6 +42,7 @@ var (
 	cpuProfile   = flag.String("gopls_cpuprofile", "", "if set, the cpu profile file suffix; see \"Profiling\" in the package doc")
 	memProfile   = flag.String("gopls_memprofile", "", "if set, the mem profile file suffix; see \"Profiling\" in the package doc")
 	allocProfile = flag.String("gopls_allocprofile", "", "if set, the alloc profile file suffix; see \"Profiling\" in the package doc")
+	blockProfile = flag.String("gopls_blockprofile", "", "if set, the block profile file suffix; see \"Profiling\" in the package doc")
 	trace        = flag.String("gopls_trace", "", "if set, the trace file suffix; see \"Profiling\" in the package doc")
 
 	// If non-empty, tempDir is a temporary working dir that was created by this
@@ -177,6 +178,9 @@ func profileArgs(name string, wantCPU bool) []string {
 	if *allocProfile != "" {
 		args = append(args, fmt.Sprintf("-profile.alloc=%s", qualifiedName(name, *allocProfile)))
 	}
+	if *blockProfile != "" {
+		args = append(args, fmt.Sprintf("-profile.block=%s", qualifiedName(name, *blockProfile)))
+	}
 	if *trace != "" {
 		args = append(args, fmt.Sprintf("-profile.trace=%s", qualifiedName(name, *trace)))
 	}
diff --git a/gopls/internal/test/integration/bench/doc.go b/gopls/internal/test/integration/bench/doc.go
index fff7bac1785..e60a3029569 100644
--- a/gopls/internal/test/integration/bench/doc.go
+++ b/gopls/internal/test/integration/bench/doc.go
@@ -16,8 +16,9 @@
 //
 // Benchmark functions run gopls in a separate process, which means the normal
 // test flags for profiling aren't useful. Instead the -gopls_cpuprofile,
-// -gopls_memprofile, -gopls_allocprofile, and -gopls_trace flags may be used
-// to pass through profiling to the gopls subproces.
+// -gopls_memprofile, -gopls_allocprofile, -gopls_blockprofile, and
+// -gopls_trace flags may be used to pass through profiling to the gopls
+// subproces.
 //
 // Each of these flags sets a suffix for the respective gopls profile, which is
 // named according to the schema ... For example,
diff --git a/gopls/internal/test/integration/bench/iwl_test.go b/gopls/internal/test/integration/bench/iwl_test.go
index ecf26f95463..09ccb301a58 100644
--- a/gopls/internal/test/integration/bench/iwl_test.go
+++ b/gopls/internal/test/integration/bench/iwl_test.go
@@ -15,6 +15,11 @@ import (
 
 // BenchmarkInitialWorkspaceLoad benchmarks the initial workspace load time for
 // a new editing session.
+//
+// The OpenFiles variant of this test is more realistic: who cares if gopls is
+// initialized if you can't use it? However, this test is left as is to
+// preserve the validity of historical data, and to represent the baseline
+// performance of validating the workspace state.
 func BenchmarkInitialWorkspaceLoad(b *testing.B) {
 	repoNames := []string{
 		"google-cloud-go",
@@ -37,13 +42,33 @@ func BenchmarkInitialWorkspaceLoad(b *testing.B) {
 			b.ResetTimer()
 
 			for i := 0; i < b.N; i++ {
-				doIWL(b, sharedEnv.Sandbox.GOPATH(), repo)
+				doIWL(b, sharedEnv.Sandbox.GOPATH(), repo, nil)
 			}
 		})
 	}
 }
 
-func doIWL(b *testing.B, gopath string, repo *repo) {
+// BenchmarkInitialWorkspaceLoadOpenFiles benchmarks the initial workspace load
+// after opening one or more files.
+//
+// It may differ significantly from [BenchmarkInitialWorkspaceLoad], since
+// there is various active state that is proportional to the number of open
+// files.
+func BenchmarkInitialWorkspaceLoadOpenFiles(b *testing.B) {
+	for _, t := range didChangeTests {
+		b.Run(t.repo, func(b *testing.B) {
+			repo := getRepo(b, t.repo)
+			sharedEnv := repo.sharedEnv(b)
+			b.ResetTimer()
+
+			for range b.N {
+				doIWL(b, sharedEnv.Sandbox.GOPATH(), repo, []string{t.file})
+			}
+		})
+	}
+}
+
+func doIWL(b *testing.B, gopath string, repo *repo, openfiles []string) {
 	// Exclude the time to set up the env from the benchmark time, as this may
 	// involve installing gopls and/or checking out the repo dir.
 	b.StopTimer()
@@ -52,11 +77,16 @@ func doIWL(b *testing.B, gopath string, repo *repo) {
 	defer env.Close()
 	b.StartTimer()
 
-	// Note: in the future, we may need to open a file in order to cause gopls to
-	// start loading the workspace.
-
+	// TODO(rfindley): not awaiting the IWL here leads to much more volatile
+	// results. Investigate.
 	env.Await(InitialWorkspaceLoad)
 
+	for _, f := range openfiles {
+		env.OpenFile(f)
+	}
+
+	env.AfterChange()
+
 	if env.Editor.HasCommand(command.MemStats) {
 		b.StopTimer()
 		params := &protocol.ExecuteCommandParams{
diff --git a/internal/tool/tool.go b/internal/tool/tool.go
index eadb0fb5ab9..46f5b87fa35 100644
--- a/internal/tool/tool.go
+++ b/internal/tool/tool.go
@@ -45,6 +45,7 @@ type Profile struct {
 	Memory string `flag:"profile.mem" help:"write memory profile to this file"`
 	Alloc  string `flag:"profile.alloc" help:"write alloc profile to this file"`
 	Trace  string `flag:"profile.trace" help:"write trace log to this file"`
+	Block  string `flag:"profile.block" help:"write block profile to this file"`
 }
 
 // Application is the interface that must be satisfied by an object passed to Main.
@@ -172,7 +173,9 @@ func Run(ctx context.Context, s *flag.FlagSet, app Application, args []string) (
 			if err := pprof.WriteHeapProfile(f); err != nil {
 				log.Printf("Writing memory profile: %v", err)
 			}
-			f.Close()
+			if err := f.Close(); err != nil {
+				log.Printf("Closing memory profile: %v", err)
+			}
 		}()
 	}
 
@@ -185,7 +188,25 @@ func Run(ctx context.Context, s *flag.FlagSet, app Application, args []string) (
 			if err := pprof.Lookup("allocs").WriteTo(f, 0); err != nil {
 				log.Printf("Writing alloc profile: %v", err)
 			}
-			f.Close()
+			if err := f.Close(); err != nil {
+				log.Printf("Closing alloc profile: %v", err)
+			}
+		}()
+	}
+
+	if p != nil && p.Block != "" {
+		f, err := os.Create(p.Block)
+		if err != nil {
+			return err
+		}
+		runtime.SetBlockProfileRate(1) // record all blocking events
+		defer func() {
+			if err := pprof.Lookup("block").WriteTo(f, 0); err != nil {
+				log.Printf("Writing block profile: %v", err)
+			}
+			if err := f.Close(); err != nil {
+				log.Printf("Closing block profile: %v", err)
+			}
 		}()
 	}
 

From 8adb6e83db735d889de177549fd9c9526c71b07f Mon Sep 17 00:00:00 2001
From: xieyuschen 
Date: Mon, 23 Sep 2024 12:49:17 +0800
Subject: [PATCH 069/102] go/ssa: migrate TestTypeparamTest away from loader

Updates golang/go#69556

Change-Id: I5683a0139595c5b505e25cdccba3dad422cc6d69
Reviewed-on: https://go-review.googlesource.com/c/tools/+/614975
Reviewed-by: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Tim King 
Auto-Submit: Alan Donovan 
---
 go/ssa/builder_test.go | 52 +++++++++++++++---------------------------
 1 file changed, 19 insertions(+), 33 deletions(-)

diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go
index cb309cf3188..acc7787cef3 100644
--- a/go/ssa/builder_test.go
+++ b/go/ssa/builder_test.go
@@ -8,11 +8,11 @@ import (
 	"bytes"
 	"fmt"
 	"go/ast"
-	"go/build"
 	"go/importer"
 	"go/parser"
 	"go/token"
 	"go/types"
+	"io/fs"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -27,6 +27,7 @@ import (
 	"golang.org/x/tools/go/buildutil"
 	"golang.org/x/tools/go/expect"
 	"golang.org/x/tools/go/loader"
+	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/go/ssa"
 	"golang.org/x/tools/go/ssa/ssautil"
 	"golang.org/x/tools/internal/testenv"
@@ -677,57 +678,42 @@ func TestTypeparamTest(t *testing.T) {
 		t.Skip("Consistent flakes on wasm (e.g. https://go.dev/issues/64726)")
 	}
 
-	dir := filepath.Join(build.Default.GOROOT, "test", "typeparam")
+	// located GOROOT based on the relative path of errors in $GOROOT/src/errors
+	stdPkgs, err := packages.Load(&packages.Config{
+		Mode: packages.NeedFiles,
+	}, "errors")
+	if err != nil {
+		t.Fatalf("Failed to load errors package from std: %s", err)
+	}
+	goroot := filepath.Dir(filepath.Dir(filepath.Dir(stdPkgs[0].GoFiles[0])))
+
+	dir := filepath.Join(goroot, "test", "typeparam")
 
 	// Collect all of the .go files in
-	list, err := os.ReadDir(dir)
+	fsys := os.DirFS(dir)
+	entries, err := fs.ReadDir(fsys, ".")
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	for _, entry := range list {
+	for _, entry := range entries {
 		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
 			continue // Consider standalone go files.
 		}
-		input := filepath.Join(dir, entry.Name())
 		t.Run(entry.Name(), func(t *testing.T) {
-			src, err := os.ReadFile(input)
+			src, err := fs.ReadFile(fsys, entry.Name())
 			if err != nil {
 				t.Fatal(err)
 			}
+
 			// Only build test files that can be compiled, or compiled and run.
 			if !bytes.HasPrefix(src, []byte("// run")) && !bytes.HasPrefix(src, []byte("// compile")) {
 				t.Skipf("not detected as a run test")
 			}
 
-			t.Logf("Input: %s\n", input)
-
-			ctx := build.Default    // copy
-			ctx.GOROOT = "testdata" // fake goroot. Makes tests ~1s. tests take ~80s.
-
-			reportErr := func(err error) {
-				t.Error(err)
-			}
-			conf := loader.Config{Build: &ctx, TypeChecker: types.Config{Error: reportErr}}
-			if _, err := conf.FromArgs([]string{input}, true); err != nil {
-				t.Fatalf("FromArgs(%s) failed: %s", input, err)
-			}
-
-			iprog, err := conf.Load()
-			if iprog != nil {
-				for _, pkg := range iprog.Created {
-					for i, e := range pkg.Errors {
-						t.Errorf("Loading pkg %s error[%d]=%s", pkg, i, e)
-					}
-				}
-			}
-			if err != nil {
-				t.Fatalf("conf.Load(%s) failed: %s", input, err)
-			}
+			t.Logf("Input: %s\n", entry.Name())
 
-			mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics
-			prog := ssautil.CreateProgram(iprog, mode)
-			prog.Build()
+			_, _ = buildPackage(t, string(src), ssa.SanityCheckFunctions|ssa.InstantiateGenerics)
 		})
 	}
 }

From 23e1af6910ab6f93deef899f7b65e103a6ee446e Mon Sep 17 00:00:00 2001
From: xieyuschen 
Date: Mon, 23 Sep 2024 13:07:06 +0800
Subject: [PATCH 070/102] go/ssa: migrate TestGenericFunctionSelector away from
 loader

Use qualified identifier to check instantiated function.

Updates golang/go#69556

Change-Id: I857de1b3d13d052dc09d96454715f8128237362e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/614995
Auto-Submit: Alan Donovan 
Reviewed-by: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Tim King 
---
 go/ssa/builder_test.go | 33 +++++++++++++++------------------
 1 file changed, 15 insertions(+), 18 deletions(-)

diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go
index acc7787cef3..0227b5d481d 100644
--- a/go/ssa/builder_test.go
+++ b/go/ssa/builder_test.go
@@ -24,7 +24,6 @@ import (
 
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/analysis/analysistest"
-	"golang.org/x/tools/go/buildutil"
 	"golang.org/x/tools/go/expect"
 	"golang.org/x/tools/go/loader"
 	"golang.org/x/tools/go/packages"
@@ -767,30 +766,28 @@ func sliceMax(s []int) []int { return s[a():b():c()] }
 
 // TestGenericFunctionSelector ensures generic functions from other packages can be selected.
 func TestGenericFunctionSelector(t *testing.T) {
-	pkgs := map[string]map[string]string{
-		"main": {"m.go": `package main; import "a"; func main() { a.F[int](); a.G[int,string](); a.H(0) }`},
-		"a":    {"a.go": `package a; func F[T any](){}; func G[S, T any](){}; func H[T any](a T){} `},
-	}
+	fsys := overlayFS(map[string][]byte{
+		"go.mod":  goMod("example.com", -1),
+		"main.go": []byte(`package main; import "example.com/a"; func main() { a.F[int](); a.G[int,string](); a.H(0) }`),
+		"a/a.go":  []byte(`package a; func F[T any](){}; func G[S, T any](){}; func H[T any](a T){} `),
+	})
 
 	for _, mode := range []ssa.BuilderMode{
 		ssa.SanityCheckFunctions,
 		ssa.SanityCheckFunctions | ssa.InstantiateGenerics,
 	} {
-		conf := loader.Config{
-			Build: buildutil.FakeContext(pkgs),
-		}
-		conf.Import("main")
 
-		lprog, err := conf.Load()
-		if err != nil {
-			t.Errorf("Load failed: %s", err)
+		pkgs := loadPackages(t, fsys, "example.com") // package main
+		if len(pkgs) != 1 {
+			t.Fatalf("Expected 1 root package but got %d", len(pkgs))
 		}
-		if lprog == nil {
-			t.Fatalf("Load returned nil *Program")
+		prog, _ := ssautil.Packages(pkgs, mode)
+		p := prog.Package(pkgs[0].Types)
+		p.Build()
+
+		if p.Pkg.Name() != "main" {
+			t.Fatalf("Expected the second package is main but got %s", p.Pkg.Name())
 		}
-		// Create and build SSA
-		prog := ssautil.CreateProgram(lprog, mode)
-		p := prog.Package(lprog.Package("main").Pkg)
 		p.Build()
 
 		var callees []string // callees of the CallInstruction.String() in main().
@@ -807,7 +804,7 @@ func TestGenericFunctionSelector(t *testing.T) {
 		}
 		sort.Strings(callees) // ignore the order in the code.
 
-		want := "[a.F[int] a.G[int string] a.H[int]]"
+		want := "[example.com/a.F[int] example.com/a.G[int string] example.com/a.H[int]]"
 		if got := fmt.Sprint(callees); got != want {
 			t.Errorf("Expected main() to contain calls %v. got %v", want, got)
 		}

From 1c9ca8b8bac40ec8a6e010a7c5b60f47591e7db8 Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Wed, 25 Sep 2024 11:55:31 -0700
Subject: [PATCH 071/102] go/types/objectpath,internal/aliases: miscellaneous
 clean up of tests that set gotypesalias

* Fix default value test ("") for TestNewAlias.
* Updates references to 1.19 to 1.22.

Change-Id: I9c19621e820c90c265235afff863863d03f2d171
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615696
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Robert Findley 
---
 go/types/objectpath/objectpath_test.go |  2 +-
 internal/aliases/aliases_test.go       | 21 ++++++++++++++-------
 2 files changed, 15 insertions(+), 8 deletions(-)

diff --git a/go/types/objectpath/objectpath_test.go b/go/types/objectpath/objectpath_test.go
index aa8ab5e0ea2..4691ba1f19a 100644
--- a/go/types/objectpath/objectpath_test.go
+++ b/go/types/objectpath/objectpath_test.go
@@ -34,7 +34,7 @@ func TestPaths(t *testing.T) {
 }
 
 func testPaths(t *testing.T, gotypesalias int) {
-	// override default set by go1.19 in go.mod
+	// override default set by go1.22 in go.mod
 	t.Setenv("GODEBUG", fmt.Sprintf("gotypesalias=%d", gotypesalias))
 
 	const src = `
diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go
index 0afee74c9e4..551e9e512f1 100644
--- a/internal/aliases/aliases_test.go
+++ b/internal/aliases/aliases_test.go
@@ -45,8 +45,8 @@ func TestNewAlias(t *testing.T) {
 
 	for _, godebug := range []string{
 		// The default gotypesalias value follows the x/tools/go.mod version
-		// The go.mod is at 1.19 so the default is gotypesalias=0.
-		// "", // Use the default GODEBUG value.
+		// The go.mod is at 1.22 so the default is gotypesalias=0.
+		"", // Use the default GODEBUG value (off).
 		"gotypesalias=0",
 		"gotypesalias=1",
 	} {
@@ -67,10 +67,14 @@ func TestNewAlias(t *testing.T) {
 				t.Errorf("Expected Unalias(A)==%q. got %q", want, got)
 			}
 
-			if godebug != "gotypesalias=0" {
-				if _, ok := A.Type().(*types.Alias); !ok {
-					t.Errorf("Expected A.Type() to be a types.Alias(). got %q", A.Type())
+			wantAlias := godebug == "gotypesalias=1"
+			_, gotAlias := A.Type().(*types.Alias)
+			if gotAlias != wantAlias {
+				verb := "to be"
+				if !wantAlias {
+					verb = "to not be"
 				}
+				t.Errorf("Expected A.Type() %s a types.Alias(). got %q", verb, A.Type())
 			}
 		})
 	}
@@ -83,9 +87,12 @@ func TestNewAlias(t *testing.T) {
 //
 // Requires gotypesalias GODEBUG and aliastypeparams GOEXPERIMENT.
 func TestNewParameterizedAlias(t *testing.T) {
-	testenv.NeedsGoExperiment(t, "aliastypeparams")
+	testenv.NeedsGo1Point(t, 23)
+	if testenv.Go1Point() == 23 {
+		testenv.NeedsGoExperiment(t, "aliastypeparams")
+	}
 
-	t.Setenv("GODEBUG", "gotypesalias=1") // needed until gotypesalias is removed (1.27).
+	t.Setenv("GODEBUG", "gotypesalias=1") // needed until gotypesalias is removed (1.27) or enabled by go.mod (1.23).
 	enabled := aliases.Enabled()
 	if !enabled {
 		t.Fatal("Need materialized aliases enabled")

From 75350b9b9987b80d40b3007d77780b0e127e7038 Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Thu, 26 Sep 2024 11:07:20 -0700
Subject: [PATCH 072/102] go/ssa: remove import of loader from builder_test.go

Unused import of loader in builder_test.go.

Change-Id: I518f7f1d866064bbb89d4690e9c5c182f80d1a47
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616175
Auto-Submit: Tim King 
Auto-Submit: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Alan Donovan 
---
 go/ssa/builder_test.go | 1 -
 1 file changed, 1 deletion(-)

diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go
index 0227b5d481d..f8baf77736d 100644
--- a/go/ssa/builder_test.go
+++ b/go/ssa/builder_test.go
@@ -25,7 +25,6 @@ import (
 	"golang.org/x/sync/errgroup"
 	"golang.org/x/tools/go/analysis/analysistest"
 	"golang.org/x/tools/go/expect"
-	"golang.org/x/tools/go/loader"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/go/ssa"
 	"golang.org/x/tools/go/ssa/ssautil"

From 3483a5ef405857d5d1453849fcaa2964469a9aa5 Mon Sep 17 00:00:00 2001
From: xieyuschen 
Date: Thu, 26 Sep 2024 23:33:31 +0800
Subject: [PATCH 073/102] go/packages: use link notation in comments for godoc
 rendering

fixes golang/go#69618

Change-Id: I7585b7cf92472b4ff1944ad3486cd930475143d1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615695
Reviewed-by: Michael Matloob 
Reviewed-by: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Alan Donovan 
---
 go/packages/doc.go      |  6 +++---
 go/packages/packages.go | 15 +++++++--------
 2 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/go/packages/doc.go b/go/packages/doc.go
index 3531ac8f5fc..50db5e7454e 100644
--- a/go/packages/doc.go
+++ b/go/packages/doc.go
@@ -64,7 +64,7 @@ graph using the Imports fields.
 
 The Load function can be configured by passing a pointer to a Config as
 the first argument. A nil Config is equivalent to the zero Config, which
-causes Load to run in LoadFiles mode, collecting minimal information.
+causes Load to run in [LoadFiles] mode, collecting minimal information.
 See the documentation for type Config for details.
 
 As noted earlier, the Config.Mode controls the amount of detail
@@ -72,14 +72,14 @@ reported about the loaded packages. See the documentation for type LoadMode
 for details.
 
 Most tools should pass their command-line arguments (after any flags)
-uninterpreted to [Load], so that it can interpret them
+uninterpreted to Load, so that it can interpret them
 according to the conventions of the underlying build system.
 
 See the Example function for typical usage.
 
 # The driver protocol
 
-[Load] may be used to load Go packages even in Go projects that use
+Load may be used to load Go packages even in Go projects that use
 alternative build systems, by installing an appropriate "driver"
 program for the build system and specifying its location in the
 GOPACKAGESDRIVER environment variable.
diff --git a/go/packages/packages.go b/go/packages/packages.go
index 0a2d40636dd..f227f1bab10 100644
--- a/go/packages/packages.go
+++ b/go/packages/packages.go
@@ -46,10 +46,10 @@ import (
 //
 // Unfortunately there are a number of open bugs related to
 // interactions among the LoadMode bits:
-// - https://github.com/golang/go/issues/56633
-// - https://github.com/golang/go/issues/56677
-// - https://github.com/golang/go/issues/58726
-// - https://github.com/golang/go/issues/63517
+//   - https://github.com/golang/go/issues/56633
+//   - https://github.com/golang/go/issues/56677
+//   - https://github.com/golang/go/issues/58726
+//   - https://github.com/golang/go/issues/63517
 type LoadMode int
 
 const (
@@ -248,14 +248,13 @@ type Config struct {
 
 // Load loads and returns the Go packages named by the given patterns.
 //
-// Config specifies loading options;
-// nil behaves the same as an empty Config.
+// The cfg parameter specifies loading options; nil behaves the same as an empty [Config].
 //
 // The [Config.Mode] field is a set of bits that determine what kinds
 // of information should be computed and returned. Modes that require
 // more information tend to be slower. See [LoadMode] for details
 // and important caveats. Its zero value is equivalent to
-// NeedName | NeedFiles | NeedCompiledGoFiles.
+// [NeedName] | [NeedFiles] | [NeedCompiledGoFiles].
 //
 // Each call to Load returns a new set of [Package] instances.
 // The Packages and their Imports form a directed acyclic graph.
@@ -272,7 +271,7 @@ type Config struct {
 // Errors associated with a particular package are recorded in the
 // corresponding Package's Errors list, and do not cause Load to
 // return an error. Clients may need to handle such errors before
-// proceeding with further analysis. The PrintErrors function is
+// proceeding with further analysis. The [PrintErrors] function is
 // provided for convenient display of all errors.
 func Load(cfg *Config, patterns ...string) ([]*Package, error) {
 	ld := newLoader(cfg)

From f05b5f456f7bba276d03c3f76ace90c60d9e0901 Mon Sep 17 00:00:00 2001
From: Alan Donovan 
Date: Thu, 26 Sep 2024 12:26:43 -0400
Subject: [PATCH 074/102] go/packages: document the role of PWD

Fixes golang/go#67757

Change-Id: I783fa88cfcfb77fdadc02a303cbaec4910f8b8fc
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616057
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Michael Matloob 
Auto-Submit: Alan Donovan 
---
 go/packages/doc.go | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/go/packages/doc.go b/go/packages/doc.go
index 50db5e7454e..f1931d10eeb 100644
--- a/go/packages/doc.go
+++ b/go/packages/doc.go
@@ -97,6 +97,15 @@ JSON-encoded [DriverRequest] message providing additional information
 is written to the driver's standard input. The driver must write a
 JSON-encoded [DriverResponse] message to its standard output. (This
 message differs from the JSON schema produced by 'go list'.)
+
+The value of the PWD environment variable seen by the driver process
+is the preferred name of its working directory. (The working directory
+may have other aliases due to symbolic links; see the comment on the
+Dir field of [exec.Cmd] for related information.)
+When the driver process emits in its response the name of a file
+that is a descendant of this directory, it must use an absolute path
+that has the value of PWD as a prefix, to ensure that the returned
+filenames satisfy the original query.
 */
 package packages // import "golang.org/x/tools/go/packages"
 

From 4fc0d79003330deeb6cbbb1b30f2981bd9183ade Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Wed, 25 Sep 2024 14:55:08 -0700
Subject: [PATCH 075/102] internal/gcimporter: remove goexperiment.unified tags

goexperiment.unified was removed in 1.21.

Change-Id: I388d657da578f33616c45b8a18a2266443956757
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615995
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Robert Findley 
Auto-Submit: Tim King 
---
 internal/gcimporter/gcimporter_test.go    | 59 ++++++++---------------
 internal/gcimporter/iexport_go118_test.go |  2 +-
 internal/gcimporter/unified_no.go         | 10 ----
 internal/gcimporter/unified_yes.go        | 10 ----
 4 files changed, 20 insertions(+), 61 deletions(-)
 delete mode 100644 internal/gcimporter/unified_no.go
 delete mode 100644 internal/gcimporter/unified_yes.go

diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go
index f23cc14d7fd..ef616148589 100644
--- a/internal/gcimporter/gcimporter_test.go
+++ b/internal/gcimporter/gcimporter_test.go
@@ -5,7 +5,7 @@
 // This file is a copy of $GOROOT/src/go/internal/gcimporter/gcimporter_test.go,
 // adjusted to make it build with code from (std lib) internal/testenv copied.
 
-package gcimporter
+package gcimporter_test
 
 import (
 	"bytes"
@@ -27,6 +27,7 @@ import (
 	"testing"
 	"time"
 
+	"golang.org/x/tools/internal/gcimporter"
 	"golang.org/x/tools/internal/goroot"
 	"golang.org/x/tools/internal/testenv"
 )
@@ -92,7 +93,7 @@ func compilePkg(t *testing.T, dirname, filename, outdirname string, packagefiles
 
 func testPath(t *testing.T, path, srcDir string) *types.Package {
 	t0 := time.Now()
-	pkg, err := Import(make(map[string]*types.Package), path, srcDir, nil)
+	pkg, err := gcimporter.Import(make(map[string]*types.Package), path, srcDir, nil)
 	if err != nil {
 		t.Errorf("testPath(%s): %s", path, err)
 		return nil
@@ -124,7 +125,7 @@ func TestImportTestdata(t *testing.T) {
 
 	packageFiles := map[string]string{}
 	for _, pkg := range []string{"go/ast", "go/token"} {
-		export, _ := FindPkg(pkg, "testdata")
+		export, _ := gcimporter.FindPkg(pkg, "testdata")
 		if export == "" {
 			t.Fatalf("no export data found for %s", pkg)
 		}
@@ -146,10 +147,7 @@ func TestImportTestdata(t *testing.T) {
 		// For now, we just test the presence of a few packages
 		// that we know are there for sure.
 		got := fmt.Sprint(pkg.Imports())
-		wants := []string{"go/ast", "go/token"}
-		if unifiedIR {
-			wants = []string{"go/ast"}
-		}
+		wants := []string{"go/ast", "go/token", "go/ast"}
 		for _, want := range wants {
 			if !strings.Contains(got, want) {
 				t.Errorf(`Package("exports").Imports() = %s, does not contain %s`, got, want)
@@ -181,17 +179,6 @@ func TestImportTypeparamTests(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	var skip map[string]string
-	if !unifiedIR {
-		// The Go 1.18 frontend still fails several cases.
-		skip = map[string]string{
-			"equal.go":      "inconsistent embedded sorting", // TODO(rfindley): investigate this.
-			"nested.go":     "fails to compile",              // TODO(rfindley): investigate this.
-			"issue47631.go": "can not handle local type declarations",
-			"issue55101.go": "fails to compile",
-		}
-	}
-
 	for _, entry := range list {
 		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
 			// For now, only consider standalone go files.
@@ -199,10 +186,6 @@ func TestImportTypeparamTests(t *testing.T) {
 		}
 
 		t.Run(entry.Name(), func(t *testing.T) {
-			if reason, ok := skip[entry.Name()]; ok {
-				t.Skip(reason)
-			}
-
 			filename := filepath.Join(rootDir, entry.Name())
 			src, err := os.ReadFile(filename)
 			if err != nil {
@@ -274,10 +257,6 @@ func checkFile(t *testing.T, filename string, src []byte) *types.Package {
 }
 
 func TestVersionHandling(t *testing.T) {
-	if debug {
-		t.Skip("TestVersionHandling panics in debug mode")
-	}
-
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
 
@@ -309,7 +288,7 @@ func TestVersionHandling(t *testing.T) {
 		}
 
 		// test that export data can be imported
-		_, err := Import(make(map[string]*types.Package), pkgpath, dir, nil)
+		_, err := gcimporter.Import(make(map[string]*types.Package), pkgpath, dir, nil)
 		if err != nil {
 			t.Errorf("import %q failed: %v", pkgpath, err)
 			continue
@@ -337,7 +316,7 @@ func TestVersionHandling(t *testing.T) {
 		os.WriteFile(filename, data, 0666)
 
 		// test that importing the corrupted file results in an error
-		_, err = Import(make(map[string]*types.Package), pkgpath, corruptdir, nil)
+		_, err = gcimporter.Import(make(map[string]*types.Package), pkgpath, corruptdir, nil)
 		if err == nil {
 			t.Errorf("import corrupted %q succeeded", pkgpath)
 		} else if msg := err.Error(); !strings.Contains(msg, "internal error") {
@@ -463,7 +442,7 @@ func importObject(t *testing.T, name string) types.Object {
 	importPath := s[0]
 	objName := s[1]
 
-	pkg, err := Import(make(map[string]*types.Package), importPath, ".", nil)
+	pkg, err := gcimporter.Import(make(map[string]*types.Package), importPath, ".", nil)
 	if err != nil {
 		t.Error(err)
 		return nil
@@ -545,7 +524,7 @@ func TestCorrectMethodPackage(t *testing.T) {
 	testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache
 
 	imports := make(map[string]*types.Package)
-	_, err := Import(imports, "net/http", ".", nil)
+	_, err := gcimporter.Import(imports, "net/http", ".", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -582,7 +561,7 @@ func TestIssue13566(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	jsonExport, _ := FindPkg("encoding/json", "testdata")
+	jsonExport, _ := gcimporter.FindPkg("encoding/json", "testdata")
 	if jsonExport == "" {
 		t.Fatalf("no export data found for encoding/json")
 	}
@@ -608,7 +587,7 @@ func TestIssue13898(t *testing.T) {
 
 	// import go/internal/gcimporter which imports go/types partially
 	imports := make(map[string]*types.Package)
-	_, err := Import(imports, "go/internal/gcimporter", ".", nil)
+	_, err := gcimporter.Import(imports, "go/internal/gcimporter", ".", nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -673,7 +652,7 @@ func TestIssue15517(t *testing.T) {
 	// The same issue occurs with vendoring.)
 	imports := make(map[string]*types.Package)
 	for i := 0; i < 3; i++ {
-		if _, err := Import(imports, "./././testdata/p", tmpdir, nil); err != nil {
+		if _, err := gcimporter.Import(imports, "./././testdata/p", tmpdir, nil); err != nil {
 			t.Fatal(err)
 		}
 	}
@@ -781,14 +760,14 @@ type K = StillBad[string]
 	}
 
 	// Export it. (Shallowness isn't important here.)
-	data, err := IExportShallow(fset, pkg1, nil)
+	data, err := gcimporter.IExportShallow(fset, pkg1, nil)
 	if err != nil {
 		t.Fatalf("export: %v", err) // any failure to export is a bug
 	}
 
 	// Re-import it.
 	imports := make(map[string]*types.Package)
-	pkg2, err := IImportShallow(fset, GetPackagesFromMap(imports), data, "p", nil)
+	pkg2, err := gcimporter.IImportShallow(fset, gcimporter.GetPackagesFromMap(imports), data, "p", nil)
 	if err != nil {
 		t.Fatalf("import: %v", err) // any failure of IExport+IImport is a bug.
 	}
@@ -878,14 +857,14 @@ func TestExportInvalid(t *testing.T) {
 
 			// Export it.
 			// (Shallowness isn't important here.)
-			data, err := IExportShallow(fset, pkg1, nil)
+			data, err := gcimporter.IExportShallow(fset, pkg1, nil)
 			if err != nil {
 				t.Fatalf("export: %v", err) // any failure to export is a bug
 			}
 
 			// Re-import it.
 			imports := make(map[string]*types.Package)
-			pkg2, err := IImportShallow(fset, GetPackagesFromMap(imports), data, "p", nil)
+			pkg2, err := gcimporter.IImportShallow(fset, gcimporter.GetPackagesFromMap(imports), data, "p", nil)
 			if err != nil {
 				t.Fatalf("import: %v", err) // any failure of IExport+IImport is a bug.
 			}
@@ -939,7 +918,7 @@ func TestIssue58296(t *testing.T) {
 	}
 
 	// make sure a and b are both imported by c.
-	pkg, err := Import(imports, "./c", testoutdir, nil)
+	pkg, err := gcimporter.Import(imports, "./c", testoutdir, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -982,7 +961,7 @@ func TestIssueAliases(t *testing.T) {
 	)
 
 	// import c from gc export data using a and b.
-	pkg, err := Import(map[string]*types.Package{
+	pkg, err := gcimporter.Import(map[string]*types.Package{
 		apkg: types.NewPackage(apkg, "a"),
 		bpkg: types.NewPackage(bpkg, "b"),
 	}, "./c", testoutdir, nil)
@@ -1026,7 +1005,7 @@ func apkg(testoutdir string) string {
 }
 
 func importPkg(t *testing.T, path, srcDir string) *types.Package {
-	pkg, err := Import(make(map[string]*types.Package), path, srcDir, nil)
+	pkg, err := gcimporter.Import(make(map[string]*types.Package), path, srcDir, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/internal/gcimporter/iexport_go118_test.go b/internal/gcimporter/iexport_go118_test.go
index 8b291972032..2f8eafd6705 100644
--- a/internal/gcimporter/iexport_go118_test.go
+++ b/internal/gcimporter/iexport_go118_test.go
@@ -96,7 +96,7 @@ func testExportSrc(t *testing.T, src []byte) {
 	testPkgData(t, fset, version, pkg, data)
 }
 
-func TestImportTypeparamTests(t *testing.T) {
+func TestIndexedImportTypeparamTests(t *testing.T) {
 	testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache
 
 	// Check go files in test/typeparam.
diff --git a/internal/gcimporter/unified_no.go b/internal/gcimporter/unified_no.go
deleted file mode 100644
index 38b624cadab..00000000000
--- a/internal/gcimporter/unified_no.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// 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.
-
-//go:build !goexperiment.unified
-// +build !goexperiment.unified
-
-package gcimporter
-
-const unifiedIR = false
diff --git a/internal/gcimporter/unified_yes.go b/internal/gcimporter/unified_yes.go
deleted file mode 100644
index b5118d0b3a5..00000000000
--- a/internal/gcimporter/unified_yes.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// 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.
-
-//go:build goexperiment.unified
-// +build goexperiment.unified
-
-package gcimporter
-
-const unifiedIR = true

From c59bc300b24fa84ed20ac1ebc68ec449113cf902 Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Wed, 25 Sep 2024 14:55:08 -0700
Subject: [PATCH 076/102] internal/gcimporter: vary the value of any in
 predeclared

Vary the types in predeclared() based on the runtime value of any
in types.Universe (either universeAnyNoAlias or universeAnyAlias).

Change-Id: Iacf43d21c7d078706ac20ded7ffdeadffbb4baf5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615683
Commit-Queue: Tim King 
Reviewed-by: Robert Findley 
LUCI-TryBot-Result: Go LUCI 
---
 internal/gcimporter/bimport.go         | 66 -------------------
 internal/gcimporter/gcimporter_test.go | 75 +++++++--------------
 internal/gcimporter/iexport_test.go    |  9 ++-
 internal/gcimporter/predeclared.go     | 91 ++++++++++++++++++++++++++
 4 files changed, 123 insertions(+), 118 deletions(-)
 create mode 100644 internal/gcimporter/predeclared.go

diff --git a/internal/gcimporter/bimport.go b/internal/gcimporter/bimport.go
index 1308ed3c3a1..d79a605ed13 100644
--- a/internal/gcimporter/bimport.go
+++ b/internal/gcimporter/bimport.go
@@ -87,69 +87,3 @@ func chanDir(d int) types.ChanDir {
 		return 0
 	}
 }
-
-var predeclOnce sync.Once
-var predecl []types.Type // initialized lazily
-
-func predeclared() []types.Type {
-	predeclOnce.Do(func() {
-		// initialize lazily to be sure that all
-		// elements have been initialized before
-		predecl = []types.Type{ // basic types
-			types.Typ[types.Bool],
-			types.Typ[types.Int],
-			types.Typ[types.Int8],
-			types.Typ[types.Int16],
-			types.Typ[types.Int32],
-			types.Typ[types.Int64],
-			types.Typ[types.Uint],
-			types.Typ[types.Uint8],
-			types.Typ[types.Uint16],
-			types.Typ[types.Uint32],
-			types.Typ[types.Uint64],
-			types.Typ[types.Uintptr],
-			types.Typ[types.Float32],
-			types.Typ[types.Float64],
-			types.Typ[types.Complex64],
-			types.Typ[types.Complex128],
-			types.Typ[types.String],
-
-			// basic type aliases
-			types.Universe.Lookup("byte").Type(),
-			types.Universe.Lookup("rune").Type(),
-
-			// error
-			types.Universe.Lookup("error").Type(),
-
-			// untyped types
-			types.Typ[types.UntypedBool],
-			types.Typ[types.UntypedInt],
-			types.Typ[types.UntypedRune],
-			types.Typ[types.UntypedFloat],
-			types.Typ[types.UntypedComplex],
-			types.Typ[types.UntypedString],
-			types.Typ[types.UntypedNil],
-
-			// package unsafe
-			types.Typ[types.UnsafePointer],
-
-			// invalid type
-			types.Typ[types.Invalid], // only appears in packages with errors
-
-			// used internally by gc; never used by this package or in .a files
-			anyType{},
-
-			// comparable
-			types.Universe.Lookup("comparable").Type(),
-
-			// any
-			types.Universe.Lookup("any").Type(),
-		}
-	})
-	return predecl
-}
-
-type anyType struct{}
-
-func (t anyType) Underlying() types.Type { return t }
-func (t anyType) String() string         { return "any" }
diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go
index ef616148589..44a3db78f41 100644
--- a/internal/gcimporter/gcimporter_test.go
+++ b/internal/gcimporter/gcimporter_test.go
@@ -542,12 +542,7 @@ func TestIssue13566(t *testing.T) {
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
 	testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache
-
-	// On windows, we have to set the -D option for the compiler to avoid having a drive
-	// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
-	if runtime.GOOS == "windows" {
-		t.Skip("avoid dealing with relative paths/drive letters on windows")
-	}
+	skipWindows(t)
 
 	tmpdir := mktmpdir(t)
 	defer os.RemoveAll(tmpdir)
@@ -626,12 +621,7 @@ func TestIssue13898(t *testing.T) {
 func TestIssue15517(t *testing.T) {
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
-
-	// On windows, we have to set the -D option for the compiler to avoid having a drive
-	// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
-	if runtime.GOOS == "windows" {
-		t.Skip("avoid dealing with relative paths/drive letters on windows")
-	}
+	skipWindows(t)
 
 	tmpdir := mktmpdir(t)
 	defer os.RemoveAll(tmpdir)
@@ -661,12 +651,7 @@ func TestIssue15517(t *testing.T) {
 func TestIssue15920(t *testing.T) {
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
-
-	// On windows, we have to set the -D option for the compiler to avoid having a drive
-	// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
-	if runtime.GOOS == "windows" {
-		t.Skip("avoid dealing with relative paths/drive letters on windows")
-	}
+	skipWindows(t)
 
 	compileAndImportPkg(t, "issue15920")
 }
@@ -674,12 +659,7 @@ func TestIssue15920(t *testing.T) {
 func TestIssue20046(t *testing.T) {
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
-
-	// On windows, we have to set the -D option for the compiler to avoid having a drive
-	// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
-	if runtime.GOOS == "windows" {
-		t.Skip("avoid dealing with relative paths/drive letters on windows")
-	}
+	skipWindows(t)
 
 	// "./issue20046".V.M must exist
 	pkg := compileAndImportPkg(t, "issue20046")
@@ -692,12 +672,7 @@ func TestIssue20046(t *testing.T) {
 func TestIssue25301(t *testing.T) {
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
-
-	// On windows, we have to set the -D option for the compiler to avoid having a drive
-	// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
-	if runtime.GOOS == "windows" {
-		t.Skip("avoid dealing with relative paths/drive letters on windows")
-	}
+	skipWindows(t)
 
 	compileAndImportPkg(t, "issue25301")
 }
@@ -705,12 +680,7 @@ func TestIssue25301(t *testing.T) {
 func TestIssue51836(t *testing.T) {
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
-
-	// On windows, we have to set the -D option for the compiler to avoid having a drive
-	// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
-	if runtime.GOOS == "windows" {
-		t.Skip("avoid dealing with relative paths/drive letters on windows")
-	}
+	skipWindows(t)
 
 	tmpdir := mktmpdir(t)
 	defer os.RemoveAll(tmpdir)
@@ -798,12 +768,7 @@ type K = StillBad[string]
 func TestIssue57015(t *testing.T) {
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
-
-	// On windows, we have to set the -D option for the compiler to avoid having a drive
-	// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
-	if runtime.GOOS == "windows" {
-		t.Skip("avoid dealing with relative paths/drive letters on windows")
-	}
+	skipWindows(t)
 
 	compileAndImportPkg(t, "issue57015")
 }
@@ -890,12 +855,7 @@ func TestIssue58296(t *testing.T) {
 	// This package only handles gc export data.
 	needsCompiler(t, "gc")
 	testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache
-
-	// On windows, we have to set the -D option for the compiler to avoid having a drive
-	// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
-	if runtime.GOOS == "windows" {
-		t.Skip("avoid dealing with relative paths/drive letters on windows")
-	}
+	skipWindows(t)
 
 	tmpdir := mktmpdir(t)
 	defer os.RemoveAll(tmpdir)
@@ -939,9 +899,9 @@ func TestIssueAliases(t *testing.T) {
 	testenv.NeedsGo1Point(t, 24)
 	needsCompiler(t, "gc")
 	testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache
-	testenv.NeedsGoExperiment(t, "aliastypeparams")
+	skipWindows(t)
 
-	t.Setenv("GODEBUG", fmt.Sprintf("gotypesalias=%d", 1))
+	t.Setenv("GODEBUG", aliasesOn)
 
 	tmpdir := mktmpdir(t)
 	defer os.RemoveAll(tmpdir)
@@ -1026,3 +986,18 @@ func lookupObj(t *testing.T, scope *types.Scope, name string) types.Object {
 	t.Fatalf("%s not found", name)
 	return nil
 }
+
+// skipWindows skips the test on windows.
+//
+// On windows, we have to set the -D option for the compiler to avoid having a drive
+// letter and an illegal ':' in the import path - just skip it (see also issue #3483).
+func skipWindows(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		t.Skip("avoid dealing with relative paths/drive letters on windows")
+	}
+}
+
+const (
+	aliasesOff = "gotypesalias=0" // default GODEBUG in 1.22 (like x/tools)
+	aliasesOn  = "gotypesalias=1" // default after 1.23
+)
diff --git a/internal/gcimporter/iexport_test.go b/internal/gcimporter/iexport_test.go
index 501d53476df..66d28457886 100644
--- a/internal/gcimporter/iexport_test.go
+++ b/internal/gcimporter/iexport_test.go
@@ -420,9 +420,12 @@ func (f importerFunc) Import(path string) (*types.Package, error) { return f(pat
 // on both declarations and uses of type parameterized aliases.
 func TestIExportDataTypeParameterizedAliases(t *testing.T) {
 	testenv.NeedsGo1Point(t, 23)
+	skipWindows(t)
+	if testenv.Go1Point() == 23 {
+		testenv.NeedsGoExperiment(t, "aliastypeparams") // testenv.Go1Point() >= 24 implies aliastypeparams=1
+	}
 
-	testenv.NeedsGoExperiment(t, "aliastypeparams")
-	t.Setenv("GODEBUG", "gotypesalias=1")
+	t.Setenv("GODEBUG", aliasesOn)
 
 	// High level steps:
 	// * parse  and typecheck
@@ -476,6 +479,8 @@ type Chained = C[Named] // B[Named, A[Named]] = B[Named, *Named] = []*Named
 		// This means that it can be loaded by go/importer or go/types.
 		// This step is not supported, but it does give test coverage for stdlib.
 		"goroot": func(t *testing.T) *types.Package {
+			testenv.NeedsGo1Point(t, 24) // requires >= 1.24 go/importer.
+
 			// Write indexed export data file contents.
 			//
 			// TODO(taking): Slightly unclear to what extent this step should be supported by go/importer.
diff --git a/internal/gcimporter/predeclared.go b/internal/gcimporter/predeclared.go
new file mode 100644
index 00000000000..907c8557a54
--- /dev/null
+++ b/internal/gcimporter/predeclared.go
@@ -0,0 +1,91 @@
+// Copyright 2024 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 gcimporter
+
+import (
+	"go/types"
+	"sync"
+)
+
+// predecl is a cache for the predeclared types in types.Universe.
+//
+// Cache a distinct result based on the runtime value of any.
+// The pointer value of the any type varies based on GODEBUG settings.
+var predeclMu sync.Mutex
+var predecl map[types.Type][]types.Type
+
+func predeclared() []types.Type {
+	anyt := types.Universe.Lookup("any").Type()
+
+	predeclMu.Lock()
+	defer predeclMu.Unlock()
+
+	if pre, ok := predecl[anyt]; ok {
+		return pre
+	}
+
+	if predecl == nil {
+		predecl = make(map[types.Type][]types.Type)
+	}
+
+	decls := []types.Type{ // basic types
+		types.Typ[types.Bool],
+		types.Typ[types.Int],
+		types.Typ[types.Int8],
+		types.Typ[types.Int16],
+		types.Typ[types.Int32],
+		types.Typ[types.Int64],
+		types.Typ[types.Uint],
+		types.Typ[types.Uint8],
+		types.Typ[types.Uint16],
+		types.Typ[types.Uint32],
+		types.Typ[types.Uint64],
+		types.Typ[types.Uintptr],
+		types.Typ[types.Float32],
+		types.Typ[types.Float64],
+		types.Typ[types.Complex64],
+		types.Typ[types.Complex128],
+		types.Typ[types.String],
+
+		// basic type aliases
+		types.Universe.Lookup("byte").Type(),
+		types.Universe.Lookup("rune").Type(),
+
+		// error
+		types.Universe.Lookup("error").Type(),
+
+		// untyped types
+		types.Typ[types.UntypedBool],
+		types.Typ[types.UntypedInt],
+		types.Typ[types.UntypedRune],
+		types.Typ[types.UntypedFloat],
+		types.Typ[types.UntypedComplex],
+		types.Typ[types.UntypedString],
+		types.Typ[types.UntypedNil],
+
+		// package unsafe
+		types.Typ[types.UnsafePointer],
+
+		// invalid type
+		types.Typ[types.Invalid], // only appears in packages with errors
+
+		// used internally by gc; never used by this package or in .a files
+		anyType{},
+
+		// comparable
+		types.Universe.Lookup("comparable").Type(),
+
+		// any
+		anyt,
+	}
+
+	predecl[anyt] = decls
+	return decls
+}
+
+type anyType struct{}
+
+func (t anyType) Underlying() types.Type { return t }
+func (t anyType) String() string         { return "any" }

From e55961d46ceb7aac2513cb859c23942ec2ce11db Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Thu, 26 Sep 2024 14:18:48 -0700
Subject: [PATCH 077/102] internal/gcimporter: use types.NewInterfaceType

Use types.NewInterfaceType directly. This has been available since Go 1.11.

Change-Id: I3669ce485bc99a758530158db02a470d81ad7b85
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615697
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Alan Donovan 
Commit-Queue: Tim King 
---
 internal/gcimporter/iimport.go        |  2 +-
 internal/gcimporter/newInterface10.go | 22 ----------------------
 internal/gcimporter/newInterface11.go | 14 --------------
 3 files changed, 1 insertion(+), 37 deletions(-)
 delete mode 100644 internal/gcimporter/newInterface10.go
 delete mode 100644 internal/gcimporter/newInterface11.go

diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go
index d1511c3244b..21908a158b4 100644
--- a/internal/gcimporter/iimport.go
+++ b/internal/gcimporter/iimport.go
@@ -960,7 +960,7 @@ func (r *importReader) doType(base *types.Named) (res types.Type) {
 			methods[i] = method
 		}
 
-		typ := newInterface(methods, embeddeds)
+		typ := types.NewInterfaceType(methods, embeddeds)
 		r.p.interfaceList = append(r.p.interfaceList, typ)
 		return typ
 
diff --git a/internal/gcimporter/newInterface10.go b/internal/gcimporter/newInterface10.go
deleted file mode 100644
index 8b163e3d058..00000000000
--- a/internal/gcimporter/newInterface10.go
+++ /dev/null
@@ -1,22 +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.
-
-//go:build !go1.11
-// +build !go1.11
-
-package gcimporter
-
-import "go/types"
-
-func newInterface(methods []*types.Func, embeddeds []types.Type) *types.Interface {
-	named := make([]*types.Named, len(embeddeds))
-	for i, e := range embeddeds {
-		var ok bool
-		named[i], ok = e.(*types.Named)
-		if !ok {
-			panic("embedding of non-defined interfaces in interfaces is not supported before Go 1.11")
-		}
-	}
-	return types.NewInterface(methods, named)
-}
diff --git a/internal/gcimporter/newInterface11.go b/internal/gcimporter/newInterface11.go
deleted file mode 100644
index 49984f40fd8..00000000000
--- a/internal/gcimporter/newInterface11.go
+++ /dev/null
@@ -1,14 +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.
-
-//go:build go1.11
-// +build go1.11
-
-package gcimporter
-
-import "go/types"
-
-func newInterface(methods []*types.Func, embeddeds []types.Type) *types.Interface {
-	return types.NewInterfaceType(methods, embeddeds)
-}

From ab64376303c1e9b164850af43d539366c456963b Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Thu, 26 Sep 2024 14:35:08 -0700
Subject: [PATCH 078/102] go/types/objectpath: skip package tests with wasip1

Use testfiles.LoadPackages to load the packages.

Change-Id: Id740fb03440fce079c63bed00b33da15770f133b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616158
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Alan Donovan 
Reviewed-by: Alan Donovan 
Commit-Queue: Alan Donovan 
---
 go/types/objectpath/objectpath_test.go | 18 +-----------------
 1 file changed, 1 insertion(+), 17 deletions(-)

diff --git a/go/types/objectpath/objectpath_test.go b/go/types/objectpath/objectpath_test.go
index 4691ba1f19a..838f1be44df 100644
--- a/go/types/objectpath/objectpath_test.go
+++ b/go/types/objectpath/objectpath_test.go
@@ -198,23 +198,7 @@ type T struct{x, y int}
 func loadPackages(t *testing.T, archive string, patterns ...string) map[string]*packages.Package {
 	// TODO(adonovan): ExtractTxtarToTmp (sans File) would be useful.
 	ar := txtar.Parse([]byte(archive))
-	fs, err := txtar.FS(ar)
-	if err != nil {
-		t.Fatal(err)
-	}
-	dir := testfiles.CopyToTmp(t, fs)
-
-	cfg := packages.Config{
-		Mode: packages.LoadAllSyntax,
-		Dir:  dir,
-	}
-	pkgs, err := packages.Load(&cfg, patterns...)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if packages.PrintErrors(pkgs) > 0 {
-		t.Fatal("Load: there were errors")
-	}
+	pkgs := testfiles.LoadPackages(t, ar, patterns...)
 	m := make(map[string]*packages.Package)
 	packages.Visit(pkgs, nil, func(pkg *packages.Package) {
 		m[pkg.Types.Path()] = pkg

From edfeacf5cbff382810f1ec4871e3cc75d3aa6add Mon Sep 17 00:00:00 2001
From: Rob Findley 
Date: Thu, 26 Sep 2024 20:21:52 +0000
Subject: [PATCH 079/102] internal/golang: fix panic in signatureHelp over
 builtin name

CL 605983 made it such that signatureHelp succeeds over the function
name of a call expression, but the builtin codepath did not handle a nil
CallExpr. Fix this, with a test. Also improve the @signature marker
errors.

Fixes golang/go#69552

Change-Id: I818c7cbdb1a8c768466f0dcbaddbc8a45a681add
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616216
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Alan Donovan 
Auto-Submit: Robert Findley 
---
 gopls/internal/golang/signature_help.go            |  7 ++++++-
 gopls/internal/test/marker/marker_test.go          | 14 ++++++++++----
 .../test/marker/testdata/signature/issue69552.txt  | 14 ++++++++++++++
 3 files changed, 30 insertions(+), 5 deletions(-)
 create mode 100644 gopls/internal/test/marker/testdata/signature/issue69552.txt

diff --git a/gopls/internal/golang/signature_help.go b/gopls/internal/golang/signature_help.go
index 26cb92c643b..dfdce041ff6 100644
--- a/gopls/internal/golang/signature_help.go
+++ b/gopls/internal/golang/signature_help.go
@@ -171,6 +171,8 @@ loop:
 	}, activeParam, nil
 }
 
+// Note: callExpr may be nil when signatureHelp is invoked outside the call
+// argument list (golang/go#69552).
 func builtinSignature(ctx context.Context, snapshot *cache.Snapshot, callExpr *ast.CallExpr, name string, pos token.Pos) (*protocol.SignatureInformation, int, error) {
 	sig, err := NewBuiltinSignature(ctx, snapshot, name)
 	if err != nil {
@@ -180,7 +182,10 @@ func builtinSignature(ctx context.Context, snapshot *cache.Snapshot, callExpr *a
 	for _, p := range sig.params {
 		paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p})
 	}
-	activeParam := activeParameter(callExpr, len(sig.params), sig.variadic, pos)
+	activeParam := 0
+	if callExpr != nil {
+		activeParam = activeParameter(callExpr, len(sig.params), sig.variadic, pos)
+	}
 	return &protocol.SignatureInformation{
 		Label:         sig.name + sig.Format(),
 		Documentation: stringToSigInfoDocumentation(sig.doc, snapshot.Options()),
diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go
index 1896d8fb19f..e566f922ac9 100644
--- a/gopls/internal/test/marker/marker_test.go
+++ b/gopls/internal/test/marker/marker_test.go
@@ -1781,20 +1781,26 @@ func tokenMarker(mark marker, loc protocol.Location, tokenType, mod string) {
 
 func signatureMarker(mark marker, src protocol.Location, label string, active int64) {
 	got := mark.run.env.SignatureHelp(src)
+	var gotLabels []string // for better error messages
+	if got != nil {
+		for _, s := range got.Signatures {
+			gotLabels = append(gotLabels, s.Label)
+		}
+	}
 	if label == "" {
 		// A null result is expected.
 		// (There's no point having a @signatureerr marker
 		// because the server handler suppresses all errors.)
-		if got != nil && len(got.Signatures) > 0 {
-			mark.errorf("signatureHelp = %v, want 0 signatures", got)
+		if got != nil && len(gotLabels) > 0 {
+			mark.errorf("signatureHelp = %v, want 0 signatures", gotLabels)
 		}
 		return
 	}
 	if got == nil || len(got.Signatures) != 1 {
-		mark.errorf("signatureHelp = %v, want exactly 1 signature", got)
+		mark.errorf("signatureHelp = %v, want exactly 1 signature", gotLabels)
 		return
 	}
-	if got := got.Signatures[0].Label; got != label {
+	if got := gotLabels[0]; got != label {
 		mark.errorf("signatureHelp: got label %q, want %q", got, label)
 	}
 	if got := int64(got.ActiveParameter); got != active {
diff --git a/gopls/internal/test/marker/testdata/signature/issue69552.txt b/gopls/internal/test/marker/testdata/signature/issue69552.txt
new file mode 100644
index 00000000000..22ecda07341
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/signature/issue69552.txt
@@ -0,0 +1,14 @@
+Regresson test for #69552: panic in activeParam of a builtin, when requesting
+signature help outside of the argument list.
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- a/a.go --
+package a
+
+func _() {
+	_ = len([]int{}) //@signature("en", "len(v Type) int", 0)
+}
+

From 8b6849d85a7e3684220093888ff9ee071594c76b Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Thu, 26 Sep 2024 15:49:27 -0700
Subject: [PATCH 080/102] go/ssa/interp: reenable lifetime tests.

For golang/go#68658

Change-Id: I6e05e21f924cda18e368543e59071b2c79c16ef2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615698
Commit-Queue: Tim King 
Reviewed-by: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
---
 go/ssa/interp/interp_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go
index c6d1b058456..90f318c231b 100644
--- a/go/ssa/interp/interp_test.go
+++ b/go/ssa/interp/interp_test.go
@@ -117,7 +117,7 @@ var testdataTests = []string{
 	"deepequal.go",
 	"defer.go",
 	"fieldprom.go",
-	// "forvarlifetime_old.go", Disabled for golang.org/cl/603895. Fix and re-enable.
+	"forvarlifetime_old.go",
 	"ifaceconv.go",
 	"ifaceprom.go",
 	"initorder.go",
@@ -129,7 +129,7 @@ var testdataTests = []string{
 	"slice2arrayptr.go",
 	"static.go",
 	"width32.go",
-	// "rangevarlifetime_old.go", Disabled for golang.org/cl/603895. Fix and re-enable.
+	"rangevarlifetime_old.go",
 	"fixedbugs/issue52342.go",
 	"fixedbugs/issue55115.go",
 	"fixedbugs/issue52835.go",

From c6f2f8edba4a9d5be665bb151d141365e03eeb6e Mon Sep 17 00:00:00 2001
From: Alan Donovan 
Date: Thu, 26 Sep 2024 15:12:18 -0400
Subject: [PATCH 081/102] refactor/eg: rewrite test without go/loader

Now, each template, its inputs and outputs are all
packages within a single .txtar archive.

Updates golang/go#69556

Change-Id: I95285487597df985a16879ea1f5cb6a75c77fa12
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616215
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Alan Donovan 
Reviewed-by: Tim King 
---
 refactor/eg/eg_test.go                        | 227 +++++++++---------
 refactor/eg/testdata/A.template               |  11 -
 refactor/eg/testdata/A1.go                    |  49 ----
 refactor/eg/testdata/A1.golden                |  50 ----
 refactor/eg/testdata/A2.go                    |  10 -
 refactor/eg/testdata/A2.golden                |  13 -
 refactor/eg/testdata/B.template               |   9 -
 refactor/eg/testdata/B1.go                    |  15 --
 refactor/eg/testdata/B1.golden                |  15 --
 refactor/eg/testdata/C.template               |  10 -
 refactor/eg/testdata/C1.go                    |  20 --
 refactor/eg/testdata/C1.golden                |  20 --
 refactor/eg/testdata/D.template               |   8 -
 refactor/eg/testdata/D1.go                    |  10 -
 refactor/eg/testdata/D1.golden                |  10 -
 refactor/eg/testdata/E.template               |  12 -
 refactor/eg/testdata/E1.go                    |   7 -
 refactor/eg/testdata/E1.golden                |  11 -
 refactor/eg/testdata/F.template               |   8 -
 refactor/eg/testdata/F1.go                    |  46 ----
 refactor/eg/testdata/F1.golden                |  46 ----
 refactor/eg/testdata/G.template               |   9 -
 refactor/eg/testdata/G1.go                    |  10 -
 refactor/eg/testdata/G1.golden                |  10 -
 refactor/eg/testdata/H.template               |   9 -
 refactor/eg/testdata/H1.go                    |  10 -
 refactor/eg/testdata/H1.golden                |  10 -
 refactor/eg/testdata/I.template               |  12 -
 refactor/eg/testdata/I1.go                    |   7 -
 refactor/eg/testdata/I1.golden                |  12 -
 refactor/eg/testdata/J.template               |   9 -
 refactor/eg/testdata/J1.go                    |   8 -
 refactor/eg/testdata/J1.golden                |   9 -
 refactor/eg/testdata/a.txtar                  | 147 ++++++++++++
 refactor/eg/testdata/b.txtar                  |  49 ++++
 .../{bad_type.template => bad_type.txtar}     |   6 +
 refactor/eg/testdata/c.txtar                  |  60 +++++
 refactor/eg/testdata/d.txtar                  |  38 +++
 refactor/eg/testdata/e.txtar                  |  40 +++
 ...atch.template => expr_type_mismatch.txtar} |   7 +
 refactor/eg/testdata/f.txtar                  | 110 +++++++++
 refactor/eg/testdata/g.txtar                  |  39 +++
 refactor/eg/testdata/h.txtar                  |  39 +++
 refactor/eg/testdata/i.txtar                  |  41 ++++
 refactor/eg/testdata/j.txtar                  |  36 +++
 ..._return.template => no_after_return.txtar} |   7 +
 .../{no_before.template => no_before.txtar}   |   7 +
 ..._mismatch.template => type_mismatch.txtar} |   7 +
 48 files changed, 745 insertions(+), 610 deletions(-)
 delete mode 100644 refactor/eg/testdata/A.template
 delete mode 100644 refactor/eg/testdata/A1.go
 delete mode 100644 refactor/eg/testdata/A1.golden
 delete mode 100644 refactor/eg/testdata/A2.go
 delete mode 100644 refactor/eg/testdata/A2.golden
 delete mode 100644 refactor/eg/testdata/B.template
 delete mode 100644 refactor/eg/testdata/B1.go
 delete mode 100644 refactor/eg/testdata/B1.golden
 delete mode 100644 refactor/eg/testdata/C.template
 delete mode 100644 refactor/eg/testdata/C1.go
 delete mode 100644 refactor/eg/testdata/C1.golden
 delete mode 100644 refactor/eg/testdata/D.template
 delete mode 100644 refactor/eg/testdata/D1.go
 delete mode 100644 refactor/eg/testdata/D1.golden
 delete mode 100644 refactor/eg/testdata/E.template
 delete mode 100644 refactor/eg/testdata/E1.go
 delete mode 100644 refactor/eg/testdata/E1.golden
 delete mode 100644 refactor/eg/testdata/F.template
 delete mode 100644 refactor/eg/testdata/F1.go
 delete mode 100644 refactor/eg/testdata/F1.golden
 delete mode 100644 refactor/eg/testdata/G.template
 delete mode 100644 refactor/eg/testdata/G1.go
 delete mode 100644 refactor/eg/testdata/G1.golden
 delete mode 100644 refactor/eg/testdata/H.template
 delete mode 100644 refactor/eg/testdata/H1.go
 delete mode 100644 refactor/eg/testdata/H1.golden
 delete mode 100644 refactor/eg/testdata/I.template
 delete mode 100644 refactor/eg/testdata/I1.go
 delete mode 100644 refactor/eg/testdata/I1.golden
 delete mode 100644 refactor/eg/testdata/J.template
 delete mode 100644 refactor/eg/testdata/J1.go
 delete mode 100644 refactor/eg/testdata/J1.golden
 create mode 100644 refactor/eg/testdata/a.txtar
 create mode 100644 refactor/eg/testdata/b.txtar
 rename refactor/eg/testdata/{bad_type.template => bad_type.txtar} (75%)
 create mode 100644 refactor/eg/testdata/c.txtar
 create mode 100644 refactor/eg/testdata/d.txtar
 create mode 100644 refactor/eg/testdata/e.txtar
 rename refactor/eg/testdata/{expr_type_mismatch.template => expr_type_mismatch.txtar} (88%)
 create mode 100644 refactor/eg/testdata/f.txtar
 create mode 100644 refactor/eg/testdata/g.txtar
 create mode 100644 refactor/eg/testdata/h.txtar
 create mode 100644 refactor/eg/testdata/i.txtar
 create mode 100644 refactor/eg/testdata/j.txtar
 rename refactor/eg/testdata/{no_after_return.template => no_after_return.txtar} (56%)
 rename refactor/eg/testdata/{no_before.template => no_before.txtar} (56%)
 rename refactor/eg/testdata/{type_mismatch.template => type_mismatch.txtar} (64%)

diff --git a/refactor/eg/eg_test.go b/refactor/eg/eg_test.go
index 36fd3add8a7..eb54f0b3f95 100644
--- a/refactor/eg/eg_test.go
+++ b/refactor/eg/eg_test.go
@@ -12,21 +12,22 @@ package eg_test
 import (
 	"bytes"
 	"flag"
-	"go/build"
+	"fmt"
 	"go/constant"
-	"go/parser"
-	"go/token"
+	"go/format"
 	"go/types"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"runtime"
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/go/loader"
+	"github.com/google/go-cmp/cmp"
+	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/internal/testenv"
+	"golang.org/x/tools/internal/testfiles"
 	"golang.org/x/tools/refactor/eg"
+	"golang.org/x/tools/txtar"
 )
 
 // TODO(adonovan): more tests:
@@ -41,80 +42,64 @@ var (
 )
 
 func Test(t *testing.T) {
-	testenv.NeedsTool(t, "go")
+	testenv.NeedsGoPackages(t)
 
 	switch runtime.GOOS {
 	case "windows":
 		t.Skipf("skipping test on %q (no /usr/bin/diff)", runtime.GOOS)
 	}
 
-	ctx := build.Default   // copy
-	ctx.CgoEnabled = false // don't use cgo
-	conf := loader.Config{
-		Fset:       token.NewFileSet(),
-		ParserMode: parser.ParseComments,
-		Build:      &ctx,
-	}
-
-	// Each entry is a single-file package.
-	// (Multi-file packages aren't interesting for this test.)
-	// Order matters: each non-template package is processed using
-	// the preceding template package.
+	// Each txtar defines a package example.com/template and zero
+	// or more input packages example.com/in/... on which to apply
+	// it. The outputs are compared with the corresponding files
+	// in example.com/out/...
 	for _, filename := range []string{
-		"testdata/A.template",
-		"testdata/A1.go",
-		"testdata/A2.go",
-
-		"testdata/B.template",
-		"testdata/B1.go",
-
-		"testdata/C.template",
-		"testdata/C1.go",
-
-		"testdata/D.template",
-		"testdata/D1.go",
-
-		"testdata/E.template",
-		"testdata/E1.go",
-
-		"testdata/F.template",
-		"testdata/F1.go",
-
-		"testdata/G.template",
-		"testdata/G1.go",
-
-		"testdata/H.template",
-		"testdata/H1.go",
-
-		"testdata/I.template",
-		"testdata/I1.go",
-
-		"testdata/J.template",
-		"testdata/J1.go",
-
-		"testdata/bad_type.template",
-		"testdata/no_before.template",
-		"testdata/no_after_return.template",
-		"testdata/type_mismatch.template",
-		"testdata/expr_type_mismatch.template",
+		"testdata/a.txtar",
+		"testdata/b.txtar",
+		"testdata/c.txtar",
+		"testdata/d.txtar",
+		"testdata/e.txtar",
+		"testdata/f.txtar",
+		"testdata/g.txtar",
+		"testdata/h.txtar",
+		"testdata/i.txtar",
+		"testdata/j.txtar",
+		"testdata/bad_type.txtar",
+		"testdata/no_before.txtar",
+		"testdata/no_after_return.txtar",
+		"testdata/type_mismatch.txtar",
+		"testdata/expr_type_mismatch.txtar",
 	} {
-		pkgname := strings.TrimSuffix(filepath.Base(filename), ".go")
-		conf.CreateFromFilenames(pkgname, filename)
-	}
-	iprog, err := conf.Load()
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	var xform *eg.Transformer
-	for _, info := range iprog.Created {
-		file := info.Files[0]
-		filename := iprog.Fset.File(file.Pos()).Name() // foo.go
+		t.Run(filename, func(t *testing.T) {
+			// Extract and load packages from test archive.
+			dir := testfiles.ExtractTxtarFileToTmp(t, filename)
+			cfg := packages.Config{
+				Mode: packages.LoadAllSyntax,
+				Dir:  dir,
+			}
+			pkgs, err := packages.Load(&cfg, "example.com/template", "example.com/in/...")
+			if err != nil {
+				t.Fatal(err)
+			}
+			if packages.PrintErrors(pkgs) > 0 {
+				t.Fatal("Load: there were errors")
+			}
 
-		if strings.HasSuffix(filename, "template") {
-			// a new template
-			shouldFail, _ := info.Pkg.Scope().Lookup("shouldFail").(*types.Const)
-			xform, err = eg.NewTransformer(iprog.Fset, info.Pkg, file, &info.Info, *verboseFlag)
+			// Find and compile the template.
+			var template *packages.Package
+			var inputs []*packages.Package
+			for _, pkg := range pkgs {
+				if pkg.Types.Name() == "template" {
+					template = pkg
+				} else {
+					inputs = append(inputs, pkg)
+				}
+			}
+			if template == nil {
+				t.Fatal("no template package")
+			}
+			shouldFail, _ := template.Types.Scope().Lookup("shouldFail").(*types.Const)
+			xform, err := eg.NewTransformer(template.Fset, template.Types, template.Syntax[0], template.TypesInfo, *verboseFlag)
 			if err != nil {
 				if shouldFail == nil {
 					t.Errorf("NewTransformer(%s): %s", filename, err)
@@ -125,55 +110,67 @@ func Test(t *testing.T) {
 				t.Errorf("NewTransformer(%s) succeeded unexpectedly; want error %q",
 					filename, shouldFail.Val())
 			}
-			continue
-		}
-
-		if xform == nil {
-			t.Errorf("%s: no previous template", filename)
-			continue
-		}
-
-		// apply previous template to this package
-		n := xform.Transform(&info.Info, info.Pkg, file)
-		if n == 0 {
-			t.Errorf("%s: no matches", filename)
-			continue
-		}
-
-		gotf, err := os.CreateTemp("", filepath.Base(filename)+"t")
-		if err != nil {
-			t.Fatal(err)
-		}
-		got := gotf.Name()          // foo.got
-		golden := filename + "lden" // foo.golden
-
-		// Write actual output to foo.got.
-		if err := eg.WriteAST(iprog.Fset, got, file); err != nil {
-			t.Error(err)
-		}
-		defer os.Remove(got)
-
-		// Compare foo.got with foo.golden.
-		var cmd *exec.Cmd
-		switch runtime.GOOS {
-		case "plan9":
-			cmd = exec.Command("/bin/diff", "-c", golden, got)
-		default:
-			cmd = exec.Command("/usr/bin/diff", "-u", golden, got)
-		}
-		buf := new(bytes.Buffer)
-		cmd.Stdout = buf
-		cmd.Stderr = os.Stderr
-		if err := cmd.Run(); err != nil {
-			t.Errorf("eg tests for %s failed: %s.\n%s\n", filename, err, buf)
 
+			// Apply template to each input package.
+			updated := make(map[string][]byte)
+			for _, pkg := range inputs {
+				for _, file := range pkg.Syntax {
+					filename, err := filepath.Rel(dir, pkg.Fset.File(file.FileStart).Name())
+					if err != nil {
+						t.Fatalf("can't relativize filename: %v", err)
+					}
+
+					// Apply the transform and reformat.
+					n := xform.Transform(pkg.TypesInfo, pkg.Types, file)
+					if n == 0 {
+						t.Fatalf("%s: no replacements", filename)
+					}
+					var got []byte
+					{
+						var out bytes.Buffer
+						format.Node(&out, pkg.Fset, file) // ignore error
+						got = out.Bytes()
+					}
+
+					// Compare formatted output with out/
+					// Errors here are not fatal, so we can proceed to -update.
+					outfile := strings.Replace(filename, "in", "out", 1)
+					updated[outfile] = got
+					want, err := os.ReadFile(filepath.Join(dir, outfile))
+					if err != nil {
+						t.Errorf("can't read output file: %v", err)
+					} else if diff := cmp.Diff(want, got); diff != "" {
+						t.Errorf("Unexpected output:\n%s\n\ngot %s:\n%s\n\nwant %s:\n%s",
+							diff,
+							filename, got, outfile, want)
+					}
+				}
+			}
+
+			// -update: replace the .txtar.
 			if *updateFlag {
-				t.Logf("Updating %s...", golden)
-				if err := exec.Command("/bin/cp", got, golden).Run(); err != nil {
-					t.Errorf("Update failed: %s", err)
+				ar, err := txtar.ParseFile(filename)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				var new bytes.Buffer
+				new.Write(ar.Comment)
+				for _, file := range ar.Files {
+					data, ok := updated[file.Name]
+					if !ok {
+						data = file.Data
+					}
+					fmt.Fprintf(&new, "-- %s --\n%s", file.Name, data)
+				}
+				t.Logf("Updating %s...", filename)
+				os.Remove(filename + ".bak")         // ignore error
+				os.Rename(filename, filename+".bak") // ignore error
+				if err := os.WriteFile(filename, new.Bytes(), 0666); err != nil {
+					t.Fatal(err)
 				}
 			}
-		}
+		})
 	}
 }
 
diff --git a/refactor/eg/testdata/A.template b/refactor/eg/testdata/A.template
deleted file mode 100644
index 6a23f12f61e..00000000000
--- a/refactor/eg/testdata/A.template
+++ /dev/null
@@ -1,11 +0,0 @@
-package template
-
-// Basic test of type-aware expression refactoring.
-
-import (
-	"errors"
-	"fmt"
-)
-
-func before(s string) error { return fmt.Errorf("%s", s) }
-func after(s string) error  { return errors.New(s) }
diff --git a/refactor/eg/testdata/A1.go b/refactor/eg/testdata/A1.go
deleted file mode 100644
index c64fd800b31..00000000000
--- a/refactor/eg/testdata/A1.go
+++ /dev/null
@@ -1,49 +0,0 @@
-package A1
-
-import (
-	. "fmt"
-	myfmt "fmt"
-	"os"
-	"strings"
-)
-
-func example(n int) {
-	x := "foo" + strings.Repeat("\t", n)
-	// Match, despite named import.
-	myfmt.Errorf("%s", x)
-
-	// Match, despite dot import.
-	Errorf("%s", x)
-
-	// Match: multiple matches in same function are possible.
-	myfmt.Errorf("%s", x)
-
-	// No match: wildcarded operand has the wrong type.
-	myfmt.Errorf("%s", 3)
-
-	// No match: function operand doesn't match.
-	myfmt.Printf("%s", x)
-
-	// No match again, dot import.
-	Printf("%s", x)
-
-	// Match.
-	myfmt.Fprint(os.Stderr, myfmt.Errorf("%s", x+"foo"))
-
-	// No match: though this literally matches the template,
-	// fmt doesn't resolve to a package here.
-	var fmt struct{ Errorf func(string, string) }
-	fmt.Errorf("%s", x)
-
-	// Recursive matching:
-
-	// Match: both matches are well-typed, so both succeed.
-	myfmt.Errorf("%s", myfmt.Errorf("%s", x+"foo").Error())
-
-	// Outer match succeeds, inner doesn't: 3 has wrong type.
-	myfmt.Errorf("%s", myfmt.Errorf("%s", 3).Error())
-
-	// Inner match succeeds, outer doesn't: the inner replacement
-	// has the wrong type (error not string).
-	myfmt.Errorf("%s", myfmt.Errorf("%s", x+"foo"))
-}
diff --git a/refactor/eg/testdata/A1.golden b/refactor/eg/testdata/A1.golden
deleted file mode 100644
index a8aeb068999..00000000000
--- a/refactor/eg/testdata/A1.golden
+++ /dev/null
@@ -1,50 +0,0 @@
-package A1
-
-import (
-	"errors"
-	. "fmt"
-	myfmt "fmt"
-	"os"
-	"strings"
-)
-
-func example(n int) {
-	x := "foo" + strings.Repeat("\t", n)
-	// Match, despite named import.
-	errors.New(x)
-
-	// Match, despite dot import.
-	errors.New(x)
-
-	// Match: multiple matches in same function are possible.
-	errors.New(x)
-
-	// No match: wildcarded operand has the wrong type.
-	myfmt.Errorf("%s", 3)
-
-	// No match: function operand doesn't match.
-	myfmt.Printf("%s", x)
-
-	// No match again, dot import.
-	Printf("%s", x)
-
-	// Match.
-	myfmt.Fprint(os.Stderr, errors.New(x+"foo"))
-
-	// No match: though this literally matches the template,
-	// fmt doesn't resolve to a package here.
-	var fmt struct{ Errorf func(string, string) }
-	fmt.Errorf("%s", x)
-
-	// Recursive matching:
-
-	// Match: both matches are well-typed, so both succeed.
-	errors.New(errors.New(x + "foo").Error())
-
-	// Outer match succeeds, inner doesn't: 3 has wrong type.
-	errors.New(myfmt.Errorf("%s", 3).Error())
-
-	// Inner match succeeds, outer doesn't: the inner replacement
-	// has the wrong type (error not string).
-	myfmt.Errorf("%s", errors.New(x+"foo"))
-}
diff --git a/refactor/eg/testdata/A2.go b/refactor/eg/testdata/A2.go
deleted file mode 100644
index 2fab7904001..00000000000
--- a/refactor/eg/testdata/A2.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package A2
-
-// This refactoring causes addition of "errors" import.
-// TODO(adonovan): fix: it should also remove "fmt".
-
-import myfmt "fmt"
-
-func example(n int) {
-	myfmt.Errorf("%s", "")
-}
diff --git a/refactor/eg/testdata/A2.golden b/refactor/eg/testdata/A2.golden
deleted file mode 100644
index 0e4ca447bc4..00000000000
--- a/refactor/eg/testdata/A2.golden
+++ /dev/null
@@ -1,13 +0,0 @@
-package A2
-
-// This refactoring causes addition of "errors" import.
-// TODO(adonovan): fix: it should also remove "fmt".
-
-import (
-	"errors"
-	myfmt "fmt"
-)
-
-func example(n int) {
-	errors.New("")
-}
diff --git a/refactor/eg/testdata/B.template b/refactor/eg/testdata/B.template
deleted file mode 100644
index c16627bd55e..00000000000
--- a/refactor/eg/testdata/B.template
+++ /dev/null
@@ -1,9 +0,0 @@
-package template
-
-// Basic test of expression refactoring.
-// (Types are not important in this case; it could be done with gofmt -r.)
-
-import "time"
-
-func before(t time.Time) time.Duration { return time.Now().Sub(t) }
-func after(t time.Time) time.Duration  { return time.Since(t) }
diff --git a/refactor/eg/testdata/B1.go b/refactor/eg/testdata/B1.go
deleted file mode 100644
index 1e09c905d27..00000000000
--- a/refactor/eg/testdata/B1.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package B1
-
-import "time"
-
-var startup = time.Now()
-
-func example() time.Duration {
-	before := time.Now()
-	time.Sleep(1)
-	return time.Now().Sub(before)
-}
-
-func msSinceStartup() int64 {
-	return int64(time.Now().Sub(startup) / time.Millisecond)
-}
diff --git a/refactor/eg/testdata/B1.golden b/refactor/eg/testdata/B1.golden
deleted file mode 100644
index b2ed30b72fc..00000000000
--- a/refactor/eg/testdata/B1.golden
+++ /dev/null
@@ -1,15 +0,0 @@
-package B1
-
-import "time"
-
-var startup = time.Now()
-
-func example() time.Duration {
-	before := time.Now()
-	time.Sleep(1)
-	return time.Since(before)
-}
-
-func msSinceStartup() int64 {
-	return int64(time.Since(startup) / time.Millisecond)
-}
diff --git a/refactor/eg/testdata/C.template b/refactor/eg/testdata/C.template
deleted file mode 100644
index f6f94d4aa9f..00000000000
--- a/refactor/eg/testdata/C.template
+++ /dev/null
@@ -1,10 +0,0 @@
-package template
-
-// Test of repeated use of wildcard in pattern.
-
-// NB: multiple patterns would be required to handle variants such as
-// s[:len(s)], s[x:len(s)], etc, since a wildcard can't match nothing at all.
-// TODO(adonovan): support multiple templates in a single pass.
-
-func before(s string) string { return s[:len(s)] }
-func after(s string) string  { return s }
diff --git a/refactor/eg/testdata/C1.go b/refactor/eg/testdata/C1.go
deleted file mode 100644
index fb565a3587f..00000000000
--- a/refactor/eg/testdata/C1.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package C1
-
-import "strings"
-
-func example() {
-	x := "foo"
-	println(x[:len(x)])
-
-	// Match, but the transformation is not sound w.r.t. possible side effects.
-	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 3))])
-
-	// No match, since second use of wildcard doesn't match first.
-	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 2))])
-
-	// Recursive match demonstrating bottom-up rewrite:
-	// only after the inner replacement occurs does the outer syntax match.
-	println((x[:len(x)])[:len(x[:len(x)])])
-	// -> (x[:len(x)])
-	// -> x
-}
diff --git a/refactor/eg/testdata/C1.golden b/refactor/eg/testdata/C1.golden
deleted file mode 100644
index d3b0b711881..00000000000
--- a/refactor/eg/testdata/C1.golden
+++ /dev/null
@@ -1,20 +0,0 @@
-package C1
-
-import "strings"
-
-func example() {
-	x := "foo"
-	println(x)
-
-	// Match, but the transformation is not sound w.r.t. possible side effects.
-	println(strings.Repeat("*", 3))
-
-	// No match, since second use of wildcard doesn't match first.
-	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 2))])
-
-	// Recursive match demonstrating bottom-up rewrite:
-	// only after the inner replacement occurs does the outer syntax match.
-	println(x)
-	// -> (x[:len(x)])
-	// -> x
-}
diff --git a/refactor/eg/testdata/D.template b/refactor/eg/testdata/D.template
deleted file mode 100644
index 6d3b6feb71d..00000000000
--- a/refactor/eg/testdata/D.template
+++ /dev/null
@@ -1,8 +0,0 @@
-package template
-
-import "fmt"
-
-// Test of semantic (not syntactic) matching of basic literals.
-
-func before() (int, error) { return fmt.Println(123, "a") }
-func after() (int, error)  { return fmt.Println(456, "!") }
diff --git a/refactor/eg/testdata/D1.go b/refactor/eg/testdata/D1.go
deleted file mode 100644
index 03a434c8738..00000000000
--- a/refactor/eg/testdata/D1.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package D1
-
-import "fmt"
-
-func example() {
-	fmt.Println(123, "a")         // match
-	fmt.Println(0x7b, `a`)        // match
-	fmt.Println(0173, "\x61")     // match
-	fmt.Println(100+20+3, "a"+"") // no match: constant expressions, but not basic literals
-}
diff --git a/refactor/eg/testdata/D1.golden b/refactor/eg/testdata/D1.golden
deleted file mode 100644
index 88d4a9e5151..00000000000
--- a/refactor/eg/testdata/D1.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-package D1
-
-import "fmt"
-
-func example() {
-	fmt.Println(456, "!")         // match
-	fmt.Println(456, "!")         // match
-	fmt.Println(456, "!")         // match
-	fmt.Println(100+20+3, "a"+"") // no match: constant expressions, but not basic literals
-}
diff --git a/refactor/eg/testdata/E.template b/refactor/eg/testdata/E.template
deleted file mode 100644
index 4bbbd1139b9..00000000000
--- a/refactor/eg/testdata/E.template
+++ /dev/null
@@ -1,12 +0,0 @@
-package template
-
-import (
-	"fmt"
-	"log"
-	"os"
-)
-
-// Replace call to void function by call to non-void function.
-
-func before(x interface{}) { log.Fatal(x) }
-func after(x interface{})  { fmt.Fprintf(os.Stderr, "warning: %v", x) }
diff --git a/refactor/eg/testdata/E1.go b/refactor/eg/testdata/E1.go
deleted file mode 100644
index 54054c81258..00000000000
--- a/refactor/eg/testdata/E1.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package E1
-
-import "log"
-
-func example() {
-	log.Fatal("oops") // match
-}
diff --git a/refactor/eg/testdata/E1.golden b/refactor/eg/testdata/E1.golden
deleted file mode 100644
index ec10b41e5c9..00000000000
--- a/refactor/eg/testdata/E1.golden
+++ /dev/null
@@ -1,11 +0,0 @@
-package E1
-
-import (
-	"fmt"
-	"log"
-	"os"
-)
-
-func example() {
-	fmt.Fprintf(os.Stderr, "warning: %v", "oops") // match
-}
diff --git a/refactor/eg/testdata/F.template b/refactor/eg/testdata/F.template
deleted file mode 100644
index df73beb28d7..00000000000
--- a/refactor/eg/testdata/F.template
+++ /dev/null
@@ -1,8 +0,0 @@
-package templates
-
-// Test
-
-import "sync"
-
-func before(s sync.RWMutex) { s.Lock() }
-func after(s sync.RWMutex)  { s.RLock() }
diff --git a/refactor/eg/testdata/F1.go b/refactor/eg/testdata/F1.go
deleted file mode 100644
index da9c9de1b2d..00000000000
--- a/refactor/eg/testdata/F1.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package F1
-
-import "sync"
-
-func example(n int) {
-	var x struct {
-		mutex sync.RWMutex
-	}
-
-	var y struct {
-		sync.RWMutex
-	}
-
-	type l struct {
-		sync.RWMutex
-	}
-
-	var z struct {
-		l
-	}
-
-	var a struct {
-		*l
-	}
-
-	var b struct{ Lock func() }
-
-	// Match
-	x.mutex.Lock()
-
-	// Match
-	y.Lock()
-
-	// Match indirect
-	z.Lock()
-
-	// Should be no match however currently matches due to:
-	// https://golang.org/issue/8584
-	// Will start failing when this is fixed then just change golden to
-	// No match pointer indirect
-	// a.Lock()
-	a.Lock()
-
-	// No match
-	b.Lock()
-}
diff --git a/refactor/eg/testdata/F1.golden b/refactor/eg/testdata/F1.golden
deleted file mode 100644
index ea5d0cde3a8..00000000000
--- a/refactor/eg/testdata/F1.golden
+++ /dev/null
@@ -1,46 +0,0 @@
-package F1
-
-import "sync"
-
-func example(n int) {
-	var x struct {
-		mutex sync.RWMutex
-	}
-
-	var y struct {
-		sync.RWMutex
-	}
-
-	type l struct {
-		sync.RWMutex
-	}
-
-	var z struct {
-		l
-	}
-
-	var a struct {
-		*l
-	}
-
-	var b struct{ Lock func() }
-
-	// Match
-	x.mutex.RLock()
-
-	// Match
-	y.RLock()
-
-	// Match indirect
-	z.RLock()
-
-	// Should be no match however currently matches due to:
-	// https://golang.org/issue/8584
-	// Will start failing when this is fixed then just change golden to
-	// No match pointer indirect
-	// a.Lock()
-	a.RLock()
-
-	// No match
-	b.Lock()
-}
diff --git a/refactor/eg/testdata/G.template b/refactor/eg/testdata/G.template
deleted file mode 100644
index ab368ce4637..00000000000
--- a/refactor/eg/testdata/G.template
+++ /dev/null
@@ -1,9 +0,0 @@
-package templates
-
-import (
-	"go/ast" // defines many unencapsulated structs
-	"go/token"
-)
-
-func before(from, to token.Pos) ast.BadExpr { return ast.BadExpr{From: from, To: to} }
-func after(from, to token.Pos) ast.BadExpr  { return ast.BadExpr{from, to} }
diff --git a/refactor/eg/testdata/G1.go b/refactor/eg/testdata/G1.go
deleted file mode 100644
index 0fb9ab95b84..00000000000
--- a/refactor/eg/testdata/G1.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package G1
-
-import "go/ast"
-
-func example() {
-	_ = ast.BadExpr{From: 123, To: 456} // match
-	_ = ast.BadExpr{123, 456}           // no match
-	_ = ast.BadExpr{From: 123}          // no match
-	_ = ast.BadExpr{To: 456}            // no match
-}
diff --git a/refactor/eg/testdata/G1.golden b/refactor/eg/testdata/G1.golden
deleted file mode 100644
index ba3704c4210..00000000000
--- a/refactor/eg/testdata/G1.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-package G1
-
-import "go/ast"
-
-func example() {
-	_ = ast.BadExpr{123, 456}  // match
-	_ = ast.BadExpr{123, 456}  // no match
-	_ = ast.BadExpr{From: 123} // no match
-	_ = ast.BadExpr{To: 456}   // no match
-}
diff --git a/refactor/eg/testdata/H.template b/refactor/eg/testdata/H.template
deleted file mode 100644
index fa6f802c8af..00000000000
--- a/refactor/eg/testdata/H.template
+++ /dev/null
@@ -1,9 +0,0 @@
-package templates
-
-import (
-	"go/ast" // defines many unencapsulated structs
-	"go/token"
-)
-
-func before(from, to token.Pos) ast.BadExpr { return ast.BadExpr{from, to} }
-func after(from, to token.Pos) ast.BadExpr  { return ast.BadExpr{From: from, To: to} }
diff --git a/refactor/eg/testdata/H1.go b/refactor/eg/testdata/H1.go
deleted file mode 100644
index e151ac87764..00000000000
--- a/refactor/eg/testdata/H1.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package H1
-
-import "go/ast"
-
-func example() {
-	_ = ast.BadExpr{From: 123, To: 456} // no match
-	_ = ast.BadExpr{123, 456}           // match
-	_ = ast.BadExpr{From: 123}          // no match
-	_ = ast.BadExpr{To: 456}            // no match
-}
diff --git a/refactor/eg/testdata/H1.golden b/refactor/eg/testdata/H1.golden
deleted file mode 100644
index da2658a6648..00000000000
--- a/refactor/eg/testdata/H1.golden
+++ /dev/null
@@ -1,10 +0,0 @@
-package H1
-
-import "go/ast"
-
-func example() {
-	_ = ast.BadExpr{From: 123, To: 456} // no match
-	_ = ast.BadExpr{From: 123, To: 456} // match
-	_ = ast.BadExpr{From: 123}          // no match
-	_ = ast.BadExpr{To: 456}            // no match
-}
diff --git a/refactor/eg/testdata/I.template b/refactor/eg/testdata/I.template
deleted file mode 100644
index b8e8f939b10..00000000000
--- a/refactor/eg/testdata/I.template
+++ /dev/null
@@ -1,12 +0,0 @@
-package templates
-
-import (
-	"errors"
-	"fmt"
-)
-
-func before(s string) error { return fmt.Errorf("%s", s) }
-func after(s string) error {
-	n := fmt.Sprintf("error - %s", s)
-	return errors.New(n)
-}
diff --git a/refactor/eg/testdata/I1.go b/refactor/eg/testdata/I1.go
deleted file mode 100644
index ef3fe8befac..00000000000
--- a/refactor/eg/testdata/I1.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package I1
-
-import "fmt"
-
-func example() {
-	_ = fmt.Errorf("%s", "foo")
-}
diff --git a/refactor/eg/testdata/I1.golden b/refactor/eg/testdata/I1.golden
deleted file mode 100644
index d0246aeb85d..00000000000
--- a/refactor/eg/testdata/I1.golden
+++ /dev/null
@@ -1,12 +0,0 @@
-package I1
-
-import (
-	"errors"
-	"fmt"
-)
-
-func example() {
-
-	n := fmt.Sprintf("error - %s", "foo")
-	_ = errors.New(n)
-}
diff --git a/refactor/eg/testdata/J.template b/refactor/eg/testdata/J.template
deleted file mode 100644
index b3b1f1872ac..00000000000
--- a/refactor/eg/testdata/J.template
+++ /dev/null
@@ -1,9 +0,0 @@
-package templates
-
-import ()
-
-func before(x int) int { return x + x + x }
-func after(x int) int {
-	temp := x + x
-	return temp + x
-}
diff --git a/refactor/eg/testdata/J1.go b/refactor/eg/testdata/J1.go
deleted file mode 100644
index 532ca13e66c..00000000000
--- a/refactor/eg/testdata/J1.go
+++ /dev/null
@@ -1,8 +0,0 @@
-package I1
-
-import "fmt"
-
-func example() {
-	temp := 5
-	fmt.Print(temp + temp + temp)
-}
diff --git a/refactor/eg/testdata/J1.golden b/refactor/eg/testdata/J1.golden
deleted file mode 100644
index 911ef874175..00000000000
--- a/refactor/eg/testdata/J1.golden
+++ /dev/null
@@ -1,9 +0,0 @@
-package I1
-
-import "fmt"
-
-func example() {
-	temp := 5
-	temp := temp + temp
-	fmt.Print(temp + temp)
-}
diff --git a/refactor/eg/testdata/a.txtar b/refactor/eg/testdata/a.txtar
new file mode 100644
index 00000000000..873197391e5
--- /dev/null
+++ b/refactor/eg/testdata/a.txtar
@@ -0,0 +1,147 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+// Basic test of type-aware expression refactoring.
+
+import (
+	"errors"
+	"fmt"
+)
+
+func before(s string) error { return fmt.Errorf("%s", s) }
+func after(s string) error  { return errors.New(s) }
+
+-- in/a1/a1.go --
+package a1
+
+import (
+	. "fmt"
+	myfmt "fmt"
+	"os"
+	"strings"
+)
+
+func example(n int) {
+	x := "foo" + strings.Repeat("\t", n)
+	// Match, despite named import.
+	myfmt.Errorf("%s", x)
+
+	// Match, despite dot import.
+	Errorf("%s", x)
+
+	// Match: multiple matches in same function are possible.
+	myfmt.Errorf("%s", x)
+
+	// No match: wildcarded operand has the wrong type.
+	myfmt.Errorf("%s", 3)
+
+	// No match: function operand doesn't match.
+	myfmt.Printf("%s", x)
+
+	// No match again, dot import.
+	Printf("%s", x)
+
+	// Match.
+	myfmt.Fprint(os.Stderr, myfmt.Errorf("%s", x+"foo"))
+
+	// No match: though this literally matches the template,
+	// fmt doesn't resolve to a package here.
+	var fmt struct{ Errorf func(string, string) }
+	fmt.Errorf("%s", x)
+
+	// Recursive matching:
+
+	// Match: both matches are well-typed, so both succeed.
+	myfmt.Errorf("%s", myfmt.Errorf("%s", x+"foo").Error())
+
+	// Outer match succeeds, inner doesn't: 3 has wrong type.
+	myfmt.Errorf("%s", myfmt.Errorf("%s", 3).Error())
+
+	// Inner match succeeds, outer doesn't: the inner replacement
+	// has the wrong type (error not string).
+	myfmt.Errorf("%s", myfmt.Errorf("%s", x+"foo"))
+}
+
+-- out/a1/a1.go --
+package a1
+
+import (
+	"errors"
+	. "fmt"
+	myfmt "fmt"
+	"os"
+	"strings"
+)
+
+func example(n int) {
+	x := "foo" + strings.Repeat("\t", n)
+	// Match, despite named import.
+	errors.New(x)
+
+	// Match, despite dot import.
+	errors.New(x)
+
+	// Match: multiple matches in same function are possible.
+	errors.New(x)
+
+	// No match: wildcarded operand has the wrong type.
+	myfmt.Errorf("%s", 3)
+
+	// No match: function operand doesn't match.
+	myfmt.Printf("%s", x)
+
+	// No match again, dot import.
+	Printf("%s", x)
+
+	// Match.
+	myfmt.Fprint(os.Stderr, errors.New(x+"foo"))
+
+	// No match: though this literally matches the template,
+	// fmt doesn't resolve to a package here.
+	var fmt struct{ Errorf func(string, string) }
+	fmt.Errorf("%s", x)
+
+	// Recursive matching:
+
+	// Match: both matches are well-typed, so both succeed.
+	errors.New(errors.New(x + "foo").Error())
+
+	// Outer match succeeds, inner doesn't: 3 has wrong type.
+	errors.New(myfmt.Errorf("%s", 3).Error())
+
+	// Inner match succeeds, outer doesn't: the inner replacement
+	// has the wrong type (error not string).
+	myfmt.Errorf("%s", errors.New(x+"foo"))
+}
+-- a2/a2.go --
+package a2
+
+// This refactoring causes addition of "errors" import.
+// TODO(adonovan): fix: it should also remove "fmt".
+
+import myfmt "fmt"
+
+func example(n int) {
+	myfmt.Errorf("%s", "")
+}
+
+-- out/a2/a2.go --
+package a2
+
+// This refactoring causes addition of "errors" import.
+// TODO(adonovan): fix: it should also remove "fmt".
+
+import (
+	"errors"
+	myfmt "fmt"
+)
+
+func example(n int) {
+	errors.New("")
+}
diff --git a/refactor/eg/testdata/b.txtar b/refactor/eg/testdata/b.txtar
new file mode 100644
index 00000000000..d55fa1ad7ea
--- /dev/null
+++ b/refactor/eg/testdata/b.txtar
@@ -0,0 +1,49 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+// Basic test of expression refactoring.
+// (Types are not important in this case; it could be done with gofmt -r.)
+
+import "time"
+
+func before(t time.Time) time.Duration { return time.Now().Sub(t) }
+func after(t time.Time) time.Duration  { return time.Since(t) }
+
+-- in/b1/b1.go --
+package b1
+
+import "time"
+
+var startup = time.Now()
+
+func example() time.Duration {
+	before := time.Now()
+	time.Sleep(1)
+	return time.Now().Sub(before)
+}
+
+func msSinceStartup() int64 {
+	return int64(time.Now().Sub(startup) / time.Millisecond)
+}
+
+-- out/b1/b1.go --
+package b1
+
+import "time"
+
+var startup = time.Now()
+
+func example() time.Duration {
+	before := time.Now()
+	time.Sleep(1)
+	return time.Since(before)
+}
+
+func msSinceStartup() int64 {
+	return int64(time.Since(startup) / time.Millisecond)
+}
diff --git a/refactor/eg/testdata/bad_type.template b/refactor/eg/testdata/bad_type.txtar
similarity index 75%
rename from refactor/eg/testdata/bad_type.template
rename to refactor/eg/testdata/bad_type.txtar
index 6d53d7e5709..3c4ff5638ba 100644
--- a/refactor/eg/testdata/bad_type.template
+++ b/refactor/eg/testdata/bad_type.txtar
@@ -1,3 +1,9 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 // Test in which replacement has a different type.
diff --git a/refactor/eg/testdata/c.txtar b/refactor/eg/testdata/c.txtar
new file mode 100644
index 00000000000..67c29fed1c1
--- /dev/null
+++ b/refactor/eg/testdata/c.txtar
@@ -0,0 +1,60 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+// Test of repeated use of wildcard in pattern.
+
+// NB: multiple patterns would be required to handle variants such as
+// s[:len(s)], s[x:len(s)], etc, since a wildcard can't match nothing at all.
+// TODO(adonovan): support multiple templates in a single pass.
+
+func before(s string) string { return s[:len(s)] }
+func after(s string) string  { return s }
+
+-- in/c1/c1.go --
+package C1
+
+import "strings"
+
+func example() {
+	x := "foo"
+	println(x[:len(x)])
+
+	// Match, but the transformation is not sound w.r.t. possible side effects.
+	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 3))])
+
+	// No match, since second use of wildcard doesn't match first.
+	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 2))])
+
+	// Recursive match demonstrating bottom-up rewrite:
+	// only after the inner replacement occurs does the outer syntax match.
+	println((x[:len(x)])[:len(x[:len(x)])])
+	// -> (x[:len(x)])
+	// -> x
+}
+
+-- out/c1/c1.go --
+package C1
+
+import "strings"
+
+func example() {
+	x := "foo"
+	println(x)
+
+	// Match, but the transformation is not sound w.r.t. possible side effects.
+	println(strings.Repeat("*", 3))
+
+	// No match, since second use of wildcard doesn't match first.
+	println(strings.Repeat("*", 3)[:len(strings.Repeat("*", 2))])
+
+	// Recursive match demonstrating bottom-up rewrite:
+	// only after the inner replacement occurs does the outer syntax match.
+	println(x)
+	// -> (x[:len(x)])
+	// -> x
+}
diff --git a/refactor/eg/testdata/d.txtar b/refactor/eg/testdata/d.txtar
new file mode 100644
index 00000000000..5b4e65d2e3c
--- /dev/null
+++ b/refactor/eg/testdata/d.txtar
@@ -0,0 +1,38 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import "fmt"
+
+// Test of semantic (not syntactic) matching of basic literals.
+
+func before() (int, error) { return fmt.Println(123, "a") }
+func after() (int, error)  { return fmt.Println(456, "!") }
+
+-- in/d1/d1.go --
+package d1
+
+import "fmt"
+
+func example() {
+	fmt.Println(123, "a")         // match
+	fmt.Println(0x7b, `a`)        // match
+	fmt.Println(0173, "\x61")     // match
+	fmt.Println(100+20+3, "a"+"") // no match: constant expressions, but not basic literals
+}
+
+-- out/d1/d1.go --
+package d1
+
+import "fmt"
+
+func example() {
+	fmt.Println(456, "!")         // match
+	fmt.Println(456, "!")         // match
+	fmt.Println(456, "!")         // match
+	fmt.Println(100+20+3, "a"+"") // no match: constant expressions, but not basic literals
+}
diff --git a/refactor/eg/testdata/e.txtar b/refactor/eg/testdata/e.txtar
new file mode 100644
index 00000000000..e82652f221a
--- /dev/null
+++ b/refactor/eg/testdata/e.txtar
@@ -0,0 +1,40 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import (
+	"fmt"
+	"log"
+	"os"
+)
+
+// Replace call to void function by call to non-void function.
+
+func before(x interface{}) { log.Fatal(x) }
+func after(x interface{})  { fmt.Fprintf(os.Stderr, "warning: %v", x) }
+
+-- in/e1/e1.go --
+package e1
+
+import "log"
+
+func example() {
+	log.Fatal("oops") // match
+}
+
+-- out/e1/e1.go --
+package e1
+
+import (
+	"fmt"
+	"log"
+	"os"
+)
+
+func example() {
+	fmt.Fprintf(os.Stderr, "warning: %v", "oops") // match
+}
diff --git a/refactor/eg/testdata/expr_type_mismatch.template b/refactor/eg/testdata/expr_type_mismatch.txtar
similarity index 88%
rename from refactor/eg/testdata/expr_type_mismatch.template
rename to refactor/eg/testdata/expr_type_mismatch.txtar
index 00e00b1c419..dca702687a0 100644
--- a/refactor/eg/testdata/expr_type_mismatch.template
+++ b/refactor/eg/testdata/expr_type_mismatch.txtar
@@ -1,3 +1,10 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 import (
diff --git a/refactor/eg/testdata/f.txtar b/refactor/eg/testdata/f.txtar
new file mode 100644
index 00000000000..139405e57c0
--- /dev/null
+++ b/refactor/eg/testdata/f.txtar
@@ -0,0 +1,110 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+// Test
+
+import "sync"
+
+func before(s sync.RWMutex) { s.Lock() }
+func after(s sync.RWMutex)  { s.RLock() }
+
+-- in/f1/f1.go --
+package F1
+
+import "sync"
+
+func example(n int) {
+	var x struct {
+		mutex sync.RWMutex
+	}
+
+	var y struct {
+		sync.RWMutex
+	}
+
+	type l struct {
+		sync.RWMutex
+	}
+
+	var z struct {
+		l
+	}
+
+	var a struct {
+		*l
+	}
+
+	var b struct{ Lock func() }
+
+	// Match
+	x.mutex.Lock()
+
+	// Match
+	y.Lock()
+
+	// Match indirect
+	z.Lock()
+
+	// Should be no match however currently matches due to:
+	// https://golang.org/issue/8584
+	// Will start failing when this is fixed then just change golden to
+	// No match pointer indirect
+	// a.Lock()
+	a.Lock()
+
+	// No match
+	b.Lock()
+}
+
+-- out/f1/f1.go --
+package F1
+
+import "sync"
+
+func example(n int) {
+	var x struct {
+		mutex sync.RWMutex
+	}
+
+	var y struct {
+		sync.RWMutex
+	}
+
+	type l struct {
+		sync.RWMutex
+	}
+
+	var z struct {
+		l
+	}
+
+	var a struct {
+		*l
+	}
+
+	var b struct{ Lock func() }
+
+	// Match
+	x.mutex.RLock()
+
+	// Match
+	y.RLock()
+
+	// Match indirect
+	z.RLock()
+
+	// Should be no match however currently matches due to:
+	// https://golang.org/issue/8584
+	// Will start failing when this is fixed then just change golden to
+	// No match pointer indirect
+	// a.Lock()
+	a.RLock()
+
+	// No match
+	b.Lock()
+}
diff --git a/refactor/eg/testdata/g.txtar b/refactor/eg/testdata/g.txtar
new file mode 100644
index 00000000000..95843bf940a
--- /dev/null
+++ b/refactor/eg/testdata/g.txtar
@@ -0,0 +1,39 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import (
+	"go/ast" // defines many unencapsulated structs
+	"go/token"
+)
+
+func before(from, to token.Pos) ast.BadExpr { return ast.BadExpr{From: from, To: to} }
+func after(from, to token.Pos) ast.BadExpr  { return ast.BadExpr{from, to} }
+
+-- in/g1/g1.go --
+package g1
+
+import "go/ast"
+
+func example() {
+	_ = ast.BadExpr{From: 123, To: 456} // match
+	_ = ast.BadExpr{123, 456}           // no match
+	_ = ast.BadExpr{From: 123}          // no match
+	_ = ast.BadExpr{To: 456}            // no match
+}
+
+-- out/g1/g1.go --
+package g1
+
+import "go/ast"
+
+func example() {
+	_ = ast.BadExpr{123, 456}  // match
+	_ = ast.BadExpr{123, 456}  // no match
+	_ = ast.BadExpr{From: 123} // no match
+	_ = ast.BadExpr{To: 456}   // no match
+}
diff --git a/refactor/eg/testdata/h.txtar b/refactor/eg/testdata/h.txtar
new file mode 100644
index 00000000000..94085350183
--- /dev/null
+++ b/refactor/eg/testdata/h.txtar
@@ -0,0 +1,39 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import (
+	"go/ast" // defines many unencapsulated structs
+	"go/token"
+)
+
+func before(from, to token.Pos) ast.BadExpr { return ast.BadExpr{from, to} }
+func after(from, to token.Pos) ast.BadExpr  { return ast.BadExpr{From: from, To: to} }
+
+-- in/h1/h1.go --
+package h1
+
+import "go/ast"
+
+func example() {
+	_ = ast.BadExpr{From: 123, To: 456} // no match
+	_ = ast.BadExpr{123, 456}           // match
+	_ = ast.BadExpr{From: 123}          // no match
+	_ = ast.BadExpr{To: 456}            // no match
+}
+
+-- out/h1/h1.go --
+package h1
+
+import "go/ast"
+
+func example() {
+	_ = ast.BadExpr{From: 123, To: 456} // no match
+	_ = ast.BadExpr{From: 123, To: 456} // match
+	_ = ast.BadExpr{From: 123}          // no match
+	_ = ast.BadExpr{To: 456}            // no match
+}
diff --git a/refactor/eg/testdata/i.txtar b/refactor/eg/testdata/i.txtar
new file mode 100644
index 00000000000..11486c2112f
--- /dev/null
+++ b/refactor/eg/testdata/i.txtar
@@ -0,0 +1,41 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import (
+	"errors"
+	"fmt"
+)
+
+func before(s string) error { return fmt.Errorf("%s", s) }
+func after(s string) error {
+	n := fmt.Sprintf("error - %s", s)
+	return errors.New(n)
+}
+
+-- in/i1/i1.go --
+package i1
+
+import "fmt"
+
+func example() {
+	_ = fmt.Errorf("%s", "foo")
+}
+
+-- out/i1/i1.go --
+package i1
+
+import (
+	"errors"
+	"fmt"
+)
+
+func example() {
+
+	n := fmt.Sprintf("error - %s", "foo")
+	_ = errors.New(n)
+}
diff --git a/refactor/eg/testdata/j.txtar b/refactor/eg/testdata/j.txtar
new file mode 100644
index 00000000000..9bb0a71418b
--- /dev/null
+++ b/refactor/eg/testdata/j.txtar
@@ -0,0 +1,36 @@
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
+package template
+
+import ()
+
+func before(x int) int { return x + x + x }
+func after(x int) int {
+	temp := x + x
+	return temp + x
+}
+
+-- in/j1/j1.go --
+package j1
+
+import "fmt"
+
+func example() {
+	temp := 5
+	fmt.Print(temp + temp + temp)
+}
+
+-- out/j1/j1.go --
+package j1
+
+import "fmt"
+
+func example() {
+	temp := 5
+	temp := temp + temp
+	fmt.Print(temp + temp)
+}
diff --git a/refactor/eg/testdata/no_after_return.template b/refactor/eg/testdata/no_after_return.txtar
similarity index 56%
rename from refactor/eg/testdata/no_after_return.template
rename to refactor/eg/testdata/no_after_return.txtar
index dd2cbf61e15..7965ddd8538 100644
--- a/refactor/eg/testdata/no_after_return.template
+++ b/refactor/eg/testdata/no_after_return.txtar
@@ -1,3 +1,10 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 func before() int { return 0 }
diff --git a/refactor/eg/testdata/no_before.template b/refactor/eg/testdata/no_before.txtar
similarity index 56%
rename from refactor/eg/testdata/no_before.template
rename to refactor/eg/testdata/no_before.txtar
index 9205e6677a4..640f7269a04 100644
--- a/refactor/eg/testdata/no_before.template
+++ b/refactor/eg/testdata/no_before.txtar
@@ -1,3 +1,10 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 const shouldFail = "no 'before' func found in template"
diff --git a/refactor/eg/testdata/type_mismatch.template b/refactor/eg/testdata/type_mismatch.txtar
similarity index 64%
rename from refactor/eg/testdata/type_mismatch.template
rename to refactor/eg/testdata/type_mismatch.txtar
index 787c9a7a8c7..94157b6dc06 100644
--- a/refactor/eg/testdata/type_mismatch.template
+++ b/refactor/eg/testdata/type_mismatch.txtar
@@ -1,3 +1,10 @@
+
+
+-- go.mod --
+module example.com
+go 1.18
+
+-- template/template.go --
 package template
 
 const shouldFail = "different signatures"

From 6c47868975202371d5cba3fc11ab04acfe1cd350 Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Thu, 26 Sep 2024 14:09:07 -0700
Subject: [PATCH 082/102] internal/gcimporter: run larger tests with and
 without aliases

Run larger and standard library tests both with and without the
gotypesaliases GODEBUG enabled. Skips two know failures on 1.22.

Fixes an incorrectly copied constant in an expectation.

Change-Id: I8054bf5609b7a0e6fb544bd906a00f2f02f75b31
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616157
Reviewed-by: Alan Donovan 
Reviewed-by: Robert Findley 
Commit-Queue: Tim King 
LUCI-TryBot-Result: Go LUCI 
---
 internal/gcimporter/gcimporter_test.go    | 39 +++++++++++++++++++++++
 internal/gcimporter/iexport_go118_test.go |  4 +++
 internal/gcimporter/iexport_test.go       |  9 +++++-
 internal/gcimporter/shallow_test.go       |  3 ++
 internal/gcimporter/stdlib_test.go        |  5 ++-
 5 files changed, 58 insertions(+), 2 deletions(-)

diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go
index 44a3db78f41..904d0828c8a 100644
--- a/internal/gcimporter/gcimporter_test.go
+++ b/internal/gcimporter/gcimporter_test.go
@@ -120,6 +120,10 @@ func TestImportTestdata(t *testing.T) {
 	needsCompiler(t, "gc")
 	testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache
 
+	testAliases(t, testImportTestdata)
+}
+
+func testImportTestdata(t *testing.T) {
 	tmpdir := mktmpdir(t)
 	defer os.RemoveAll(tmpdir)
 
@@ -168,9 +172,22 @@ func TestImportTypeparamTests(t *testing.T) {
 		t.Skipf("gc-built packages not available (compiler = %s)", runtime.Compiler)
 	}
 
+	testAliases(t, testImportTypeparamTests)
+}
+
+func testImportTypeparamTests(t *testing.T) {
 	tmpdir := mktmpdir(t)
 	defer os.RemoveAll(tmpdir)
 
+	// GoVersion -> GoDebug -> filename -> reason to skip
+	skips := map[int]map[string]map[string]string{
+		22: {aliasesOn: {
+			"issue50259.go": "internal compiler error: unexpected types2.Invalid",
+			"struct.go":     "badly formatted expectation E[int]",
+		}},
+	}
+	dbg, version := os.Getenv("GODEBUG"), testenv.Go1Point()
+
 	// Check go files in test/typeparam, except those that fail for a known
 	// reason.
 	rootDir := filepath.Join(runtime.GOROOT(), "test", "typeparam")
@@ -186,6 +203,11 @@ func TestImportTypeparamTests(t *testing.T) {
 		}
 
 		t.Run(entry.Name(), func(t *testing.T) {
+			if reason := skips[version][dbg][entry.Name()]; reason != "" {
+				t.Skipf("Skipping file %q with GODEBUG=%q due to %q at version 1.%d",
+					entry.Name(), dbg, reason, version)
+			}
+
 			filename := filepath.Join(rootDir, entry.Name())
 			src, err := os.ReadFile(filename)
 			if err != nil {
@@ -333,6 +355,10 @@ func TestImportStdLib(t *testing.T) {
 	needsCompiler(t, "gc")
 	testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache
 
+	testAliases(t, testImportStdLib)
+}
+
+func testImportStdLib(t *testing.T) {
 	// Get list of packages in stdlib. Filter out test-only packages with {{if .GoFiles}} check.
 	var stderr bytes.Buffer
 	cmd := exec.Command("go", "list", "-f", "{{if .GoFiles}}{{.ImportPath}}{{end}}", "std")
@@ -1001,3 +1027,16 @@ const (
 	aliasesOff = "gotypesalias=0" // default GODEBUG in 1.22 (like x/tools)
 	aliasesOn  = "gotypesalias=1" // default after 1.23
 )
+
+// testAliases runs f within subtests with the GODEBUG gotypesalias enables and disabled.
+func testAliases(t *testing.T, f func(*testing.T)) {
+	for _, dbg := range []string{
+		aliasesOff,
+		aliasesOn,
+	} {
+		t.Run(dbg, func(t *testing.T) {
+			t.Setenv("GODEBUG", dbg)
+			f(t)
+		})
+	}
+}
diff --git a/internal/gcimporter/iexport_go118_test.go b/internal/gcimporter/iexport_go118_test.go
index 2f8eafd6705..005b95b94f3 100644
--- a/internal/gcimporter/iexport_go118_test.go
+++ b/internal/gcimporter/iexport_go118_test.go
@@ -99,6 +99,10 @@ func testExportSrc(t *testing.T, src []byte) {
 func TestIndexedImportTypeparamTests(t *testing.T) {
 	testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache
 
+	testAliases(t, testIndexedImportTypeparamTests)
+}
+
+func testIndexedImportTypeparamTests(t *testing.T) {
 	// Check go files in test/typeparam.
 	rootDir := filepath.Join(runtime.GOROOT(), "test", "typeparam")
 	list, err := os.ReadDir(rootDir)
diff --git a/internal/gcimporter/iexport_test.go b/internal/gcimporter/iexport_test.go
index 66d28457886..ff3a322001f 100644
--- a/internal/gcimporter/iexport_test.go
+++ b/internal/gcimporter/iexport_test.go
@@ -47,6 +47,10 @@ func TestIExportData_stdlib(t *testing.T) {
 		t.Skip("skipping RAM hungry test in -short mode")
 	}
 
+	testAliases(t, testIExportData_stdlib)
+}
+
+func testIExportData_stdlib(t *testing.T) {
 	var errorsDir string // GOROOT/src/errors directory
 	{
 		cfg := packages.Config{
@@ -105,7 +109,7 @@ type UnknownType undefined
 	})
 
 	// Assert that we saw a plausible sized library.
-	const minStdlibPackages = 284
+	const minStdlibPackages = 248
 	if n := len(allPkgs); n < minStdlibPackages {
 		t.Errorf("Loaded only %d packages, want at least %d", n, minStdlibPackages)
 	}
@@ -225,6 +229,9 @@ func TestIExportData_long(t *testing.T) {
 }
 
 func TestIExportData_typealiases(t *testing.T) {
+	testAliases(t, testIExportData_typealiases)
+}
+func testIExportData_typealiases(t *testing.T) {
 	// parse and typecheck
 	fset1 := token.NewFileSet()
 	f, err := parser.ParseFile(fset1, "p.go", src, 0)
diff --git a/internal/gcimporter/shallow_test.go b/internal/gcimporter/shallow_test.go
index e412a947bdf..f1ae8781e83 100644
--- a/internal/gcimporter/shallow_test.go
+++ b/internal/gcimporter/shallow_test.go
@@ -27,6 +27,9 @@ func TestShallowStd(t *testing.T) {
 	}
 	testenv.NeedsTool(t, "go")
 
+	testAliases(t, testShallowStd)
+}
+func testShallowStd(t *testing.T) {
 	// Load import graph of the standard library.
 	// (No parsing or type-checking.)
 	cfg := &packages.Config{
diff --git a/internal/gcimporter/stdlib_test.go b/internal/gcimporter/stdlib_test.go
index 33ff7958118..85547a49d7b 100644
--- a/internal/gcimporter/stdlib_test.go
+++ b/internal/gcimporter/stdlib_test.go
@@ -19,10 +19,13 @@ import (
 )
 
 // TestStdlib ensures that all packages in std and x/tools can be
-// type-checked using export data. Takes around 3s.
+// type-checked using export data.
 func TestStdlib(t *testing.T) {
 	testenv.NeedsGoPackages(t)
 
+	testAliases(t, testStdlib)
+}
+func testStdlib(t *testing.T) {
 	// gcexportdata.Read rapidly consumes FileSet address space,
 	// so disable the test on 32-bit machines.
 	// (We could use a fresh FileSet per type-check, but that

From 18bc03245e50fdd5ce3241a7a5fc261c5e56fbdf Mon Sep 17 00:00:00 2001
From: Rob Findley 
Date: Fri, 27 Sep 2024 18:50:17 +0000
Subject: [PATCH 083/102] gopls: update x/telemetry to pick up fix for
 golang/go#69681

Update the x/telemetry dependency to pick up the fix for crash reporting
from golang/go#69681.

For golang/go#69681

Change-Id: Ic8bda6e1e80a5004989567a5e7645ef99cf9130f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616415
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Robert Findley 
Reviewed-by: Alan Donovan 
---
 gopls/go.mod | 2 +-
 gopls/go.sum | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/gopls/go.mod b/gopls/go.mod
index beae17b01f7..c54d5e96cd2 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -10,7 +10,7 @@ require (
 	golang.org/x/mod v0.21.0
 	golang.org/x/sync v0.8.0
 	golang.org/x/sys v0.25.0
-	golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98
+	golang.org/x/telemetry v0.0.0-20240927184629-19675431963b
 	golang.org/x/text v0.18.0
 	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
 	golang.org/x/vuln v1.0.4
diff --git a/gopls/go.sum b/gopls/go.sum
index 2819e487d71..e546853db07 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -36,8 +36,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
 golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
-golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98 h1:Wm3cG5X6sZ0RSVRc/H1/sciC4AT6HAKgLCSH2lbpR/c=
-golang.org/x/telemetry v0.0.0-20240829154258-f29ab539cc98/go.mod h1:m7R/r+o5h7UvF2JD9n2iLSGY4v8v+zNSyTJ6xynLrqs=
+golang.org/x/telemetry v0.0.0-20240927184629-19675431963b h1:PfPrmVDHfPgLVpiYnf2R1uL8SCXBjkqT51+f/fQHR6Q=
+golang.org/x/telemetry v0.0.0-20240927184629-19675431963b/go.mod h1:PsFMgI0jiuY7j+qwXANuh9a/x5kQESTSnRow3gapUyk=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
 golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=

From 7d92dd6f9b6fd569213b4c4ae7e2366fe141ca4f Mon Sep 17 00:00:00 2001
From: xieyuschen 
Date: Mon, 23 Sep 2024 18:17:31 +0800
Subject: [PATCH 084/102] go/ssa/ssautil: use go/packages to load packages in
 switch_test.go

Updates golang/go#69556

Change-Id: I5459a835b96f6f79d193538fdbc85a3e1c7324cc
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615035
LUCI-TryBot-Result: Go LUCI 
Commit-Queue: Tim King 
Reviewed-by: Alan Donovan 
Auto-Submit: Alan Donovan 
Reviewed-by: Tim King 
---
 go/ssa/ssautil/switch_test.go                 | 31 +++++++++++++++++--
 .../testdata/{switches.go => switches.txtar}  |  5 ++-
 2 files changed, 33 insertions(+), 3 deletions(-)
 rename go/ssa/ssautil/testdata/{switches.go => switches.txtar} (99%)

diff --git a/go/ssa/ssautil/switch_test.go b/go/ssa/ssautil/switch_test.go
index 6db41052454..ed7d2d5f93b 100644
--- a/go/ssa/ssautil/switch_test.go
+++ b/go/ssa/ssautil/switch_test.go
@@ -10,18 +10,43 @@
 package ssautil_test
 
 import (
+	"go/ast"
 	"go/parser"
+	"path/filepath"
 	"strings"
 	"testing"
 
 	"golang.org/x/tools/go/loader"
 	"golang.org/x/tools/go/ssa"
 	"golang.org/x/tools/go/ssa/ssautil"
+	"golang.org/x/tools/internal/testfiles"
+	"golang.org/x/tools/txtar"
 )
 
 func TestSwitches(t *testing.T) {
+	archive, err := txtar.ParseFile("testdata/switches.txtar")
+	if err != nil {
+		t.Fatal(err)
+	}
+	ppkgs := testfiles.LoadPackages(t, archive, ".")
+	if len(ppkgs) != 1 {
+		t.Fatalf("Expected to load one package but got %d", len(ppkgs))
+	}
+
+	prog, _ := ssautil.Packages(ppkgs, ssa.BuilderMode(0))
+	mainPkg := prog.Package(ppkgs[0].Types)
+	mainPkg.Build()
+	testSwitches(t, ppkgs[0].Syntax[0], mainPkg)
+}
+
+// TestCreateProgram uses loader and ssautil.CreateProgram to create an *ssa.Program.
+// It has the same testing logic with TestSwitches.
+// CreateProgram is deprecated, but it is a part of the public API.
+// For now keep a test that exercises it.
+func TestCreateProgram(t *testing.T) {
+	dir := testfiles.ExtractTxtarFileToTmp(t, "testdata/switches.txtar")
 	conf := loader.Config{ParserMode: parser.ParseComments}
-	f, err := conf.ParseFile("testdata/switches.go", nil)
+	f, err := conf.ParseFile(filepath.Join(dir, "switches.go"), nil)
 	if err != nil {
 		t.Error(err)
 		return
@@ -33,11 +58,13 @@ func TestSwitches(t *testing.T) {
 		t.Error(err)
 		return
 	}
-
 	prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0))
 	mainPkg := prog.Package(iprog.Created[0].Pkg)
 	mainPkg.Build()
+	testSwitches(t, f, mainPkg)
+}
 
+func testSwitches(t *testing.T, f *ast.File, mainPkg *ssa.Package) {
 	for _, mem := range mainPkg.Members {
 		if fn, ok := mem.(*ssa.Function); ok {
 			if fn.Synthetic != "" {
diff --git a/go/ssa/ssautil/testdata/switches.go b/go/ssa/ssautil/testdata/switches.txtar
similarity index 99%
rename from go/ssa/ssautil/testdata/switches.go
rename to go/ssa/ssautil/testdata/switches.txtar
index 8ab4c118f16..1f0d96c58d9 100644
--- a/go/ssa/ssautil/testdata/switches.go
+++ b/go/ssa/ssautil/testdata/switches.txtar
@@ -1,5 +1,8 @@
-// +build ignore
+-- go.mod --
+module example.com
+go 1.22
 
+-- switches.go --
 package main
 
 // This file is the input to TestSwitches in switch_test.go.

From 66afc1a4e787e59f4d0849b353402c2b8b51a5db Mon Sep 17 00:00:00 2001
From: Muir Manders 
Date: Thu, 20 Jun 2024 22:26:01 -0700
Subject: [PATCH 085/102] gopls/completion: tweak fuzz handling to make its own
 MethodSet

I'm going to try changing methodsAndFields() to not use
types.MethodSet anymore. The `*testing.F` handling depends on the
MethodSet, so let's change it to make its own.

Change-Id: Iba81a58eb66142ede87f2be9125ae08753fb1700
Reviewed-on: https://go-review.googlesource.com/c/tools/+/594235
TryBot-Result: Gopher Robot 
Reviewed-by: Joedian Reid 
Reviewed-by: Robert Findley 
LUCI-TryBot-Result: Go LUCI 
Run-TryBot: Muir Manders 
---
 gopls/internal/golang/completion/completion.go | 14 +++++++-------
 gopls/internal/golang/completion/fuzz.go       |  4 +++-
 2 files changed, 10 insertions(+), 8 deletions(-)

diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go
index ca9ecba5a0d..cf398693113 100644
--- a/gopls/internal/golang/completion/completion.go
+++ b/gopls/internal/golang/completion/completion.go
@@ -1496,6 +1496,13 @@ func ignoreUnimportedCompletion(fix *imports.ImportFix) bool {
 }
 
 func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *importInfo, cb func(candidate)) {
+	if isStarTestingDotF(typ) {
+		// is that a sufficient test? (or is more care needed?)
+		if c.fuzz(typ, imp, cb) {
+			return
+		}
+	}
+
 	mset := c.methodSetCache[methodSetKey{typ, addressable}]
 	if mset == nil {
 		if addressable && !types.IsInterface(typ) && !isPointer(typ) {
@@ -1508,13 +1515,6 @@ func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *impo
 		c.methodSetCache[methodSetKey{typ, addressable}] = mset
 	}
 
-	if isStarTestingDotF(typ) && addressable {
-		// is that a sufficient test? (or is more care needed?)
-		if c.fuzz(mset, imp, cb) {
-			return
-		}
-	}
-
 	for i := 0; i < mset.Len(); i++ {
 		obj := mset.At(i).Obj()
 		// to the other side of the cb() queue?
diff --git a/gopls/internal/golang/completion/fuzz.go b/gopls/internal/golang/completion/fuzz.go
index 313e7f7b391..3f5ac99c428 100644
--- a/gopls/internal/golang/completion/fuzz.go
+++ b/gopls/internal/golang/completion/fuzz.go
@@ -20,12 +20,14 @@ import (
 // PJW: are there other packages where we can deduce usage constraints?
 
 // if we find fuzz completions, then return true, as those are the only completions to offer
-func (c *completer) fuzz(mset *types.MethodSet, imp *importInfo, cb func(candidate)) bool {
+func (c *completer) fuzz(testingF types.Type, imp *importInfo, cb func(candidate)) bool {
 	// 1. inside f.Fuzz? (only f.Failed and f.Name)
 	// 2. possible completing f.Fuzz?
 	//    [Ident,SelectorExpr,Callexpr,ExprStmt,BlockiStmt,FuncDecl(Fuzz...)]
 	// 3. before f.Fuzz, same (for 2., offer choice when looking at an F)
 
+	mset := types.NewMethodSet(testingF)
+
 	// does the path contain FuncLit as arg to f.Fuzz CallExpr?
 	inside := false
 Loop:

From 6ded0c4495e2d8ea31ad26203648003fe166495a Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Fri, 27 Sep 2024 15:39:01 -0700
Subject: [PATCH 086/102] internal/gcexporter: cleanup test skipping in
 TestImportTypeparamTests

Cleaning up the documentation for skipping tests in 1.22 when gotypesalias=1.

Change-Id: Iaf387ff83d2d771d3a0196d6ca26aa49516799dd
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615699
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Robert Findley 
Auto-Submit: Tim King 
---
 internal/gcimporter/gcimporter_test.go | 31 ++++++++++++++------------
 1 file changed, 17 insertions(+), 14 deletions(-)

diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go
index 904d0828c8a..5519fa08a92 100644
--- a/internal/gcimporter/gcimporter_test.go
+++ b/internal/gcimporter/gcimporter_test.go
@@ -172,22 +172,26 @@ func TestImportTypeparamTests(t *testing.T) {
 		t.Skipf("gc-built packages not available (compiler = %s)", runtime.Compiler)
 	}
 
-	testAliases(t, testImportTypeparamTests)
+	testAliases(t, func(t *testing.T) {
+		var skip map[string]string
+
+		// Add tests to skip.
+		if testenv.Go1Point() == 22 && os.Getenv("GODEBUG") == aliasesOn {
+			// The tests below can be skipped in 1.22 as gotypesalias=1 was experimental.
+			// These do not need to be addressed.
+			skip = map[string]string{
+				"struct.go":     "1.22 differences in formatting a *types.Alias",
+				"issue50259.go": "1.22 cannot compile due to an understood types.Alias bug",
+			}
+		}
+		testImportTypeparamTests(t, skip)
+	})
 }
 
-func testImportTypeparamTests(t *testing.T) {
+func testImportTypeparamTests(t *testing.T, skip map[string]string) {
 	tmpdir := mktmpdir(t)
 	defer os.RemoveAll(tmpdir)
 
-	// GoVersion -> GoDebug -> filename -> reason to skip
-	skips := map[int]map[string]map[string]string{
-		22: {aliasesOn: {
-			"issue50259.go": "internal compiler error: unexpected types2.Invalid",
-			"struct.go":     "badly formatted expectation E[int]",
-		}},
-	}
-	dbg, version := os.Getenv("GODEBUG"), testenv.Go1Point()
-
 	// Check go files in test/typeparam, except those that fail for a known
 	// reason.
 	rootDir := filepath.Join(runtime.GOROOT(), "test", "typeparam")
@@ -203,9 +207,8 @@ func testImportTypeparamTests(t *testing.T) {
 		}
 
 		t.Run(entry.Name(), func(t *testing.T) {
-			if reason := skips[version][dbg][entry.Name()]; reason != "" {
-				t.Skipf("Skipping file %q with GODEBUG=%q due to %q at version 1.%d",
-					entry.Name(), dbg, reason, version)
+			if reason := skip[entry.Name()]; reason != "" {
+				t.Skipf("Skipping due to %s", reason)
 			}
 
 			filename := filepath.Join(rootDir, entry.Name())

From ee661349718cbfc42c63a92c7b7d56ee690f8979 Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Mon, 30 Sep 2024 10:38:36 -0700
Subject: [PATCH 087/102] go/ssa/ssautil: isolate deprecated CreateProgram

Isolate CreateProgram and its test into their own files
to further emphasize that these are deprecated.

Change-Id: I441a456752cc3d1728ab86c98e785c59696980f2
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616976
Reviewed-by: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Tim King 
---
 go/ssa/ssautil/deprecated.go      | 36 +++++++++++++++++++++++
 go/ssa/ssautil/deprecated_test.go | 49 +++++++++++++++++++++++++++++++
 go/ssa/ssautil/load.go            | 24 ---------------
 go/ssa/ssautil/switch_test.go     | 33 +--------------------
 4 files changed, 86 insertions(+), 56 deletions(-)
 create mode 100644 go/ssa/ssautil/deprecated.go
 create mode 100644 go/ssa/ssautil/deprecated_test.go

diff --git a/go/ssa/ssautil/deprecated.go b/go/ssa/ssautil/deprecated.go
new file mode 100644
index 00000000000..4feff7131ac
--- /dev/null
+++ b/go/ssa/ssautil/deprecated.go
@@ -0,0 +1,36 @@
+// Copyright 2015 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 ssautil
+
+// This file contains deprecated public APIs.
+// We discourage their use.
+
+import (
+	"golang.org/x/tools/go/loader"
+	"golang.org/x/tools/go/ssa"
+)
+
+// CreateProgram returns a new program in SSA form, given a program
+// loaded from source.  An SSA package is created for each transitively
+// error-free package of lprog.
+//
+// Code for bodies of functions is not built until Build is called
+// on the result.
+//
+// The mode parameter controls diagnostics and checking during SSA construction.
+//
+// Deprecated: Use [golang.org/x/tools/go/packages] and the [Packages]
+// function instead; see ssa.Example_loadPackages.
+func CreateProgram(lprog *loader.Program, mode ssa.BuilderMode) *ssa.Program {
+	prog := ssa.NewProgram(lprog.Fset, mode)
+
+	for _, info := range lprog.AllPackages {
+		if info.TransitivelyErrorFree {
+			prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable)
+		}
+	}
+
+	return prog
+}
diff --git a/go/ssa/ssautil/deprecated_test.go b/go/ssa/ssautil/deprecated_test.go
new file mode 100644
index 00000000000..9bc39e7eebd
--- /dev/null
+++ b/go/ssa/ssautil/deprecated_test.go
@@ -0,0 +1,49 @@
+// Copyright 2024 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 ssautil_test
+
+// Tests of deprecated public APIs.
+// We are keeping some tests around to have some test of the public API.
+
+import (
+	"go/parser"
+	"os"
+	"testing"
+
+	"golang.org/x/tools/go/loader"
+	"golang.org/x/tools/go/ssa"
+	"golang.org/x/tools/go/ssa/ssautil"
+)
+
+// TestCreateProgram tests CreateProgram which has an x/tools/go/loader.Program.
+func TestCreateProgram(t *testing.T) {
+	conf := loader.Config{ParserMode: parser.ParseComments}
+	f, err := conf.ParseFile("hello.go", hello)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	conf.CreateFromFiles("main", f)
+	iprog, err := conf.Load()
+	if err != nil {
+		t.Fatal(err)
+	}
+	if len(iprog.Created) != 1 {
+		t.Fatalf("Expected 1 Created package. got %d", len(iprog.Created))
+	}
+	pkg := iprog.Created[0].Pkg
+
+	prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0))
+	ssapkg := prog.Package(pkg)
+	ssapkg.Build()
+
+	if pkg.Name() != "main" {
+		t.Errorf("pkg.Name() = %s, want main", pkg.Name())
+	}
+	if ssapkg.Func("main") == nil {
+		ssapkg.WriteTo(os.Stderr)
+		t.Errorf("ssapkg has no main function")
+	}
+}
diff --git a/go/ssa/ssautil/load.go b/go/ssa/ssautil/load.go
index 3daa67a07e4..51fba054541 100644
--- a/go/ssa/ssautil/load.go
+++ b/go/ssa/ssautil/load.go
@@ -11,7 +11,6 @@ import (
 	"go/token"
 	"go/types"
 
-	"golang.org/x/tools/go/loader"
 	"golang.org/x/tools/go/packages"
 	"golang.org/x/tools/go/ssa"
 	"golang.org/x/tools/internal/versions"
@@ -111,29 +110,6 @@ func doPackages(initial []*packages.Package, mode ssa.BuilderMode, deps bool) (*
 	return prog, ssapkgs
 }
 
-// CreateProgram returns a new program in SSA form, given a program
-// loaded from source.  An SSA package is created for each transitively
-// error-free package of lprog.
-//
-// Code for bodies of functions is not built until Build is called
-// on the result.
-//
-// The mode parameter controls diagnostics and checking during SSA construction.
-//
-// Deprecated: Use [golang.org/x/tools/go/packages] and the [Packages]
-// function instead; see ssa.Example_loadPackages.
-func CreateProgram(lprog *loader.Program, mode ssa.BuilderMode) *ssa.Program {
-	prog := ssa.NewProgram(lprog.Fset, mode)
-
-	for _, info := range lprog.AllPackages {
-		if info.TransitivelyErrorFree {
-			prog.CreatePackage(info.Pkg, info.Files, &info.Info, info.Importable)
-		}
-	}
-
-	return prog
-}
-
 // BuildPackage builds an SSA program with SSA intermediate
 // representation (IR) for all functions of a single package.
 //
diff --git a/go/ssa/ssautil/switch_test.go b/go/ssa/ssautil/switch_test.go
index ed7d2d5f93b..081b09010ee 100644
--- a/go/ssa/ssautil/switch_test.go
+++ b/go/ssa/ssautil/switch_test.go
@@ -10,13 +10,9 @@
 package ssautil_test
 
 import (
-	"go/ast"
-	"go/parser"
-	"path/filepath"
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/go/loader"
 	"golang.org/x/tools/go/ssa"
 	"golang.org/x/tools/go/ssa/ssautil"
 	"golang.org/x/tools/internal/testfiles"
@@ -32,39 +28,12 @@ func TestSwitches(t *testing.T) {
 	if len(ppkgs) != 1 {
 		t.Fatalf("Expected to load one package but got %d", len(ppkgs))
 	}
+	f := ppkgs[0].Syntax[0]
 
 	prog, _ := ssautil.Packages(ppkgs, ssa.BuilderMode(0))
 	mainPkg := prog.Package(ppkgs[0].Types)
 	mainPkg.Build()
-	testSwitches(t, ppkgs[0].Syntax[0], mainPkg)
-}
-
-// TestCreateProgram uses loader and ssautil.CreateProgram to create an *ssa.Program.
-// It has the same testing logic with TestSwitches.
-// CreateProgram is deprecated, but it is a part of the public API.
-// For now keep a test that exercises it.
-func TestCreateProgram(t *testing.T) {
-	dir := testfiles.ExtractTxtarFileToTmp(t, "testdata/switches.txtar")
-	conf := loader.Config{ParserMode: parser.ParseComments}
-	f, err := conf.ParseFile(filepath.Join(dir, "switches.go"), nil)
-	if err != nil {
-		t.Error(err)
-		return
-	}
-
-	conf.CreateFromFiles("main", f)
-	iprog, err := conf.Load()
-	if err != nil {
-		t.Error(err)
-		return
-	}
-	prog := ssautil.CreateProgram(iprog, ssa.BuilderMode(0))
-	mainPkg := prog.Package(iprog.Created[0].Pkg)
-	mainPkg.Build()
-	testSwitches(t, f, mainPkg)
-}
 
-func testSwitches(t *testing.T, f *ast.File, mainPkg *ssa.Package) {
 	for _, mem := range mainPkg.Members {
 		if fn, ok := mem.(*ssa.Function); ok {
 			if fn.Synthetic != "" {

From 2848ab8d89e46844ade5dc8b880fcd12e01f196c Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Mon, 30 Sep 2024 10:38:36 -0700
Subject: [PATCH 088/102] internal/gcimporter: clean up test expectations

Use (types.Object).String() to write expectations now that the
formatting for type parameterized aliases was fixed.

This resolves a TODO in TestIExportDataTypeParameterizedAliases.

Change-Id: I5b5d55cb7c190686c89dec59bdad30d83de595a5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616995
Auto-Submit: Tim King 
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Robert Findley 
---
 internal/gcimporter/iexport_test.go | 76 ++++++++---------------------
 1 file changed, 20 insertions(+), 56 deletions(-)

diff --git a/internal/gcimporter/iexport_test.go b/internal/gcimporter/iexport_test.go
index ff3a322001f..cb6ccdd7929 100644
--- a/internal/gcimporter/iexport_test.go
+++ b/internal/gcimporter/iexport_test.go
@@ -24,7 +24,6 @@ import (
 
 	"golang.org/x/tools/go/gcexportdata"
 	"golang.org/x/tools/go/packages"
-	"golang.org/x/tools/internal/aliases"
 	"golang.org/x/tools/internal/gcimporter"
 	"golang.org/x/tools/internal/testenv"
 )
@@ -440,7 +439,7 @@ func TestIExportDataTypeParameterizedAliases(t *testing.T) {
 	// * import the data (via either x/tools or GOROOT's gcimporter), and
 	// * check the imported types.
 
-	const src = `package a
+	const src = `package pkg
 
 type A[T any] = *T
 type B[R any, S *R] = []S
@@ -452,12 +451,12 @@ type Chained = C[Named] // B[Named, A[Named]] = B[Named, *Named] = []*Named
 
 	// parse and typecheck
 	fset1 := token.NewFileSet()
-	f, err := parser.ParseFile(fset1, "a", src, 0)
+	f, err := parser.ParseFile(fset1, "pkg", src, 0)
 	if err != nil {
 		t.Fatal(err)
 	}
 	var conf types.Config
-	pkg1, err := conf.Check("a", fset1, []*ast.File{f}, nil)
+	pkg1, err := conf.Check("pkg", fset1, []*ast.File{f}, nil)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -499,7 +498,7 @@ type Chained = C[Named] // B[Named, A[Named]] = B[Named, *Named] = []*Named
 
 			// Write export data to temporary file
 			out := t.TempDir()
-			name := filepath.Join(out, "a.out")
+			name := filepath.Join(out, "pkg.out")
 			if err := os.WriteFile(name+".a", buf.Bytes(), 0644); err != nil {
 				t.Fatal(err)
 			}
@@ -514,58 +513,23 @@ type Chained = C[Named] // B[Named, A[Named]] = B[Named, *Named] = []*Named
 	for name, importer := range testcases {
 		t.Run(name, func(t *testing.T) {
 			pkg := importer(t)
+			for name, want := range map[string]string{
+				"A":       "type pkg.A[T any] = *T",
+				"B":       "type pkg.B[R any, S *R] = []S",
+				"C":       "type pkg.C[U any] = pkg.B[U, pkg.A[U]]",
+				"Named":   "type pkg.Named int",
+				"Chained": "type pkg.Chained = pkg.C[pkg.Named]",
+			} {
+				obj := pkg.Scope().Lookup(name)
+				if obj == nil {
+					t.Errorf("failed to find %q in package %s", name, pkg)
+					continue
+				}
 
-			obj := pkg.Scope().Lookup("A")
-			if obj == nil {
-				t.Fatalf("failed to find %q in package %s", "A", pkg)
-			}
-
-			// Check that A is type A[T any] = *T.
-			// TODO(taking): fix how go/types prints parameterized aliases to simplify tests.
-			alias, ok := obj.Type().(*types.Alias)
-			if !ok {
-				t.Fatalf("Obj %s is not an Alias", obj)
-			}
-
-			targs := aliases.TypeArgs(alias)
-			if targs.Len() != 0 {
-				t.Errorf("%s has %d type arguments. expected 0", alias, targs.Len())
-			}
-
-			tparams := aliases.TypeParams(alias)
-			if tparams.Len() != 1 {
-				t.Fatalf("%s has %d type arguments. expected 1", alias, targs.Len())
-			}
-			tparam := tparams.At(0)
-			if got, want := tparam.String(), "T"; got != want {
-				t.Errorf("(%q).TypeParams().At(0)=%q. want %q", alias, got, want)
-			}
-
-			anyt := types.Universe.Lookup("any").Type()
-			if c := tparam.Constraint(); !types.Identical(anyt, c) {
-				t.Errorf("(%q).Constraint()=%q. expected %q", tparam, c, anyt)
-			}
-
-			ptparam := types.NewPointer(tparam)
-			if rhs := aliases.Rhs(alias); !types.Identical(ptparam, rhs) {
-				t.Errorf("(%q).Rhs()=%q. expected %q", alias, rhs, ptparam)
-			}
-
-			// TODO(taking): add tests for B and C once it is simpler to write tests.
-
-			chained := pkg.Scope().Lookup("Chained")
-			if chained == nil {
-				t.Fatalf("failed to find %q in package %s", "Chained", pkg)
-			}
-
-			named, _ := pkg.Scope().Lookup("Named").(*types.TypeName)
-			if named == nil {
-				t.Fatalf("failed to find %q in package %s", "Named", pkg)
-			}
-
-			want := types.NewSlice(types.NewPointer(named.Type()))
-			if got := chained.Type(); !types.Identical(got, want) {
-				t.Errorf("(%q).Type()=%q which should be identical to %q", chained, got, want)
+				got := strings.ReplaceAll(obj.String(), pkg.Path(), "pkg")
+				if got != want {
+					t.Errorf("(%q).String()=%q. wanted %q", name, got, want)
+				}
 			}
 		})
 	}

From 60b6bcdd609c7907ae6ac274bee99d7b0689b705 Mon Sep 17 00:00:00 2001
From: Chiawen Chen 
Date: Sun, 29 Sep 2024 13:16:30 +0800
Subject: [PATCH 089/102] gopls/rename: include filename in error message

Error messages now display the filename along with the package name
when a rename is blocked.

Fixes golang/go#69563

Change-Id: I00277281a78134271f7a0a7e8497a5863e48b92d
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615686
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Robert Findley 
Reviewed-by: Michael Knyszek 
Auto-Submit: Robert Findley 
---
 gopls/internal/golang/rename_check.go              | 12 ++++++++++--
 gopls/internal/test/marker/testdata/rename/bad.txt | 12 +++++++++++-
 2 files changed, 21 insertions(+), 3 deletions(-)

diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go
index 1dda36169f7..ed6424c918f 100644
--- a/gopls/internal/golang/rename_check.go
+++ b/gopls/internal/golang/rename_check.go
@@ -866,9 +866,17 @@ func (r *renamer) satisfy() map[satisfy.Constraint]bool {
 			//
 			// Only proceed if all packages have no errors.
 			if len(pkg.ParseErrors()) > 0 || len(pkg.TypeErrors()) > 0 {
+				var filename string
+				if len(pkg.ParseErrors()) > 0 {
+					err := pkg.ParseErrors()[0][0]
+					filename = filepath.Base(err.Pos.Filename)
+				} else if len(pkg.TypeErrors()) > 0 {
+					err := pkg.TypeErrors()[0]
+					filename = filepath.Base(err.Fset.File(err.Pos).Name())
+				}
 				r.errorf(token.NoPos, // we don't have a position for this error.
-					"renaming %q to %q not possible because %q has errors",
-					r.from, r.to, pkg.Metadata().PkgPath)
+					"renaming %q to %q not possible because %q in %q has errors",
+					r.from, r.to, filename, pkg.Metadata().PkgPath)
 				return nil
 			}
 			f.Find(pkg.TypesInfo(), pkg.Syntax())
diff --git a/gopls/internal/test/marker/testdata/rename/bad.txt b/gopls/internal/test/marker/testdata/rename/bad.txt
index c596ad13c92..882989cacef 100644
--- a/gopls/internal/test/marker/testdata/rename/bad.txt
+++ b/gopls/internal/test/marker/testdata/rename/bad.txt
@@ -11,9 +11,19 @@ package bad
 type myStruct struct {
 }
 
-func (s *myStruct) sFunc() bool { //@renameerr("sFunc", "rFunc", re"not possible")
+func (s *myStruct) sFunc() bool { //@renameerr("sFunc", "rFunc", "not possible because \"bad.go\" in \"golang.org/lsptests/bad\" has errors")
 	return s.Bad //@diag("Bad", re"no field or method")
 }
 
 -- bad_test.go --
 package bad
+
+
+-- badsyntax/badsyntax.go --
+package badsyntax
+
+type S struct {}
+
+func (s *S) sFunc() bool { //@renameerr("sFunc", "rFunc", "not possible because \"badsyntax.go\" in \"golang.org/lsptests/bad/badsyntax\" has errors")
+	# //@diag("#", re"expected statement, found")
+}

From 4e80b325ef5069318ea186ffbcb3b1ed440f52fe Mon Sep 17 00:00:00 2001
From: Viktor Blomqvist 
Date: Fri, 9 Aug 2024 16:51:35 +0200
Subject: [PATCH 090/102] cmd/stringer: support types declared in test files

Adds additional passes over the test variants of the given
package.  Picks the most "narrow" (least amount of files)
package where each type can be found. The generated code
will be placed in the same package and test-or-not-test file
as the original type declaration.

Closes golang/go#66557.

Change-Id: I2354cd44e357a9ce0b45f5e2fadc1c141455a88d
Reviewed-on: https://go-review.googlesource.com/c/tools/+/604535
Reviewed-by: Robert Findley 
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Robert Findley 
Reviewed-by: Michael Knyszek 
---
 cmd/stringer/endtoend_test.go  | 123 +++++++++
 cmd/stringer/golden_test.go    |  19 +-
 cmd/stringer/multifile_test.go | 464 +++++++++++++++++++++++++++++++++
 cmd/stringer/stringer.go       | 200 +++++++++-----
 4 files changed, 736 insertions(+), 70 deletions(-)
 create mode 100644 cmd/stringer/multifile_test.go

diff --git a/cmd/stringer/endtoend_test.go b/cmd/stringer/endtoend_test.go
index 2b9afa370c5..2b7d6a786a5 100644
--- a/cmd/stringer/endtoend_test.go
+++ b/cmd/stringer/endtoend_test.go
@@ -17,6 +17,8 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"reflect"
+	"slices"
 	"strings"
 	"sync"
 	"testing"
@@ -197,6 +199,127 @@ func TestConstValueChange(t *testing.T) {
 	}
 }
 
+var testfileSrcs = map[string]string{
+	"go.mod": "module foo",
+
+	// Normal file in the package.
+	"main.go": `package foo
+
+type Foo int
+
+const (
+	fooX Foo = iota
+	fooY
+	fooZ
+)
+`,
+
+	// Test file in the package.
+	"main_test.go": `package foo
+
+type Bar int
+
+const (
+	barX Bar = iota
+	barY
+	barZ
+)
+`,
+
+	// Test file in the test package.
+	"main_pkg_test.go": `package foo_test
+
+type Baz int
+
+const (
+	bazX Baz = iota
+	bazY
+	bazZ
+)
+`,
+}
+
+// Test stringer on types defined in different kinds of tests.
+// The generated code should not interfere between itself.
+func TestTestFiles(t *testing.T) {
+	testenv.NeedsTool(t, "go")
+	stringer := stringerPath(t)
+
+	dir := t.TempDir()
+	t.Logf("TestTestFiles in: %s \n", dir)
+	for name, src := range testfileSrcs {
+		source := filepath.Join(dir, name)
+		err := os.WriteFile(source, []byte(src), 0666)
+		if err != nil {
+			t.Fatalf("write file: %s", err)
+		}
+	}
+
+	// Must run stringer in the temp directory, see TestTags.
+	err := runInDir(t, dir, stringer, "-type=Foo,Bar,Baz", dir)
+	if err != nil {
+		t.Fatalf("run stringer: %s", err)
+	}
+
+	// Check that stringer has created the expected files.
+	content, err := os.ReadDir(dir)
+	if err != nil {
+		t.Fatalf("read dir: %s", err)
+	}
+	gotFiles := []string{}
+	for _, f := range content {
+		if !f.IsDir() {
+			gotFiles = append(gotFiles, f.Name())
+		}
+	}
+	wantFiles := []string{
+		// Original.
+		"go.mod",
+		"main.go",
+		"main_test.go",
+		"main_pkg_test.go",
+		// Generated.
+		"foo_string.go",
+		"bar_string_test.go",
+		"baz_string_test.go",
+	}
+	slices.Sort(gotFiles)
+	slices.Sort(wantFiles)
+	if !reflect.DeepEqual(gotFiles, wantFiles) {
+		t.Errorf("stringer generated files:\n%s\n\nbut want:\n%s",
+			strings.Join(gotFiles, "\n"),
+			strings.Join(wantFiles, "\n"),
+		)
+	}
+
+	// Run go test as a smoke test.
+	err = runInDir(t, dir, "go", "test", "-count=1", ".")
+	if err != nil {
+		t.Fatalf("go test: %s", err)
+	}
+}
+
+// The -output flag cannot be used in combiation with matching types across multiple packages.
+func TestCollidingOutput(t *testing.T) {
+	testenv.NeedsTool(t, "go")
+	stringer := stringerPath(t)
+
+	dir := t.TempDir()
+	for name, src := range testfileSrcs {
+		source := filepath.Join(dir, name)
+		err := os.WriteFile(source, []byte(src), 0666)
+		if err != nil {
+			t.Fatalf("write file: %s", err)
+		}
+	}
+
+	// Must run stringer in the temp directory, see TestTags.
+	err := runInDir(t, dir, stringer, "-type=Foo,Bar,Baz", "-output=somefile.go", dir)
+	if err == nil {
+		t.Fatal("unexpected stringer success")
+	}
+}
+
 var exe struct {
 	path string
 	err  error
diff --git a/cmd/stringer/golden_test.go b/cmd/stringer/golden_test.go
index a26eef35e36..2a81c0855aa 100644
--- a/cmd/stringer/golden_test.go
+++ b/cmd/stringer/golden_test.go
@@ -455,11 +455,6 @@ func TestGolden(t *testing.T) {
 	for _, test := range golden {
 		test := test
 		t.Run(test.name, func(t *testing.T) {
-			g := Generator{
-				trimPrefix:  test.trimPrefix,
-				lineComment: test.lineComment,
-				logf:        t.Logf,
-			}
 			input := "package test\n" + test.input
 			file := test.name + ".go"
 			absFile := filepath.Join(dir, file)
@@ -468,16 +463,24 @@ func TestGolden(t *testing.T) {
 				t.Fatal(err)
 			}
 
-			g.parsePackage([]string{absFile}, nil)
+			pkgs := loadPackages([]string{absFile}, nil, test.trimPrefix, test.lineComment, t.Logf)
+			if len(pkgs) != 1 {
+				t.Fatalf("got %d parsed packages but expected 1", len(pkgs))
+			}
 			// Extract the name and type of the constant from the first line.
 			tokens := strings.SplitN(test.input, " ", 3)
 			if len(tokens) != 3 {
 				t.Fatalf("%s: need type declaration on first line", test.name)
 			}
-			g.generate(tokens[1])
+
+			g := Generator{
+				pkg:  pkgs[0],
+				logf: t.Logf,
+			}
+			g.generate(tokens[1], findValues(tokens[1], pkgs[0]))
 			got := string(g.format())
 			if got != test.output {
-				t.Errorf("%s: got(%d)\n====\n%q====\nexpected(%d)\n====%q", test.name, len(got), got, len(test.output), test.output)
+				t.Errorf("%s: got(%d)\n====\n%q====\nexpected(%d)\n====\n%q", test.name, len(got), got, len(test.output), test.output)
 			}
 		})
 	}
diff --git a/cmd/stringer/multifile_test.go b/cmd/stringer/multifile_test.go
new file mode 100644
index 00000000000..7a7ae669065
--- /dev/null
+++ b/cmd/stringer/multifile_test.go
@@ -0,0 +1,464 @@
+// Copyright 2024 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.
+
+// For os.CopyFS
+//go:build go1.23
+
+package main
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"golang.org/x/tools/internal/diffp"
+	"golang.org/x/tools/internal/testenv"
+	"golang.org/x/tools/txtar"
+)
+
+// This file contains a test that checks the output files existence
+// and content when stringer has types from multiple different input
+// files to choose from.
+//
+// Input is specified in a txtar string.
+
+// Several tests expect the type Foo generated in some package.
+func expectFooString(pkg string) []byte {
+	return []byte(fmt.Sprintf(`
+// Header comment ignored.
+
+package %s
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[fooX-0]
+	_ = x[fooY-1]
+	_ = x[fooZ-2]
+}
+
+const _Foo_name = "fooXfooYfooZ"
+
+var _Foo_index = [...]uint8{0, 4, 8, 12}
+
+func (i Foo) String() string {
+	if i < 0 || i >= Foo(len(_Foo_index)-1) {
+		return "Foo(" + strconv.FormatInt(int64(i), 10) + ")"
+	}
+	return _Foo_name[_Foo_index[i]:_Foo_index[i+1]]
+}`, pkg))
+}
+
+func TestMultifileStringer(t *testing.T) {
+	testenv.NeedsTool(t, "go")
+	stringer := stringerPath(t)
+
+	tests := []struct {
+		name        string
+		args        []string
+		archive     []byte
+		expectFiles map[string][]byte
+	}{
+		{
+			name: "package only",
+			args: []string{"-type=Foo"},
+			archive: []byte(`
+-- go.mod --
+module foo
+
+-- main.go --
+package main
+
+type Foo int
+
+const (
+	fooX Foo = iota
+	fooY
+	fooZ
+)`),
+			expectFiles: map[string][]byte{
+				"foo_string.go": expectFooString("main"),
+			},
+		},
+		{
+			name: "test package only",
+			args: []string{"-type=Foo"},
+			archive: []byte(`
+-- go.mod --
+module foo
+
+-- main.go --
+package main
+
+func main() {}
+
+-- main_test.go --
+package main
+
+type Foo int
+
+const (
+	fooX Foo = iota
+	fooY
+	fooZ
+)`),
+			expectFiles: map[string][]byte{
+				"foo_string_test.go": expectFooString("main"),
+			},
+		},
+		{
+			name: "x_test package only",
+			args: []string{"-type=Foo"},
+			archive: []byte(`
+-- go.mod --
+module foo
+
+-- main.go --
+package main
+
+func main() {}
+
+-- main_test.go --
+package main_test
+
+type Foo int
+
+const (
+	fooX Foo = iota
+	fooY
+	fooZ
+)`),
+			expectFiles: map[string][]byte{
+				"foo_string_test.go": expectFooString("main_test"),
+			},
+		},
+		{
+			// Re-declaring the type in a less prioritized package does not change our output.
+			name: "package over test package",
+			args: []string{"-type=Foo"},
+			archive: []byte(`
+-- go.mod --
+module foo
+
+-- main.go --
+package main
+
+type Foo int
+
+const (
+	fooX Foo = iota
+	fooY
+	fooZ
+)
+
+-- main_test.go --
+package main
+
+type Foo int
+
+const (
+	otherX Foo = iota
+	otherY
+	otherZ
+)
+`),
+			expectFiles: map[string][]byte{
+				"foo_string.go": expectFooString("main"),
+			},
+		},
+		{
+			// Re-declaring the type in a less prioritized package does not change our output.
+			name: "package over x_test package",
+			args: []string{"-type=Foo"},
+			archive: []byte(`
+-- go.mod --
+module foo
+
+-- main.go --
+package main
+
+type Foo int
+
+const (
+	fooX Foo = iota
+	fooY
+	fooZ
+)
+
+-- main_test.go --
+package main_test
+
+type Foo int
+
+const (
+	otherX Foo = iota
+	otherY
+	otherZ
+)
+`),
+			expectFiles: map[string][]byte{
+				"foo_string.go": expectFooString("main"),
+			},
+		},
+		{
+			// Re-declaring the type in a less prioritized package does not change our output.
+			name: "test package over x_test package",
+			args: []string{"-type=Foo"},
+			archive: []byte(`
+-- go.mod --
+module foo
+
+-- main.go --
+package main
+
+-- main_test.go --
+package main
+
+type Foo int
+
+const (
+	fooX Foo = iota
+	fooY
+	fooZ
+)
+
+-- main_pkg_test.go --
+package main_test
+
+type Foo int
+
+const (
+	otherX Foo = iota
+	otherY
+	otherZ
+)`),
+			expectFiles: map[string][]byte{
+				"foo_string_test.go": expectFooString("main"),
+			},
+		},
+		{
+			name: "unique type in each package variant",
+			args: []string{"-type=Foo,Bar,Baz"},
+			archive: []byte(`
+-- go.mod --
+module foo
+
+-- main.go --
+package main
+
+type Foo int
+
+const fooX Foo = 1
+
+-- main_test.go --
+package main
+
+type Bar int
+
+const barX Bar = 1
+
+-- main_pkg_test.go --
+package main_test
+
+type Baz int
+
+const bazX Baz = 1
+`),
+			expectFiles: map[string][]byte{
+				"foo_string.go": []byte(`
+// Header comment ignored.
+
+package main
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[fooX-1]
+}
+
+const _Foo_name = "fooX"
+
+var _Foo_index = [...]uint8{0, 4}
+
+func (i Foo) String() string {
+	i -= 1
+	if i < 0 || i >= Foo(len(_Foo_index)-1) {
+		return "Foo(" + strconv.FormatInt(int64(i+1), 10) + ")"
+	}
+	return _Foo_name[_Foo_index[i]:_Foo_index[i+1]]
+}`),
+
+				"bar_string_test.go": []byte(`
+// Header comment ignored.
+
+package main
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[barX-1]
+}
+
+const _Bar_name = "barX"
+
+var _Bar_index = [...]uint8{0, 4}
+
+func (i Bar) String() string {
+	i -= 1
+	if i < 0 || i >= Bar(len(_Bar_index)-1) {
+		return "Bar(" + strconv.FormatInt(int64(i+1), 10) + ")"
+	}
+	return _Bar_name[_Bar_index[i]:_Bar_index[i+1]]
+}`),
+
+				"baz_string_test.go": []byte(`
+// Header comment ignored.
+
+package main_test
+
+import "strconv"
+
+func _() {
+	// An "invalid array index" compiler error signifies that the constant values have changed.
+	// Re-run the stringer command to generate them again.
+	var x [1]struct{}
+	_ = x[bazX-1]
+}
+
+const _Baz_name = "bazX"
+
+var _Baz_index = [...]uint8{0, 4}
+
+func (i Baz) String() string {
+	i -= 1
+	if i < 0 || i >= Baz(len(_Baz_index)-1) {
+		return "Baz(" + strconv.FormatInt(int64(i+1), 10) + ")"
+	}
+	return _Baz_name[_Baz_index[i]:_Baz_index[i+1]]
+}`),
+			},
+		},
+
+		{
+			name: "package over test package with custom output",
+			args: []string{"-type=Foo", "-output=custom_output.go"},
+			archive: []byte(`
+-- go.mod --
+module foo
+
+-- main.go --
+package main
+
+type Foo int
+
+const (
+	fooX Foo = iota
+	fooY
+	fooZ
+)
+
+-- main_test.go --
+package main
+
+type Foo int
+
+const (
+	otherX Foo = iota
+	otherY
+	otherZ
+)`),
+			expectFiles: map[string][]byte{
+				"custom_output.go": expectFooString("main"),
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tmpDir := t.TempDir()
+
+			arFS, err := txtar.FS(txtar.Parse(tt.archive))
+			if err != nil {
+				t.Fatalf("txtar.FS: %s", err)
+			}
+			err = os.CopyFS(tmpDir, arFS)
+			if err != nil {
+				t.Fatalf("copy fs: %s", err)
+			}
+			before := dirContent(t, tmpDir)
+
+			// Must run stringer in the temp directory, see TestTags.
+			args := append(tt.args, tmpDir)
+			err = runInDir(t, tmpDir, stringer, args...)
+			if err != nil {
+				t.Fatalf("run stringer: %s", err)
+			}
+
+			// Check that all !path files have been created with the expected content.
+			for f, want := range tt.expectFiles {
+				got, err := os.ReadFile(filepath.Join(tmpDir, f))
+				if errors.Is(err, os.ErrNotExist) {
+					t.Errorf("expected file not written during test: %s", f)
+					continue
+				}
+				if err != nil {
+					t.Fatalf("read file %q: %s", f, err)
+				}
+				// Trim data for more robust comparison.
+				got = trimHeader(bytes.TrimSpace(got))
+				want = trimHeader(bytes.TrimSpace(want))
+				if !bytes.Equal(want, got) {
+					t.Errorf("file %s does not have the expected content:\n%s", f, diffp.Diff("want", want, "got", got))
+				}
+			}
+
+			// Check that nothing else has been created.
+			after := dirContent(t, tmpDir)
+			for f := range after {
+				if _, expected := tt.expectFiles[f]; !expected && !before[f] {
+					t.Errorf("found %q in output directory, it is neither input or expected output", f)
+				}
+			}
+
+		})
+	}
+}
+
+func dirContent(t *testing.T, dir string) map[string]bool {
+	entries, err := os.ReadDir(dir)
+	if err != nil {
+		t.Fatalf("read dir: %s", err)
+	}
+
+	out := map[string]bool{}
+	for _, e := range entries {
+		out[e.Name()] = true
+	}
+	return out
+}
+
+// trimHeader that stringer puts in file.
+// It depends on location and interferes with comparing file content.
+func trimHeader(s []byte) []byte {
+	if !bytes.HasPrefix(s, []byte("//")) {
+		return s
+	}
+	_, after, ok := bytes.Cut(s, []byte{'\n'})
+	if ok {
+		return after
+	}
+	return s
+}
diff --git a/cmd/stringer/stringer.go b/cmd/stringer/stringer.go
index 2b19c93e8ea..09be11ca58e 100644
--- a/cmd/stringer/stringer.go
+++ b/cmd/stringer/stringer.go
@@ -58,6 +58,11 @@
 // where t is the lower-cased name of the first type listed. It can be overridden
 // with the -output flag.
 //
+// Types can also be declared in tests, in which case type declarations in the
+// non-test package or its test variant are preferred over types defined in the
+// package with suffix "_test".
+// The default output file for type declarations in tests is t_string_test.go with t picked as above.
+//
 // The -linecomment flag tells stringer to generate the text of any line comment, trimmed
 // of leading spaces, instead of the constant name. For instance, if the constants above had a
 // Pill prefix, one could write
@@ -128,10 +133,6 @@ func main() {
 
 	// Parse the package once.
 	var dir string
-	g := Generator{
-		trimPrefix:  *trimprefix,
-		lineComment: *linecomment,
-	}
 	// TODO(suzmue): accept other patterns for packages (directories, list of files, import paths, etc).
 	if len(args) == 1 && isDirectory(args[0]) {
 		dir = args[0]
@@ -142,33 +143,90 @@ func main() {
 		dir = filepath.Dir(args[0])
 	}
 
-	g.parsePackage(args, tags)
+	// For each type, generate code in the first package where the type is declared.
+	// The order of packages is as follows:
+	// package x
+	// package x compiled for tests
+	// package x_test
+	//
+	// Each package pass could result in a separate generated file.
+	// These files must have the same package and test/not-test nature as the types
+	// from which they were generated.
+	//
+	// Types will be excluded when generated, to avoid repetitions.
+	pkgs := loadPackages(args, tags, *trimprefix, *linecomment, nil /* logf */)
+	sort.Slice(pkgs, func(i, j int) bool {
+		// Put x_test packages last.
+		iTest := strings.HasSuffix(pkgs[i].name, "_test")
+		jTest := strings.HasSuffix(pkgs[j].name, "_test")
+		if iTest != jTest {
+			return !iTest
+		}
 
-	// Print the header and package clause.
-	g.Printf("// Code generated by \"stringer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
-	g.Printf("\n")
-	g.Printf("package %s", g.pkg.name)
-	g.Printf("\n")
-	g.Printf("import \"strconv\"\n") // Used by all methods.
+		return len(pkgs[i].files) < len(pkgs[j].files)
+	})
+	for _, pkg := range pkgs {
+		g := Generator{
+			pkg: pkg,
+		}
 
-	// Run generate for each type.
-	for _, typeName := range types {
-		g.generate(typeName)
+		// Print the header and package clause.
+		g.Printf("// Code generated by \"stringer %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
+		g.Printf("\n")
+		g.Printf("package %s", g.pkg.name)
+		g.Printf("\n")
+		g.Printf("import \"strconv\"\n") // Used by all methods.
+
+		// Run generate for types that can be found. Keep the rest for the remainingTypes iteration.
+		var foundTypes, remainingTypes []string
+		for _, typeName := range types {
+			values := findValues(typeName, pkg)
+			if len(values) > 0 {
+				g.generate(typeName, values)
+				foundTypes = append(foundTypes, typeName)
+			} else {
+				remainingTypes = append(remainingTypes, typeName)
+			}
+		}
+		if len(foundTypes) == 0 {
+			// This package didn't have any of the relevant types, skip writing a file.
+			continue
+		}
+		if len(remainingTypes) > 0 && output != nil && *output != "" {
+			log.Fatalf("cannot write to single file (-output=%q) when matching types are found in multiple packages", *output)
+		}
+		types = remainingTypes
+
+		// Format the output.
+		src := g.format()
+
+		// Write to file.
+		outputName := *output
+		if outputName == "" {
+			// Type names will be unique across packages since only the first
+			// match is picked.
+			// So there won't be collisions between a package compiled for tests
+			// and the separate package of tests (package foo_test).
+			outputName = filepath.Join(dir, baseName(pkg, foundTypes[0]))
+		}
+		err := os.WriteFile(outputName, src, 0644)
+		if err != nil {
+			log.Fatalf("writing output: %s", err)
+		}
 	}
 
-	// Format the output.
-	src := g.format()
-
-	// Write to file.
-	outputName := *output
-	if outputName == "" {
-		baseName := fmt.Sprintf("%s_string.go", types[0])
-		outputName = filepath.Join(dir, strings.ToLower(baseName))
+	if len(types) > 0 {
+		log.Fatalf("no values defined for types: %s", strings.Join(types, ","))
 	}
-	err := os.WriteFile(outputName, src, 0644)
-	if err != nil {
-		log.Fatalf("writing output: %s", err)
+}
+
+// baseName that will put the generated code together with pkg.
+func baseName(pkg *Package, typename string) string {
+	suffix := "string.go"
+	if pkg.hasTestFiles {
+		suffix = "string_test.go"
 	}
+	return fmt.Sprintf("%s_%s", strings.ToLower(typename), suffix)
 }
 
 // isDirectory reports whether the named file is a directory.
@@ -186,9 +244,6 @@ type Generator struct {
 	buf bytes.Buffer // Accumulated output.
 	pkg *Package     // Package we are scanning.
 
-	trimPrefix  string
-	lineComment bool
-
 	logf func(format string, args ...interface{}) // test logging hook; nil when not testing
 }
 
@@ -209,54 +264,74 @@ type File struct {
 }
 
 type Package struct {
-	name  string
-	defs  map[*ast.Ident]types.Object
-	files []*File
+	name         string
+	defs         map[*ast.Ident]types.Object
+	files        []*File
+	hasTestFiles bool
 }
 
-// parsePackage analyzes the single package constructed from the patterns and tags.
-// parsePackage exits if there is an error.
-func (g *Generator) parsePackage(patterns []string, tags []string) {
+// loadPackages analyzes the single package constructed from the patterns and tags.
+// loadPackages exits if there is an error.
+//
+// Returns all variants (such as tests) of the package.
+//
+// logf is a test logging hook. It can be nil when not testing.
+func loadPackages(
+	patterns, tags []string,
+	trimPrefix string, lineComment bool,
+	logf func(format string, args ...interface{}),
+) []*Package {
 	cfg := &packages.Config{
-		Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax,
-		// TODO: Need to think about constants in test files. Maybe write type_string_test.go
-		// in a separate pass? For later.
-		Tests:      false,
+		Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedFiles,
+		// Tests are included, let the caller decide how to fold them in.
+		Tests:      true,
 		BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))},
-		Logf:       g.logf,
+		Logf:       logf,
 	}
 	pkgs, err := packages.Load(cfg, patterns...)
 	if err != nil {
 		log.Fatal(err)
 	}
-	if len(pkgs) != 1 {
-		log.Fatalf("error: %d packages matching %v", len(pkgs), strings.Join(patterns, " "))
+	if len(pkgs) == 0 {
+		log.Fatalf("error: no packages matching %v", strings.Join(patterns, " "))
 	}
-	g.addPackage(pkgs[0])
-}
 
-// addPackage adds a type checked Package and its syntax files to the generator.
-func (g *Generator) addPackage(pkg *packages.Package) {
-	g.pkg = &Package{
-		name:  pkg.Name,
-		defs:  pkg.TypesInfo.Defs,
-		files: make([]*File, len(pkg.Syntax)),
-	}
+	out := make([]*Package, len(pkgs))
+	for i, pkg := range pkgs {
+		p := &Package{
+			name:  pkg.Name,
+			defs:  pkg.TypesInfo.Defs,
+			files: make([]*File, len(pkg.Syntax)),
+		}
+
+		for j, file := range pkg.Syntax {
+			p.files[j] = &File{
+				file: file,
+				pkg:  p,
 
-	for i, file := range pkg.Syntax {
-		g.pkg.files[i] = &File{
-			file:        file,
-			pkg:         g.pkg,
-			trimPrefix:  g.trimPrefix,
-			lineComment: g.lineComment,
+				trimPrefix:  trimPrefix,
+				lineComment: lineComment,
+			}
 		}
+
+		// Keep track of test files, since we might want to generated
+		// code that ends up in that kind of package.
+		// Can be replaced once https://go.dev/issue/38445 lands.
+		for _, f := range pkg.GoFiles {
+			if strings.HasSuffix(f, "_test.go") {
+				p.hasTestFiles = true
+				break
+			}
+		}
+
+		out[i] = p
 	}
+	return out
 }
 
-// generate produces the String method for the named type.
-func (g *Generator) generate(typeName string) {
+func findValues(typeName string, pkg *Package) []Value {
 	values := make([]Value, 0, 100)
-	for _, file := range g.pkg.files {
+	for _, file := range pkg.files {
 		// Set the state for this run of the walker.
 		file.typeName = typeName
 		file.values = nil
@@ -265,10 +340,11 @@ func (g *Generator) generate(typeName string) {
 			values = append(values, file.values...)
 		}
 	}
+	return values
+}
 
-	if len(values) == 0 {
-		log.Fatalf("no values defined for type %s", typeName)
-	}
+// generate produces the String method for the named type.
+func (g *Generator) generate(typeName string, values []Value) {
 	// Generate code that will fail if the constants change value.
 	g.Printf("func _() {\n")
 	g.Printf("\t// An \"invalid array index\" compiler error signifies that the constant values have changed.\n")

From d2e46216ad6388cf93e30c1999171c38215a0919 Mon Sep 17 00:00:00 2001
From: Alan Donovan 
Date: Mon, 30 Sep 2024 16:10:20 -0400
Subject: [PATCH 091/102] gopls/internal/server: CodeAction: interpret Only=[]
 as [QuickFix]
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This CL changes the interpretation of an empty list of
CodeActionKind. Previously, we have always used it to mean
"all kinds"; however, the new guidance in the LSP 3.18 spec
is that servers should treat it equivalent to [QuickFix].

Following the spec exactly would reduce the frequency of
distracting lightbulbs displayed by VS Code's ⌘-. menu
for actions that are not fixes (e.g. Inline call to f).
But it would deny most clients (VS Code, Emacs, Vim, ...) the
natural way to ask the server what code actions are
currently available, making it impossible to discover
any code action (e.g. Browse gopls docs) that doesn't
fit into one of the existing categories with its own
command (e.g. Refactor, Source Action).

So, we compromise: if the CodeAction query was triggered
by cursor motion (Automatic), we treat [] as [QuickFix].
But if it was explicitly Invoked, we respond with all
available actions, equivalent to [""].

This does unfortunately double the test space; all but
one of our tests (TestVSCodeIssue65167)use TriggerKindUnknown.

Details:
- Adjust hierarchical matching to permit kind="" (protocol.Empty)
  to match all kinds.
- Change CLI and fake.Editor clients to populate
   Capabilities.TextDocument.CodeAction.CodeActionLiteralSupport.\
     CodeActionKind.ValueSet (!!), a 3.18 feature.
   (This isn't really needed now that the latest draft
   returns all available actions when trigger=automatic.)
- The @codeaction marker passes kind="".
- 'gopls codeaction' now passes Only=[""] when no -kind flag is specified.
- 'gopls imports' now passes Only=[SourceOrganizeImports]
   instead of obsolete title filtering.
- Editor.{serverCapabilities,semTokOpts} are no longer
  unnecessarily guarded by the mutex.
  (In an earlier draft I needed to expose Editor.ServerCapabilities
  but it proved unnecessary.)

Fixes golang/go#68783

Change-Id: Ia4246c47b54b59f6f03eada3e916428de50c42f4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616837
Commit-Queue: Alan Donovan 
Reviewed-by: Robert Findley 
Auto-Submit: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
---
 gopls/internal/cmd/capabilities_test.go       |  3 +
 gopls/internal/cmd/cmd.go                     |  7 ++
 gopls/internal/cmd/codeaction.go              |  2 +
 gopls/internal/cmd/imports.go                 |  6 +-
 gopls/internal/cmd/integration_test.go        | 40 +++++-----
 gopls/internal/server/code_action.go          | 79 ++++++++++++++-----
 gopls/internal/settings/codeactionkind.go     | 25 ++++--
 .../internal/test/integration/fake/editor.go  | 25 +++---
 gopls/internal/test/marker/marker_test.go     | 22 +++---
 9 files changed, 139 insertions(+), 70 deletions(-)

diff --git a/gopls/internal/cmd/capabilities_test.go b/gopls/internal/cmd/capabilities_test.go
index 97eb49652d0..e1cc11bf408 100644
--- a/gopls/internal/cmd/capabilities_test.go
+++ b/gopls/internal/cmd/capabilities_test.go
@@ -104,6 +104,9 @@ func TestCapabilities(t *testing.T) {
 		TextDocument: protocol.TextDocumentIdentifier{
 			URI: uri,
 		},
+		Context: protocol.CodeActionContext{
+			Only: []protocol.CodeActionKind{protocol.SourceOrganizeImports},
+		},
 	})
 	if err != nil {
 		t.Fatal(err)
diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go
index 9ec5b630d6c..4afac6a7aff 100644
--- a/gopls/internal/cmd/cmd.go
+++ b/gopls/internal/cmd/cmd.go
@@ -365,6 +365,13 @@ func (c *connection) initialize(ctx context.Context, options func(*settings.Opti
 	params.Capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true}
 	params.Capabilities.TextDocument.SemanticTokens.TokenTypes = protocol.SemanticTypes()
 	params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = protocol.SemanticModifiers()
+	params.Capabilities.TextDocument.CodeAction = protocol.CodeActionClientCapabilities{
+		CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{
+			CodeActionKind: protocol.ClientCodeActionKindOptions{
+				ValueSet: []protocol.CodeActionKind{protocol.Empty}, // => all
+			},
+		},
+	}
 	params.Capabilities.Window.WorkDoneProgress = true
 
 	params.InitializationOptions = map[string]interface{}{
diff --git a/gopls/internal/cmd/codeaction.go b/gopls/internal/cmd/codeaction.go
index 84d7d181b88..c349c7ab653 100644
--- a/gopls/internal/cmd/codeaction.go
+++ b/gopls/internal/cmd/codeaction.go
@@ -144,6 +144,8 @@ func (cmd *codeaction) Run(ctx context.Context, args ...string) error {
 		for _, kind := range strings.Split(cmd.Kind, ",") {
 			kinds = append(kinds, protocol.CodeActionKind(kind))
 		}
+	} else {
+		kinds = append(kinds, protocol.Empty) // => all
 	}
 	actions, err := conn.CodeAction(ctx, &protocol.CodeActionParams{
 		TextDocument: protocol.TextDocumentIdentifier{URI: uri},
diff --git a/gopls/internal/cmd/imports.go b/gopls/internal/cmd/imports.go
index c64c871e390..b0f67590748 100644
--- a/gopls/internal/cmd/imports.go
+++ b/gopls/internal/cmd/imports.go
@@ -59,15 +59,15 @@ func (t *imports) Run(ctx context.Context, args ...string) error {
 		TextDocument: protocol.TextDocumentIdentifier{
 			URI: uri,
 		},
+		Context: protocol.CodeActionContext{
+			Only: []protocol.CodeActionKind{protocol.SourceOrganizeImports},
+		},
 	})
 	if err != nil {
 		return fmt.Errorf("%v: %v", from, err)
 	}
 	var edits []protocol.TextEdit
 	for _, a := range actions {
-		if a.Title != "Organize Imports" {
-			continue
-		}
 		for _, c := range a.Edit.DocumentChanges {
 			// This code action should affect only the specified file;
 			// it is safe to ignore others.
diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go
index dfb2a164a42..39698f37334 100644
--- a/gopls/internal/cmd/integration_test.go
+++ b/gopls/internal/cmd/integration_test.go
@@ -48,7 +48,7 @@ import (
 	"golang.org/x/tools/txtar"
 )
 
-// TestVersion tests the 'version' subcommand (../info.go).
+// TestVersion tests the 'version' subcommand (info.go).
 func TestVersion(t *testing.T) {
 	t.Parallel()
 
@@ -84,7 +84,7 @@ func TestVersion(t *testing.T) {
 	}
 }
 
-// TestCheck tests the 'check' subcommand (../check.go).
+// TestCheck tests the 'check' subcommand (check.go).
 func TestCheck(t *testing.T) {
 	t.Parallel()
 
@@ -143,7 +143,7 @@ var C int
 	}
 }
 
-// TestCallHierarchy tests the 'call_hierarchy' subcommand (../call_hierarchy.go).
+// TestCallHierarchy tests the 'call_hierarchy' subcommand (call_hierarchy.go).
 func TestCallHierarchy(t *testing.T) {
 	t.Parallel()
 
@@ -186,7 +186,7 @@ func h() {
 	}
 }
 
-// TestCodeLens tests the 'codelens' subcommand (../codelens.go).
+// TestCodeLens tests the 'codelens' subcommand (codelens.go).
 func TestCodeLens(t *testing.T) {
 	t.Parallel()
 
@@ -238,7 +238,7 @@ func TestFail(t *testing.T) { t.Fatal("fail") }
 	}
 }
 
-// TestDefinition tests the 'definition' subcommand (../definition.go).
+// TestDefinition tests the 'definition' subcommand (definition.go).
 func TestDefinition(t *testing.T) {
 	t.Parallel()
 
@@ -289,7 +289,7 @@ func g() {
 	}
 }
 
-// TestExecute tests the 'execute' subcommand (../execute.go).
+// TestExecute tests the 'execute' subcommand (execute.go).
 func TestExecute(t *testing.T) {
 	t.Parallel()
 
@@ -363,7 +363,7 @@ func TestHello(t *testing.T) {
 	}
 }
 
-// TestFoldingRanges tests the 'folding_ranges' subcommand (../folding_range.go).
+// TestFoldingRanges tests the 'folding_ranges' subcommand (folding_range.go).
 func TestFoldingRanges(t *testing.T) {
 	t.Parallel()
 
@@ -393,7 +393,7 @@ func f(x int) {
 	}
 }
 
-// TestFormat tests the 'format' subcommand (../format.go).
+// TestFormat tests the 'format' subcommand (format.go).
 func TestFormat(t *testing.T) {
 	t.Parallel()
 
@@ -453,7 +453,7 @@ func f() {}
 	}
 }
 
-// TestHighlight tests the 'highlight' subcommand (../highlight.go).
+// TestHighlight tests the 'highlight' subcommand (highlight.go).
 func TestHighlight(t *testing.T) {
 	t.Parallel()
 
@@ -482,7 +482,7 @@ func f() {
 	}
 }
 
-// TestImplementations tests the 'implementation' subcommand (../implementation.go).
+// TestImplementations tests the 'implementation' subcommand (implementation.go).
 func TestImplementations(t *testing.T) {
 	t.Parallel()
 
@@ -511,7 +511,7 @@ func (T) String() string { return "" }
 	}
 }
 
-// TestImports tests the 'imports' subcommand (../imports.go).
+// TestImports tests the 'imports' subcommand (imports.go).
 func TestImports(t *testing.T) {
 	t.Parallel()
 
@@ -560,7 +560,7 @@ func _() {
 	}
 }
 
-// TestLinks tests the 'links' subcommand (../links.go).
+// TestLinks tests the 'links' subcommand (links.go).
 func TestLinks(t *testing.T) {
 	t.Parallel()
 
@@ -605,7 +605,7 @@ func f() {}
 	}
 }
 
-// TestReferences tests the 'references' subcommand (../references.go).
+// TestReferences tests the 'references' subcommand (references.go).
 func TestReferences(t *testing.T) {
 	t.Parallel()
 
@@ -643,7 +643,7 @@ func g() {
 	}
 }
 
-// TestSignature tests the 'signature' subcommand (../signature.go).
+// TestSignature tests the 'signature' subcommand (signature.go).
 func TestSignature(t *testing.T) {
 	t.Parallel()
 
@@ -674,7 +674,7 @@ func f() {
 	}
 }
 
-// TestPrepareRename tests the 'prepare_rename' subcommand (../prepare_rename.go).
+// TestPrepareRename tests the 'prepare_rename' subcommand (prepare_rename.go).
 func TestPrepareRename(t *testing.T) {
 	t.Parallel()
 
@@ -713,7 +713,7 @@ func oldname() {}
 	}
 }
 
-// TestRename tests the 'rename' subcommand (../rename.go).
+// TestRename tests the 'rename' subcommand (rename.go).
 func TestRename(t *testing.T) {
 	t.Parallel()
 
@@ -759,7 +759,7 @@ func oldname() {}
 	}
 }
 
-// TestSymbols tests the 'symbols' subcommand (../symbols.go).
+// TestSymbols tests the 'symbols' subcommand (symbols.go).
 func TestSymbols(t *testing.T) {
 	t.Parallel()
 
@@ -790,7 +790,7 @@ const c = 0
 	}
 }
 
-// TestSemtok tests the 'semtok' subcommand (../semantictokens.go).
+// TestSemtok tests the 'semtok' subcommand (semantictokens.go).
 func TestSemtok(t *testing.T) {
 	t.Parallel()
 
@@ -941,7 +941,7 @@ package foo
 	}
 }
 
-// TestCodeAction tests the 'codeaction' subcommand (../codeaction.go).
+// TestCodeAction tests the 'codeaction' subcommand (codeaction.go).
 func TestCodeAction(t *testing.T) {
 	t.Parallel()
 
@@ -1040,7 +1040,7 @@ func (c C) Read(p []byte) (n int, err error) {
 	}
 }
 
-// TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (../workspace_symbol.go).
+// TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (workspace_symbol.go).
 func TestWorkspaceSymbol(t *testing.T) {
 	t.Parallel()
 
diff --git a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go
index 5fc0fb6aa21..e00e343850d 100644
--- a/gopls/internal/server/code_action.go
+++ b/gopls/internal/server/code_action.go
@@ -36,34 +36,65 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara
 	// Determine the supported code action kinds for this file.
 	//
 	// We interpret CodeActionKinds hierarchically, so refactor.rewrite
-	// subsumes refactor.rewrite.change_quote, for example.
+	// subsumes refactor.rewrite.change_quote, for example,
+	// and "" (protocol.Empty) subsumes all kinds.
 	// See ../protocol/codeactionkind.go for some code action theory.
-	codeActionKinds, ok := snapshot.Options().SupportedCodeActions[kind]
-	if !ok {
-		return nil, fmt.Errorf("no supported code actions for %v file kind", kind)
-	}
-
-	// The Only field of the context specifies which code actions
-	// the client wants. If Only is empty, assume the client wants
-	// all supported code actions.
+	//
+	// The Context.Only field specifies which code actions
+	// the client wants. According to LSP 3.18 textDocument_codeAction,
+	// an Only=[] should be interpreted as Only=["quickfix"]:
+	//
+	//   "In version 1.0 of the protocol, there weren’t any
+	//   source or refactoring code actions. Code actions
+	//   were solely used to (quick) fix code, not to
+	//   write/rewrite code. So if a client asks for code
+	//   actions without any kind, the standard quick fix
+	//   code actions should be returned."
+	//
+	// However, this would deny clients (e.g. Vim+coc.nvim,
+	// Emacs+eglot, and possibly others) the easiest and most
+	// natural way of querying the server for the entire set of
+	// available code actions. But reporting all available code
+	// actions would be a nuisance for VS Code, since mere cursor
+	// motion into a region with a code action (~anywhere) would
+	// trigger a lightbulb usually associated with quickfixes.
+	//
+	// As a compromise, we use the trigger kind as a heuristic: if
+	// the query was triggered by cursor motion (Automatic), we
+	// respond with only quick fixes; if the query was invoked
+	// explicitly (Invoked), we respond with all available
+	// actions.
+	codeActionKinds := make(map[protocol.CodeActionKind]bool)
 	if len(params.Context.Only) > 0 {
-		codeActionKinds = make(map[protocol.CodeActionKind]bool)
-		for _, kind := range params.Context.Only {
+		for _, kind := range params.Context.Only { // kind may be "" (=> all)
 			codeActionKinds[kind] = true
 		}
+	} else {
+		// No explicit kind specified.
+		// Heuristic: decide based on trigger.
+		if triggerKind(params) == protocol.CodeActionAutomatic {
+			// e.g. cursor motion: show only quick fixes
+			codeActionKinds[protocol.QuickFix] = true
+		} else {
+			// e.g. a menu selection (or unknown trigger kind,
+			// as in our tests): show all available code actions.
+			codeActionKinds[protocol.Empty] = true
+		}
 	}
 
 	// enabled reports whether the specified kind of code action is required.
 	enabled := func(kind protocol.CodeActionKind) bool {
 		// Given "refactor.rewrite.foo", check for it,
-		// then "refactor.rewrite", "refactor".
+		// then "refactor.rewrite", "refactor", then "".
 		// A false map entry prunes the search for ancestors.
+		//
+		// If codeActionKinds contains protocol.Empty (""),
+		// all kinds are enabled.
 		for {
 			if v, ok := codeActionKinds[kind]; ok {
 				return v
 			}
-			dot := strings.LastIndexByte(string(kind), '.')
-			if dot < 0 {
+			if kind == "" {
 				return false
 			}
 
@@ -88,7 +119,12 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara
 				return false // don't search ancestors
 			}
 
-			kind = kind[:dot]
+			// Try the parent.
+			if dot := strings.LastIndexByte(string(kind), '.'); dot >= 0 {
+				kind = kind[:dot] // "refactor.foo" -> "refactor"
+			} else {
+				kind = "" // "refactor" -> ""
+			}
 		}
 	}
 
@@ -139,11 +175,7 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara
 		}
 
 		// computed code actions (may include quickfixes from diagnostics)
-		trigger := protocol.CodeActionUnknownTrigger
-		if k := params.Context.TriggerKind; k != nil { // (some clients omit it)
-			trigger = *k
-		}
-		moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, enabled, trigger)
+		moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, enabled, triggerKind(params))
 		if err != nil {
 			return nil, err
 		}
@@ -175,6 +207,13 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara
 	}
 }
 
+func triggerKind(params *protocol.CodeActionParams) protocol.CodeActionTriggerKind {
+	if kind := params.Context.TriggerKind; kind != nil { // (some clients omit it)
+		return *kind
+	}
+	return protocol.CodeActionUnknownTrigger
+}
+
 // ResolveCodeAction resolves missing Edit information (that is, computes the
 // details of the necessary patch) in the given code action using the provided
 // Data field of the CodeAction, which should contain the raw json of a protocol.Command.
diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go
index e8e29d8ddb8..fa06b90e7e3 100644
--- a/gopls/internal/settings/codeactionkind.go
+++ b/gopls/internal/settings/codeactionkind.go
@@ -24,14 +24,15 @@ import "golang.org/x/tools/gopls/internal/protocol"
 //	notebook
 //
 // Kinds are hierarchical: "refactor" subsumes "refactor.inline",
-// which subsumes "refactor.inline.call". The "Only" field in a
-// CodeAction request may specify a category such as "refactor"; any
-// matching code action will be returned.
+// which subsumes "refactor.inline.call". This rule implies that the
+// empty string, confusingly named protocol.Empty, subsumes all kinds.
+// The "Only" field in a CodeAction request may specify a category
+// such as "refactor"; any matching code action will be returned.
 //
 // All CodeActions returned by gopls use a specific leaf kind such as
 // "refactor.inline.call", except for quick fixes, which all use
 // "quickfix". TODO(adonovan): perhaps quick fixes should also be
-// hierarchical (e.g. quickfix.govulncheck.{reset,upgrade}).
+// hierarchical (e.g. quickfix.govulncheck.{reset,upgrade})?
 //
 // # VS Code
 //
@@ -41,10 +42,16 @@ import "golang.org/x/tools/gopls/internal/protocol"
 // Clicking on the "Refactor..." menu item shows a submenu of actions
 // with kind="refactor.*", and clicking on "Source action..." shows
 // actions with kind="source.*". A lightbulb appears in both cases.
+//
 // A third menu, "Quick fix...", not found on the usual context
 // menu but accessible through the command palette or "⌘.",
-// displays code actions of kind "quickfix.*" and "refactor.*",
-// and ad hoc ones ("More actions...") such as "gopls.*".
+// does not set the Only field in its request, so the set of
+// kinds is determined by how the server interprets the default.
+// The LSP 3.18 guidance is that this should be treated
+// equivalent to Only=["quickfix"], and that is what gopls
+// now does. (If the server responds with more kinds, they will
+// be displayed in menu subsections.)
+//
 // All of these CodeAction requests have triggerkind=Invoked.
 //
 // Cursor motion also performs a CodeAction request, but with
@@ -60,7 +67,7 @@ import "golang.org/x/tools/gopls/internal/protocol"
 //
 // In all these menus, VS Code organizes the actions' menu items
 // into groups based on their kind, with hardwired captions such as
-// "Extract", "Inline", "More actions", and "Quick fix".
+// "Refactor...", "Extract", "Inline", "More actions", and "Quick fix".
 //
 // The special category "source.fixAll" is intended for actions that
 // are unambiguously safe to apply so that clients may automatically
@@ -74,6 +81,10 @@ const (
 	GoTest        protocol.CodeActionKind = "source.test"
 
 	// gopls
+	// TODO(adonovan): we should not use this category as it will
+	// never be requested now that we no longer interpret "no kind
+	// restriction" as "quickfix" instead of "all kinds".
+	// We need another way to make docs discoverable.
 	GoplsDocFeatures protocol.CodeActionKind = "gopls.doc.features"
 
 	// refactor.rewrite
diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go
index 09cdbaf8c66..876d055da21 100644
--- a/gopls/internal/test/integration/fake/editor.go
+++ b/gopls/internal/test/integration/fake/editor.go
@@ -42,13 +42,15 @@ type Editor struct {
 
 	// TODO(rfindley): buffers should be keyed by protocol.DocumentURI.
 	mu                       sync.Mutex
-	config                   EditorConfig                // editor configuration
-	buffers                  map[string]buffer           // open buffers (relative path -> buffer content)
-	serverCapabilities       protocol.ServerCapabilities // capabilities / options
-	semTokOpts               protocol.SemanticTokensOptions
-	watchPatterns            []*glob.Glob // glob patterns to watch
+	config                   EditorConfig      // editor configuration
+	buffers                  map[string]buffer // open buffers (relative path -> buffer content)
+	watchPatterns            []*glob.Glob      // glob patterns to watch
 	suggestionUseReplaceMode bool
 
+	// These fields are populated by Connect.
+	serverCapabilities protocol.ServerCapabilities
+	semTokOpts         protocol.SemanticTokensOptions
+
 	// Call metrics for the purpose of expectations. This is done in an ad-hoc
 	// manner for now. Perhaps in the future we should do something more
 	// systematic. Guarded with a separate mutex as calls may need to be accessed
@@ -320,10 +322,8 @@ func (e *Editor) initialize(ctx context.Context) error {
 		if err != nil {
 			return fmt.Errorf("unmarshalling semantic tokens options: %v", err)
 		}
-		e.mu.Lock()
 		e.serverCapabilities = resp.Capabilities
 		e.semTokOpts = semTokOpts
-		e.mu.Unlock()
 
 		if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
 			return fmt.Errorf("initialized: %w", err)
@@ -357,6 +357,14 @@ func clientCapabilities(cfg EditorConfig) (protocol.ClientCapabilities, error) {
 		// Additional modifiers supported by this client:
 		"interface", "struct", "signature", "pointer", "array", "map", "slice", "chan", "string", "number", "bool", "invalid",
 	}
+	// Request that the server provide its complete list of code action kinds.
+	capabilities.TextDocument.CodeAction = protocol.CodeActionClientCapabilities{
+		CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{
+			CodeActionKind: protocol.ClientCodeActionKindOptions{
+				ValueSet: []protocol.CodeActionKind{protocol.Empty}, // => all
+			},
+		},
+	}
 	// The LSP tests have historically enabled this flag,
 	// but really we should test both ways for older editors.
 	capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true
@@ -1570,6 +1578,7 @@ func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnost
 		Context: protocol.CodeActionContext{
 			Diagnostics: diagnostics,
 			TriggerKind: &trigger,
+			Only:        []protocol.CodeActionKind{protocol.Empty}, // => all
 		},
 		Range: loc.Range, // may be zero
 	}
@@ -1680,9 +1689,7 @@ type SemanticToken struct {
 // Note: previously this function elided comment, string, and number tokens.
 // Instead, filtering of token types should be done by the caller.
 func (e *Editor) interpretTokens(x []uint32, contents string) []SemanticToken {
-	e.mu.Lock()
 	legend := e.semTokOpts.Legend
-	e.mu.Unlock()
 	lines := strings.Split(contents, "\n")
 	ans := []SemanticToken{}
 	line, col := 1, 1
diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go
index e566f922ac9..a128bcb7f18 100644
--- a/gopls/internal/test/marker/marker_test.go
+++ b/gopls/internal/test/marker/marker_test.go
@@ -1933,7 +1933,7 @@ func codeActionMarker(mark marker, start, end protocol.Location, actionKind stri
 	loc.Range.End = end.Range.End
 
 	// Apply the fix it suggests.
-	changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil)
+	changed, err := codeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(actionKind), nil)
 	if err != nil {
 		mark.errorf("codeAction failed: %v", err)
 		return
@@ -1944,7 +1944,7 @@ func codeActionMarker(mark marker, start, end protocol.Location, actionKind stri
 }
 
 func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden) {
-	changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil)
+	changed, err := codeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(actionKind), nil)
 	if err != nil {
 		mark.errorf("codeAction failed: %v", err)
 		return
@@ -1956,7 +1956,7 @@ func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string,
 func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr stringMatcher) {
 	loc := start
 	loc.Range.End = end.Range.End
-	_, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil)
+	_, err := codeAction(mark.run.env, loc.URI, loc.Range, protocol.CodeActionKind(actionKind), nil)
 	wantErr.checkErr(mark, err)
 }
 
@@ -2071,8 +2071,8 @@ func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp
 // The resulting map contains resulting file contents after the code action is
 // applied. Currently, this function does not support code actions that return
 // edits directly; it only supports code action commands.
-func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic) (map[string][]byte, error) {
-	changes, err := codeActionChanges(env, uri, rng, actionKind, diag)
+func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) (map[string][]byte, error) {
+	changes, err := codeActionChanges(env, uri, rng, kind, diag)
 	if err != nil {
 		return nil, err
 	}
@@ -2082,15 +2082,15 @@ func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Ran
 // codeActionChanges executes a textDocument/codeAction request for the
 // specified location and kind, and captures the resulting document changes.
 // If diag is non-nil, it is used as the code action context.
-func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic) ([]protocol.DocumentChange, error) {
+func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, kind protocol.CodeActionKind, diag *protocol.Diagnostic) ([]protocol.DocumentChange, error) {
 	// Request all code actions that apply to the diagnostic.
-	// (The protocol supports filtering using Context.Only={actionKind}
-	// but we can give a better error if we don't filter.)
+	// A production client would set Only=[kind],
+	// but we can give a better error if we don't filter.
 	params := &protocol.CodeActionParams{
 		TextDocument: protocol.TextDocumentIdentifier{URI: uri},
 		Range:        rng,
 		Context: protocol.CodeActionContext{
-			Only: nil, // => all kinds
+			Only: []protocol.CodeActionKind{protocol.Empty}, // => all
 		},
 	}
 	if diag != nil {
@@ -2106,7 +2106,7 @@ func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng proto
 	// (e.g. refactor.inline.call).
 	var candidates []protocol.CodeAction
 	for _, act := range actions {
-		if act.Kind == protocol.CodeActionKind(actionKind) {
+		if act.Kind == kind {
 			candidates = append(candidates, act)
 		}
 	}
@@ -2114,7 +2114,7 @@ func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng proto
 		for _, act := range actions {
 			env.T.Logf("found CodeAction Kind=%s Title=%q", act.Kind, act.Title)
 		}
-		return nil, fmt.Errorf("found %d CodeActions of kind %s for this diagnostic, want 1", len(candidates), actionKind)
+		return nil, fmt.Errorf("found %d CodeActions of kind %s for this diagnostic, want 1", len(candidates), kind)
 	}
 	action := candidates[0]
 

From a7552bcbbe800a534d729c30e6dc933bff9b0074 Mon Sep 17 00:00:00 2001
From: Alan Donovan 
Date: Fri, 27 Sep 2024 13:52:24 -0400
Subject: [PATCH 092/102] go/ast/inspector: add PreorderSeq and All iterators

This CL adds two functions that return go1.23 iterators
over selected nodes:
- inspect.PreorderSeq(types...), which is like Preorder but
  honors the break statement;
- All[N](inspect), which iterates over nodes of a single
  type, determined by the type parameter.

+ Tests, benchmark.

Fixes golang/go#67795

Change-Id: I77817f2e595846cf3ce29dc347ae895e927fc805
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616218
Auto-Submit: Alan Donovan 
Commit-Queue: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Robert Findley 
---
 go/ast/inspector/inspector.go      |  9 ++++
 go/ast/inspector/inspector_test.go |  3 +-
 go/ast/inspector/iter.go           | 85 ++++++++++++++++++++++++++++++
 go/ast/inspector/iter_test.go      | 83 +++++++++++++++++++++++++++++
 4 files changed, 179 insertions(+), 1 deletion(-)
 create mode 100644 go/ast/inspector/iter.go
 create mode 100644 go/ast/inspector/iter_test.go

diff --git a/go/ast/inspector/inspector.go b/go/ast/inspector/inspector.go
index 1fc1de0bd10..0e0ba4c035c 100644
--- a/go/ast/inspector/inspector.go
+++ b/go/ast/inspector/inspector.go
@@ -73,6 +73,15 @@ func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) {
 	// check, Preorder is almost twice as fast as Nodes. The two
 	// features seem to contribute similar slowdowns (~1.4x each).
 
+	// This function is equivalent to the PreorderSeq call below,
+	// but to avoid the additional dynamic call (which adds 13-35%
+	// to the benchmarks), we expand it out.
+	//
+	// in.PreorderSeq(types...)(func(n ast.Node) bool {
+	// 	f(n)
+	// 	return true
+	// })
+
 	mask := maskOf(types)
 	for i := 0; i < len(in.events); {
 		ev := in.events[i]
diff --git a/go/ast/inspector/inspector_test.go b/go/ast/inspector/inspector_test.go
index 5d7cb6e44eb..a19ba653e0a 100644
--- a/go/ast/inspector/inspector_test.go
+++ b/go/ast/inspector/inspector_test.go
@@ -160,7 +160,8 @@ func TestInspectPruning(t *testing.T) {
 	compare(t, nodesA, nodesB)
 }
 
-func compare(t *testing.T, nodesA, nodesB []ast.Node) {
+// compare calls t.Error if !slices.Equal(nodesA, nodesB).
+func compare[N comparable](t *testing.T, nodesA, nodesB []N) {
 	if len(nodesA) != len(nodesB) {
 		t.Errorf("inconsistent node lists: %d vs %d", len(nodesA), len(nodesB))
 	} else {
diff --git a/go/ast/inspector/iter.go b/go/ast/inspector/iter.go
new file mode 100644
index 00000000000..b7e959114cb
--- /dev/null
+++ b/go/ast/inspector/iter.go
@@ -0,0 +1,85 @@
+// Copyright 2024 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.
+
+//go:build go1.23
+
+package inspector
+
+import (
+	"go/ast"
+	"iter"
+)
+
+// PreorderSeq returns an iterator that visits all the
+// nodes of the files supplied to New in depth-first order.
+// It visits each node n before n's children.
+// The complete traversal sequence is determined by ast.Inspect.
+//
+// The types argument, if non-empty, enables type-based
+// filtering of events: only nodes whose type matches an
+// element of the types slice are included in the sequence.
+func (in *Inspector) PreorderSeq(types ...ast.Node) iter.Seq[ast.Node] {
+
+	// This implementation is identical to Preorder,
+	// except that it supports breaking out of the loop.
+
+	return func(yield func(ast.Node) bool) {
+		mask := maskOf(types)
+		for i := 0; i < len(in.events); {
+			ev := in.events[i]
+			if ev.index > i {
+				// push
+				if ev.typ&mask != 0 {
+					if !yield(ev.node) {
+						break
+					}
+				}
+				pop := ev.index
+				if in.events[pop].typ&mask == 0 {
+					// Subtrees do not contain types: skip them and pop.
+					i = pop + 1
+					continue
+				}
+			}
+			i++
+		}
+	}
+}
+
+// All[N] returns an iterator over all the nodes of type N.
+// N must be a pointer-to-struct type that implements ast.Node.
+//
+// Example:
+//
+//	for call := range All[*ast.CallExpr](in) { ... }
+func All[N interface {
+	*S
+	ast.Node
+}, S any](in *Inspector) iter.Seq[N] {
+
+	// To avoid additional dynamic call overheads,
+	// we duplicate rather than call the logic of PreorderSeq.
+
+	mask := typeOf((N)(nil))
+	return func(yield func(N) bool) {
+		for i := 0; i < len(in.events); {
+			ev := in.events[i]
+			if ev.index > i {
+				// push
+				if ev.typ&mask != 0 {
+					if !yield(ev.node.(N)) {
+						break
+					}
+				}
+				pop := ev.index
+				if in.events[pop].typ&mask == 0 {
+					// Subtrees do not contain types: skip them and pop.
+					i = pop + 1
+					continue
+				}
+			}
+			i++
+		}
+	}
+}
diff --git a/go/ast/inspector/iter_test.go b/go/ast/inspector/iter_test.go
new file mode 100644
index 00000000000..2f52998c558
--- /dev/null
+++ b/go/ast/inspector/iter_test.go
@@ -0,0 +1,83 @@
+// Copyright 2024 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.
+
+//go:build go1.23
+
+package inspector_test
+
+import (
+	"go/ast"
+	"iter"
+	"slices"
+	"testing"
+
+	"golang.org/x/tools/go/ast/inspector"
+)
+
+// TestPreorderSeq checks PreorderSeq against Preorder.
+func TestPreorderSeq(t *testing.T) {
+	inspect := inspector.New(netFiles)
+
+	nodeFilter := []ast.Node{(*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)}
+
+	// reference implementation
+	var want []ast.Node
+	inspect.Preorder(nodeFilter, func(n ast.Node) {
+		want = append(want, n)
+	})
+
+	// Check entire sequence.
+	got := slices.Collect(inspect.PreorderSeq(nodeFilter...))
+	compare(t, got, want)
+
+	// Check that break works.
+	got = firstN(10, inspect.PreorderSeq(nodeFilter...))
+	compare(t, got, want[:10])
+}
+
+// TestAll checks All against Preorder.
+func TestAll(t *testing.T) {
+	inspect := inspector.New(netFiles)
+
+	// reference implementation
+	var want []*ast.CallExpr
+	inspect.Preorder([]ast.Node{(*ast.CallExpr)(nil)}, func(n ast.Node) {
+		want = append(want, n.(*ast.CallExpr))
+	})
+
+	// Check entire sequence.
+	got := slices.Collect(inspector.All[*ast.CallExpr](inspect))
+	compare(t, got, want)
+
+	// Check that break works.
+	got = firstN(10, inspector.All[*ast.CallExpr](inspect))
+	compare(t, got, want[:10])
+}
+
+// firstN(n, seq), returns a slice of up to n elements of seq.
+func firstN[T any](n int, seq iter.Seq[T]) (res []T) {
+	for x := range seq {
+		res = append(res, x)
+		if len(res) == n {
+			break
+		}
+	}
+	return res
+}
+
+// BenchmarkAllCalls is like BenchmarkInspectCalls,
+// but using the single-type filtering iterator, All.
+// (The iterator adds about 5-15%.)
+func BenchmarkAllCalls(b *testing.B) {
+	inspect := inspector.New(netFiles)
+	b.ResetTimer()
+
+	// Measure marginal cost of traversal.
+	var ncalls int
+	for range b.N {
+		for range inspector.All[*ast.CallExpr](inspect) {
+			ncalls++
+		}
+	}
+}

From a2ff832d75d41ee8d9f7659a36d431ff0ac4b090 Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Mon, 30 Sep 2024 13:19:16 -0700
Subject: [PATCH 093/102] go/ssa: remove references to GOEXPERIMENT range

Range over int is in all supported Go versions for x/tools.

Change-Id: I16d3da47121fcf34251fa5cad9561848ef699a56
Reviewed-on: https://go-review.googlesource.com/c/tools/+/616977
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Tim King 
Reviewed-by: Alan Donovan 
---
 go/ssa/builder_test.go                              |  2 --
 go/ssa/interp/interp_test.go                        |  1 +
 .../{interp_go122_test.go => rangefunc_test.go}     | 13 -------------
 go/ssa/interp/testdata/rangeoverint.go              |  4 +---
 4 files changed, 2 insertions(+), 18 deletions(-)
 rename go/ssa/interp/{interp_go122_test.go => rangefunc_test.go} (94%)

diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go
index f8baf77736d..bc1989c58b7 100644
--- a/go/ssa/builder_test.go
+++ b/go/ssa/builder_test.go
@@ -1291,8 +1291,6 @@ func TestMultipleGoversions(t *testing.T) {
 // the type of each range var v (identified by print(v) calls)
 // has the expected type.
 func TestRangeOverInt(t *testing.T) {
-	testenv.NeedsGoExperiment(t, "range")
-
 	const rangeOverIntSrc = `
 		package p
 
diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go
index 90f318c231b..8ce9f368aec 100644
--- a/go/ssa/interp/interp_test.go
+++ b/go/ssa/interp/interp_test.go
@@ -124,6 +124,7 @@ var testdataTests = []string{
 	"methprom.go",
 	"mrvchain.go",
 	"range.go",
+	"rangeoverint.go",
 	"recover.go",
 	"reflect.go",
 	"slice2arrayptr.go",
diff --git a/go/ssa/interp/interp_go122_test.go b/go/ssa/interp/rangefunc_test.go
similarity index 94%
rename from go/ssa/interp/interp_go122_test.go
rename to go/ssa/interp/rangefunc_test.go
index bf82f35cf86..58b7f43eca4 100644
--- a/go/ssa/interp/interp_go122_test.go
+++ b/go/ssa/interp/rangefunc_test.go
@@ -16,19 +16,6 @@ import (
 	"golang.org/x/tools/internal/testenv"
 )
 
-// TestExperimentRange tests files in testdata with GOEXPERIMENT=range set.
-func TestExperimentRange(t *testing.T) {
-	testenv.NeedsGoExperiment(t, "range")
-
-	// TODO: Is cwd actually needed here?
-	goroot := makeGoroot(t)
-	cwd, err := os.Getwd()
-	if err != nil {
-		log.Fatal(err)
-	}
-	run(t, filepath.Join(cwd, "testdata", "rangeoverint.go"), goroot)
-}
-
 func TestIssue69298(t *testing.T) {
 	testenv.NeedsGo1Point(t, 23)
 
diff --git a/go/ssa/interp/testdata/rangeoverint.go b/go/ssa/interp/testdata/rangeoverint.go
index 9a02d829764..60df354f4e2 100644
--- a/go/ssa/interp/testdata/rangeoverint.go
+++ b/go/ssa/interp/testdata/rangeoverint.go
@@ -1,8 +1,6 @@
 package main
 
-// Range over integers.
-
-// Currently requires 1.22 and GOEXPERIMENT=range.
+// Range over integers (Go 1.22).
 
 import "fmt"
 

From ce2a33e35d87bb317bcd526328efd39ec6671405 Mon Sep 17 00:00:00 2001
From: Chiawen Chen 
Date: Wed, 2 Oct 2024 15:58:39 +0800
Subject: [PATCH 094/102] gopls/internal: fix extract refactor for cases with
 anonymous functions

This fix ignores return statements inside anonymous functions. These
return statements can be ignored because they does not meddle with
the control flow of the outer function.

Fixes golang/go#64821

Change-Id: I21f82f8663bf3343412d5b537802a56efc34495f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/617335
Reviewed-by: Robert Findley 
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Alan Donovan 
Reviewed-by: Alan Donovan 
Auto-Submit: Robert Findley 
---
 gopls/internal/golang/extract.go              |  4 +++
 .../codeaction/functionextraction.txt         | 33 +++++++++++++++++++
 2 files changed, 37 insertions(+)

diff --git a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go
index d1092d85409..6ea011e220e 100644
--- a/gopls/internal/golang/extract.go
+++ b/gopls/internal/golang/extract.go
@@ -246,6 +246,10 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte
 		if n.Pos() < start || n.End() > end {
 			return n.Pos() <= end
 		}
+		// exclude return statements in function literals because they don't affect the refactor.
+		if _, ok := n.(*ast.FuncLit); ok {
+			return false
+		}
 		ret, ok := n.(*ast.ReturnStmt)
 		if !ok {
 			return true
diff --git a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt
index 1c65fcd2329..1b9f487c49d 100644
--- a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt
+++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt
@@ -581,3 +581,36 @@ func newFunction() (int, error) {
 	return u, err
 }
 
+-- anonymousfunc.go --
+package extract
+import "cmp"
+import "slices"
+
+// issue go#64821
+func _() {
+	var s []string //@codeaction("var", anonEnd, "refactor.extract.function", anon1)
+	slices.SortFunc(s, func(a, b string) int {
+		return cmp.Compare(a, b)
+	})
+	println(s) //@loc(anonEnd, ")")
+}
+
+-- @anon1/anonymousfunc.go --
+package extract
+import "cmp"
+import "slices"
+
+// issue go#64821
+func _() {
+	//@codeaction("var", anonEnd, "refactor.extract.function", anon1)
+	newFunction() //@loc(anonEnd, ")")
+}
+
+func newFunction() {
+	var s []string
+	slices.SortFunc(s, func(a, b string) int {
+		return cmp.Compare(a, b)
+	})
+	println(s)
+}
+

From a24facf9e5586c95743d2f4ad15d148c7a8cf00b Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Tue, 1 Oct 2024 09:22:28 -0700
Subject: [PATCH 095/102] all: set gotypesalias=0 explicitly

Set GODEBUG variable gotypesalias to off in all main packages
in x/tools that use go/types.

This is a no-op as the x/tools go.mod is uses version 1.22,
which disables gotypesalias in main packages and tests by default.

Change-Id: I1593cd63ec018e230e126d990530bd59fb9a96cf
Reviewed-on: https://go-review.googlesource.com/c/tools/+/617095
LUCI-TryBot-Result: Go LUCI 
Commit-Queue: Tim King 
Reviewed-by: Robert Findley 
---
 cmd/bundle/main.go                                           | 3 +++
 cmd/callgraph/main.go                                        | 3 +++
 cmd/deadcode/deadcode.go                                     | 2 ++
 cmd/eg/eg.go                                                 | 3 +++
 cmd/godex/godex.go                                           | 2 ++
 cmd/godoc/main.go                                            | 2 ++
 cmd/goimports/goimports.go                                   | 2 ++
 cmd/gomvpkg/main.go                                          | 3 +++
 cmd/gotype/gotype.go                                         | 3 +++
 cmd/ssadump/main.go                                          | 3 +++
 cmd/stringer/stringer.go                                     | 3 +++
 go/analysis/passes/defers/cmd/defers/main.go                 | 3 +++
 go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go | 3 +++
 go/analysis/passes/findcall/cmd/findcall/main.go             | 3 +++
 go/analysis/passes/httpmux/cmd/httpmux/main.go               | 3 +++
 go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go       | 3 +++
 go/analysis/passes/lostcancel/cmd/lostcancel/main.go         | 3 +++
 go/analysis/passes/nilness/cmd/nilness/main.go               | 3 +++
 go/analysis/passes/shadow/cmd/shadow/main.go                 | 3 +++
 go/analysis/passes/stringintconv/cmd/stringintconv/main.go   | 3 +++
 go/analysis/passes/unmarshal/cmd/unmarshal/main.go           | 3 +++
 go/analysis/passes/unusedresult/cmd/unusedresult/main.go     | 3 +++
 go/packages/gopackages/main.go                               | 3 +++
 go/packages/internal/nodecount/nodecount.go                  | 3 +++
 go/types/internal/play/play.go                               | 3 +++
 25 files changed, 71 insertions(+)

diff --git a/cmd/bundle/main.go b/cmd/bundle/main.go
index fa73eb83a0a..a4831c78776 100644
--- a/cmd/bundle/main.go
+++ b/cmd/bundle/main.go
@@ -68,6 +68,9 @@
 // Update all bundles in the standard library:
 //
 //	go generate -run bundle std
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/cmd/callgraph/main.go b/cmd/callgraph/main.go
index 9e440bbafb9..1b5af1b52e1 100644
--- a/cmd/callgraph/main.go
+++ b/cmd/callgraph/main.go
@@ -4,6 +4,9 @@
 
 // callgraph: a tool for reporting the call graph of a Go program.
 // See Usage for details, or run with -help.
+
+//go:debug gotypesalias=0
+
 package main // import "golang.org/x/tools/cmd/callgraph"
 
 // TODO(adonovan):
diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go
index f129102cc4c..e6f32bb9979 100644
--- a/cmd/deadcode/deadcode.go
+++ b/cmd/deadcode/deadcode.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/cmd/eg/eg.go b/cmd/eg/eg.go
index 108b9e3009f..07e73d2efe7 100644
--- a/cmd/eg/eg.go
+++ b/cmd/eg/eg.go
@@ -5,6 +5,9 @@
 // The eg command performs example-based refactoring.
 // For documentation, run the command, or see Help in
 // golang.org/x/tools/refactor/eg.
+
+//go:debug gotypesalias=0
+
 package main // import "golang.org/x/tools/cmd/eg"
 
 import (
diff --git a/cmd/godex/godex.go b/cmd/godex/godex.go
index e91dbfcea5f..4955600f2d6 100644
--- a/cmd/godex/godex.go
+++ b/cmd/godex/godex.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/cmd/godoc/main.go b/cmd/godoc/main.go
index a665be0769d..1c874cc0e15 100644
--- a/cmd/godoc/main.go
+++ b/cmd/godoc/main.go
@@ -14,6 +14,8 @@
 //				http://godoc/pkg/compress/zlib)
 //
 
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/cmd/goimports/goimports.go b/cmd/goimports/goimports.go
index dcb5023a2e7..7463e641e95 100644
--- a/cmd/goimports/goimports.go
+++ b/cmd/goimports/goimports.go
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/cmd/gomvpkg/main.go b/cmd/gomvpkg/main.go
index 5de1e44062d..d54b7070dec 100644
--- a/cmd/gomvpkg/main.go
+++ b/cmd/gomvpkg/main.go
@@ -4,6 +4,9 @@
 
 // The gomvpkg command moves go packages, updating import declarations.
 // See the -help message or Usage constant for details.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/cmd/gotype/gotype.go b/cmd/gotype/gotype.go
index 4a731f26233..09b66207e63 100644
--- a/cmd/gotype/gotype.go
+++ b/cmd/gotype/gotype.go
@@ -85,6 +85,9 @@ To verify the output of a pipe:
 
 	echo "package foo" | gotype
 */
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/cmd/ssadump/main.go b/cmd/ssadump/main.go
index 275e0a92aef..2ecf04fba50 100644
--- a/cmd/ssadump/main.go
+++ b/cmd/ssadump/main.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // ssadump: a tool for displaying and interpreting the SSA form of Go programs.
+
+//go:debug gotypesalias=0
+
 package main // import "golang.org/x/tools/cmd/ssadump"
 
 import (
diff --git a/cmd/stringer/stringer.go b/cmd/stringer/stringer.go
index 09be11ca58e..94eaee844a4 100644
--- a/cmd/stringer/stringer.go
+++ b/cmd/stringer/stringer.go
@@ -70,6 +70,9 @@
 //	PillAspirin // Aspirin
 //
 // to suppress it in the output.
+
+//go:debug gotypesalias=0
+
 package main // import "golang.org/x/tools/cmd/stringer"
 
 import (
diff --git a/go/analysis/passes/defers/cmd/defers/main.go b/go/analysis/passes/defers/cmd/defers/main.go
index b3dc8b94eca..ffa5ae2da9b 100644
--- a/go/analysis/passes/defers/cmd/defers/main.go
+++ b/go/analysis/passes/defers/cmd/defers/main.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // The defers command runs the defers analyzer.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go b/go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go
index 47d383d2d0e..9ec4b9b505e 100644
--- a/go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go
+++ b/go/analysis/passes/fieldalignment/cmd/fieldalignment/main.go
@@ -1,6 +1,9 @@
 // Copyright 2021 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.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/findcall/cmd/findcall/main.go b/go/analysis/passes/findcall/cmd/findcall/main.go
index e0ce9137d61..1ada9668313 100644
--- a/go/analysis/passes/findcall/cmd/findcall/main.go
+++ b/go/analysis/passes/findcall/cmd/findcall/main.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // The findcall command runs the findcall analyzer.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/httpmux/cmd/httpmux/main.go b/go/analysis/passes/httpmux/cmd/httpmux/main.go
index e8a631157dc..5933df923da 100644
--- a/go/analysis/passes/httpmux/cmd/httpmux/main.go
+++ b/go/analysis/passes/httpmux/cmd/httpmux/main.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // The httpmux command runs the httpmux analyzer.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go b/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go
index 42250f93df8..32390be1643 100644
--- a/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go
+++ b/go/analysis/passes/ifaceassert/cmd/ifaceassert/main.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // The ifaceassert command runs the ifaceassert analyzer.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/lostcancel/cmd/lostcancel/main.go b/go/analysis/passes/lostcancel/cmd/lostcancel/main.go
index 0bba8465242..3f2ac7c38f5 100644
--- a/go/analysis/passes/lostcancel/cmd/lostcancel/main.go
+++ b/go/analysis/passes/lostcancel/cmd/lostcancel/main.go
@@ -4,6 +4,9 @@
 
 // The lostcancel command applies the golang.org/x/tools/go/analysis/passes/lostcancel
 // analysis to the specified packages of Go source code.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/nilness/cmd/nilness/main.go b/go/analysis/passes/nilness/cmd/nilness/main.go
index 136ac254a45..91b4d5c44b3 100644
--- a/go/analysis/passes/nilness/cmd/nilness/main.go
+++ b/go/analysis/passes/nilness/cmd/nilness/main.go
@@ -4,6 +4,9 @@
 
 // The nilness command applies the golang.org/x/tools/go/analysis/passes/nilness
 // analysis to the specified packages of Go source code.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/shadow/cmd/shadow/main.go b/go/analysis/passes/shadow/cmd/shadow/main.go
index f9e36ecee95..38de46beb3e 100644
--- a/go/analysis/passes/shadow/cmd/shadow/main.go
+++ b/go/analysis/passes/shadow/cmd/shadow/main.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // The shadow command runs the shadow analyzer.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/stringintconv/cmd/stringintconv/main.go b/go/analysis/passes/stringintconv/cmd/stringintconv/main.go
index 118b9579a50..8dfb9a2056d 100644
--- a/go/analysis/passes/stringintconv/cmd/stringintconv/main.go
+++ b/go/analysis/passes/stringintconv/cmd/stringintconv/main.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // The stringintconv command runs the stringintconv analyzer.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/unmarshal/cmd/unmarshal/main.go b/go/analysis/passes/unmarshal/cmd/unmarshal/main.go
index 1a17cd64de3..fd69013fa59 100644
--- a/go/analysis/passes/unmarshal/cmd/unmarshal/main.go
+++ b/go/analysis/passes/unmarshal/cmd/unmarshal/main.go
@@ -3,6 +3,9 @@
 // license that can be found in the LICENSE file.
 
 // The unmarshal command runs the unmarshal analyzer.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/analysis/passes/unusedresult/cmd/unusedresult/main.go b/go/analysis/passes/unusedresult/cmd/unusedresult/main.go
index 8116c6e06e9..635883c4051 100644
--- a/go/analysis/passes/unusedresult/cmd/unusedresult/main.go
+++ b/go/analysis/passes/unusedresult/cmd/unusedresult/main.go
@@ -4,6 +4,9 @@
 
 // The unusedresult command applies the golang.org/x/tools/go/analysis/passes/unusedresult
 // analysis to the specified packages of Go source code.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go
index aab3362dbfd..7387e7fd10e 100644
--- a/go/packages/gopackages/main.go
+++ b/go/packages/gopackages/main.go
@@ -6,6 +6,9 @@
 // how to use golang.org/x/tools/go/packages to load, parse,
 // type-check, and print one or more Go packages.
 // Its precise output is unspecified and may change.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/packages/internal/nodecount/nodecount.go b/go/packages/internal/nodecount/nodecount.go
index a9f25bfdc6c..4b36a579ac0 100644
--- a/go/packages/internal/nodecount/nodecount.go
+++ b/go/packages/internal/nodecount/nodecount.go
@@ -13,6 +13,9 @@
 // A typical distribution is 40% identifiers, 10% literals, 8%
 // selectors, and 6% calls; around 3% each of BinaryExpr, BlockStmt,
 // AssignStmt, Field, and Comment; and the rest accounting for 20%.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (
diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go
index e8ab0540cee..e8b8cb9bbbe 100644
--- a/go/types/internal/play/play.go
+++ b/go/types/internal/play/play.go
@@ -9,6 +9,9 @@
 // It is intended for convenient exploration and debugging of
 // go/types. The command and its web interface are not officially
 // supported and they may be changed arbitrarily in the future.
+
+//go:debug gotypesalias=0
+
 package main
 
 import (

From a02ee353deaf34f19b8618d96cc313b20a799fce Mon Sep 17 00:00:00 2001
From: Tim King 
Date: Fri, 27 Sep 2024 13:07:51 -0700
Subject: [PATCH 096/102] go/analysis/passes/stdversion: reenable tests

Enable units tests after 1.23. Tests were rewritten for the changes
to the 1.21 file version event horizon.

The test is not enabled for 1.22. A backport for golang/go#68658 may be required.

For golang/go#68658

Change-Id: I15f46e3ff3de7c02592d5a357ae199116323c29f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/615685
Reviewed-by: Alan Donovan 
Commit-Queue: Tim King 
LUCI-TryBot-Result: Go LUCI 
---
 .../passes/stdversion/stdversion_test.go      |  15 +-
 .../passes/stdversion/testdata/test.txtar     | 188 ++++++++++++------
 2 files changed, 142 insertions(+), 61 deletions(-)

diff --git a/go/analysis/passes/stdversion/stdversion_test.go b/go/analysis/passes/stdversion/stdversion_test.go
index cee636082a1..71dc1de0ec8 100644
--- a/go/analysis/passes/stdversion/stdversion_test.go
+++ b/go/analysis/passes/stdversion/stdversion_test.go
@@ -10,17 +10,22 @@ import (
 
 	"golang.org/x/tools/go/analysis/analysistest"
 	"golang.org/x/tools/go/analysis/passes/stdversion"
+	"golang.org/x/tools/internal/testenv"
 	"golang.org/x/tools/internal/testfiles"
 )
 
 func Test(t *testing.T) {
-	t.Skip("Disabled for golang.org/cl/603895. Fix and re-enable.")
+	testenv.NeedsGo1Point(t, 23) // TODO(#68658): Waiting on 1.22 backport.
+
 	// The test relies on go1.21 std symbols, but the analyzer
 	// itself requires the go1.22 implementation of versions.FileVersions.
 	dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "test.txtar"))
 	analysistest.Run(t, dir, stdversion.Analyzer,
-		"example.com/a",
-		"example.com/sub",
-		"example.com/sub20",
-		"example.com/old")
+		"example.com/basic",
+		"example.com/despite",
+		"example.com/mod20",
+		"example.com/mod21",
+		"example.com/mod22",
+		"example.com/old",
+	)
 }
diff --git a/go/analysis/passes/stdversion/testdata/test.txtar b/go/analysis/passes/stdversion/testdata/test.txtar
index 0d27f112b04..cb04407be9d 100644
--- a/go/analysis/passes/stdversion/testdata/test.txtar
+++ b/go/analysis/passes/stdversion/testdata/test.txtar
@@ -2,18 +2,17 @@ Test of "too new" diagnostics from the stdversion analyzer.
 
 This test references go1.21 and go1.22 symbols from std.
 
-It uses a txtar file due to golang/go#37054.
-
 See also gopls/internal/test/marker/testdata/diagnostics/stdversion.txt
 which runs the same test within the gopls analysis driver, to ensure
 coverage of per-file Go version support.
 
 -- go.work --
-go 1.21
+go 1.22
 
 use .
-use sub
-use sub20
+use mod20
+use mod21
+use mod22
 use old
 
 -- go.mod --
@@ -21,8 +20,9 @@ module example.com
 
 go 1.21
 
--- a/a.go --
-package a
+-- basic/basic.go --
+// File version is 1.21.
+package basic
 
 import "go/types"
 
@@ -43,13 +43,77 @@ func _() {
 	a.Underlying() // no diagnostic
 }
 
--- sub/go.mod --
-module example.com/sub
+-- despite/errors.go --
+// File version is 1.21.
+
+// Check that RunDespiteErrors is enabled.
+package ignore
+
+import "go/types"
+
+func _() {
+	// report something before the syntax error.
+	_ = new(types.Info).FileVersions // want `types.FileVersions requires go1.22 or later \(module is go1.21\)`
+}
+
+invalid syntax // exercise RunDespiteErrors
+
+-- mod20/go.mod --
+module example.com/mod20
+
+go 1.20
+
+-- mod20/notag.go --
+// The 1.20 module is before the forward compatibility regime:
+// The file's build tag effects selection, but
+// not language semantics, so stdversion is silent.
+
+package mod20
+
+import "go/types"
+
+func _() {
+	var _ types.Alias
+}
+
+-- mod20/tag16.go --
+// The 1.20 module is before the forward compatibility regime:
+// The file's build tag effects selection, but
+// not language semantics, so stdversion is silent.
+
+//go:build go1.16
+
+package mod20
+
+import "bytes"
+import "go/types"
+
+var _ = bytes.Clone
+var _ = types.Alias
+
+-- mod20/tag22.go --
+// The 1.20 module is before the forward compatibility regime:
+// The file's build tag effects selection, but
+// not language semantics, so stdversion is silent.
+
+//go:build go1.22
+
+package mod20
+
+import "bytes"
+import "go/types"
+
+var _ = bytes.Clone
+var _ = types.Alias
+
+-- mod21/go.mod --
+module example.com/mod21
 
 go 1.21
 
--- sub/sub.go --
-package sub
+-- mod21/notag.go --
+// File version is 1.21.
+package mod21
 
 import "go/types"
 
@@ -70,79 +134,91 @@ func _() {
 	a.Underlying() // no diagnostic
 }
 
-invalid syntax // exercise RunDespiteErrors
+-- mod21/tag16.go --
+// File version is 1.21.
+//
+// The module is within the forward compatibility regime so
+// the build tag (1.16) can modify the file version, but it cannot
+// go below the 1.21 "event horizon" (#68658).
 
--- sub/tagged.go --
-//go:build go1.22
+//go:build go1.16
 
-package sub
+package mod21
 
+import "bytes"
 import "go/types"
 
-func _() {
-	// old package-level type
-	var _ types.Info
+var _ = bytes.Clone
+var _ = types.Alias // want `types.Alias requires go1.22 or later \(module is go1.21\)`
 
-	// new field of older type
-	_ = new(types.Info).FileVersions
+-- mod21/tag22.go --
+// File version is 1.22.
+//
+// The module is within the forward compatibility regime so
+// the build tag (1.22) updates the file version to 1.22.
 
-	// new method of older type
-	new(types.Info).PkgNameOf
+//go:build go1.22
 
-	// new package-level type
-	var a types.Alias
+package mod21
 
-	// new method of new type
-	a.Underlying()
-}
+import "bytes"
+import "go/types"
 
--- old/go.mod --
-module example.com/old
+var _ = bytes.Clone
+var _ = types.Alias 
 
-go 1.5
+-- mod22/go.mod --
+module example.com/mod22
 
--- old/old.go --
-package old
+go 1.22
+
+-- mod22/notag.go --
+// File version is 1.22.
+package mod22
 
 import "go/types"
 
-var _ types.Alias // no diagnostic: go.mod is too old for us to care
+func _() {
+	var _ = bytes.Clone
+	var _ = types.Alias
+}
 
--- sub/oldtagged.go --
-// The file Go version (1.16) overrides the go.mod Go version (1.21),
-// even when this means a downgrade (#67123).
-// (stdversion is silent for go.mod versions before 1.21:
-// before the forward compatibility regime, the meaning
-// of the go.mod version was not clearly defined.)
+-- mod22/tag16.go --
+// File version is 1.21.
+//
+// The module is within the forward compatibility regime so
+// the build tag (1.16) can modify the file version, but it cannot
+// go below the 1.21 "event horizon" (#68658).
 
 //go:build go1.16
 
-package sub
+package mod22
 
 import "bytes"
 import "go/types"
 
-var _ = bytes.Clone // want `bytes.Clone requires go1.20 or later \(file is go1.16\)`
-var _ = types.Alias // want `types.Alias requires go1.22 or later \(file is go1.16\)`
+var _ = bytes.Clone
+var _ = types.Alias // want `types.Alias requires go1.22 or later \(file is go1.21\)`
 
--- sub20/go.mod --
-module example.com/sub20
+-- old/go.mod --
+module example.com/old
 
-go 1.20
+go 1.5
 
--- sub20/oldtagged.go --
-// Same test again, but with a go1.20 mod,
-// before the forward compatibility regime:
-// The file's build tag effects selection, but
-// not language semantics, so stdversion is silent.
+-- old/notag.go --
+package old
 
-//go:build go1.16
+import "go/types"
 
-package sub
+var _ types.Alias // no diagnostic: go.mod is too old for us to care
 
-import "bytes"
-import "go/types"
+-- old/tag21.go --
+// The build tag is ignored due to the module version.
 
-var _ = bytes.Clone
-var _ = types.Alias
+//go:build go1.21
+
+package old
+
+import "go/types"
 
+var _ = types.Alias // no diagnostic: go.mod is too old for us to care

From dd745ec14bc7518bb8d1c06bf50d6e092c3bed54 Mon Sep 17 00:00:00 2001
From: Robert Griesemer 
Date: Wed, 2 Oct 2024 09:52:01 -0700
Subject: [PATCH 097/102] gopls/internal/test/marker: update regression test
 issue68918.txt

The fix for golang/go#69092 produces an extra error message in Go 1.24.
Ignore it.

Updates golang/go#68918.
Updates golang/go#69092.

Change-Id: I41ecff6fe916a04ed943e29ad10184d340ef84d5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/617475
Reviewed-by: Alan Donovan 
Reviewed-by: Robert Griesemer 
Auto-Submit: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
---
 .../internal/test/marker/testdata/highlight/issue68918.txt  | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/gopls/internal/test/marker/testdata/highlight/issue68918.txt b/gopls/internal/test/marker/testdata/highlight/issue68918.txt
index ff2afc18f07..b6ffb882df4 100644
--- a/gopls/internal/test/marker/testdata/highlight/issue68918.txt
+++ b/gopls/internal/test/marker/testdata/highlight/issue68918.txt
@@ -1,6 +1,12 @@
 Regression test for https://github.com/golang/go/issues/68918:
 crash due to missing type information in CompositeLit.
 
+The corresponding go/types fix in Go 1.24 introduces a
+new error message, hence the -ignore_extra_diags flag.
+
+-- flags --
+-ignore_extra_diags
+
 -- a.go --
 package a
 

From a19eef6bcb20e66f8d655ff8c1956804293a7bdf Mon Sep 17 00:00:00 2001
From: Rob Findley 
Date: Thu, 19 Sep 2024 13:05:48 +0000
Subject: [PATCH 098/102] gopls/internal/cache: express packageHandle as a
 state machine

In preparation for storing active packages on packageHandle, express the
various package handle states using a new packageState type, and hold on
to package handle data even if their local files may have changed, as
long as their metadata did not change.

Also: rename buildPackageHandle to evaluatePackageHandle, which better
matches its meaning, and move the package ID index to the View, since it
is shared across all snapshots.

Change-Id: I2c14613d320b1121f20bb3960d42370bef5ad98b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/614164
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Alan Donovan 
---
 gopls/internal/cache/check.go    | 342 ++++++++++++++++---------------
 gopls/internal/cache/session.go  |   2 +-
 gopls/internal/cache/snapshot.go |  28 +--
 gopls/internal/cache/view.go     |   4 +
 4 files changed, 195 insertions(+), 181 deletions(-)

diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go
index 601a3e4fd32..08d57f4e657 100644
--- a/gopls/internal/cache/check.go
+++ b/gopls/internal/cache/check.go
@@ -77,6 +77,7 @@ func (b *typeCheckBatch) addHandles(handles map[PackageID]*packageHandle) {
 	b.handleMu.Lock()
 	defer b.handleMu.Unlock()
 	for id, ph := range handles {
+		assert(ph.state == validKey, "invalid handle")
 		if alt, ok := b._handles[id]; ok {
 			// Once handles have been reevaluated, they should not change. Therefore,
 			// we should only ever encounter exactly one handle instance for a given
@@ -832,40 +833,62 @@ func (b *typeCheckBatch) importLookup(mp *metadata.Package) func(PackagePath) Pa
 	}
 }
 
-// A packageHandle holds inputs required to compute a Package, including
-// metadata, derived diagnostics, files, and settings. Additionally,
-// packageHandles manage a key for these inputs, to use in looking up
-// precomputed results.
+// A packageState is the state of a [packageHandle]; see below for details.
+type packageState uint8
+
+const (
+	validMetadata  packageState = iota // the package has valid metadata (initial state)
+	validLocalData                     // local package files have been analyzed
+	validKey                           // dependencies have been analyzed, and key produced
+)
+
+// A packageHandle holds information derived from a metadata.Package, and
+// records its degree of validity as state changes occur: successful analysis
+// causes the state to progress; invalidation due to changes causes it to
+// regress.
 //
-// packageHandles may be invalid following an invalidation via snapshot.clone,
-// but the handles returned by getPackageHandles will always be valid.
+// In the initial state (validMetadata), all we know is the metadata for the
+// package itself. This is the lowest state, and it cannot become invalid
+// because the metadata for a given snapshot never changes. (Each handle is
+// implicitly associated with a Snapshot.)
 //
-// packageHandles are critical for implementing "precise pruning" in gopls:
-// packageHandle.key is a hash of a precise set of inputs, such as package
-// files and "reachable" syntax, that may affect type checking.
+// After the files of the package have been read (validLocalData), we can
+// perform computations that are local to that package, such as parsing, or
+// building the symbol reference graph (SRG). This information is invalidated
+// by a change to any file in the package. The local information is thus
+// sufficient to form a cache key for saved parsed trees or the SRG.
 //
-// packageHandles also keep track of state that allows gopls to compute, and
-// then quickly recompute, these keys. This state is split into two categories:
-//   - local state, which depends only on the package's local files and metadata
-//   - other state, which includes data derived from dependencies.
+// Once all dependencies have been analyzed (validKey), we can type-check the
+// package. This information is invalidated by any change to the package
+// itself, or to any dependency that is transitively reachable through the SRG.
+// The cache key for saved type information must thus incorporate information
+// from all reachable dependencies. This reachability analysis implements what
+// we sometimes refer to as "precise pruning", or fine-grained invalidation:
+// https://go.dev/blog/gopls-scalability#invalidation
 //
-// Dividing the data in this way allows gopls to minimize invalidation when a
-// package is modified. For example, any change to a package file fully
-// invalidates the package handle. On the other hand, if that change was not
-// metadata-affecting it may be the case that packages indirectly depending on
-// the modified package are unaffected by the change. For that reason, we have
-// two types of invalidation, corresponding to the two types of data above:
-//   - deletion of the handle, which occurs when the package itself changes
-//   - clearing of the validated field, which marks the package as possibly
-//     invalid.
+// Following a change, the packageHandle is cloned in the new snapshot with a
+// new state set to its least known valid state, as described above: if package
+// files changed, it is reset to validMetadata; if dependencies changed, it is
+// reset to validLocalData. However, the derived data from its previous state
+// is not yet removed, as keys may not have changed after they are reevaluated,
+// in which case we can avoid recomputing the derived data.
 //
-// With the second type of invalidation, packageHandles are re-evaluated from the
-// bottom up. If this process encounters a packageHandle whose deps have not
-// changed (as detected by the depkeys field), then the packageHandle in
-// question must also not have changed, and we need not re-evaluate its key.
+// See [packageHandleBuilder.evaluatePackageHandle] for more details of the
+// reevaluation algorithm.
+//
+// packageHandles are immutable once they are stored in the Snapshot.packages
+// map: any changes to packageHandle fields evaluatePackageHandle must be made
+// to a cloned packageHandle, and inserted back into Snapshot.packages. Data
+// referred to by the packageHandle may be shared by multiple clones, and so
+// referents must not be mutated.
 type packageHandle struct {
 	mp *metadata.Package
 
+	// state indicates which data below are still valid.
+	state packageState
+
+	// Local data:
+
 	// loadDiagnostics memoizes the result of processing error messages from
 	// go/packages (i.e. `go list`).
 	//
@@ -883,27 +906,19 @@ type packageHandle struct {
 	// (Nevertheless, since the lifetime of load diagnostics matches that of the
 	// Metadata, it is convenient to memoize them here.)
 	loadDiagnostics []*Diagnostic
-
-	// Local data:
-
 	// localInputs holds all local type-checking localInputs, excluding
 	// dependencies.
-	localInputs typeCheckInputs
+	localInputs *typeCheckInputs
 	// localKey is a hash of localInputs.
 	localKey file.Hash
 	// refs is the result of syntactic dependency analysis produced by the
-	// typerefs package.
+	// typerefs package. Derived from localInputs.
 	refs map[string][]typerefs.Symbol
 
-	// Data derived from dependencies:
+	// Keys, computed through reachability analysis of dependencies.
 
-	// validated indicates whether the current packageHandle is known to have a
-	// valid key. Invalidated package handles are stored for packages whose
-	// type information may have changed.
-	validated bool
 	// depKeys records the key of each dependency that was used to calculate the
-	// key above. If the handle becomes invalid, we must re-check that each still
-	// matches.
+	// key below. If state < validKey, we must re-check that each still matches.
 	depKeys map[PackageID]file.Hash
 	// key is the hashed key for the package.
 	//
@@ -912,36 +927,35 @@ type packageHandle struct {
 	key file.Hash
 }
 
-// clone returns a copy of the receiver with the validated bit set to the
-// provided value.
-func (ph *packageHandle) clone(validated bool) *packageHandle {
-	copy := *ph
-	copy.validated = validated
-	return ©
+// clone returns a shallow copy of the receiver.
+func (ph *packageHandle) clone() *packageHandle {
+	clone := *ph
+	return &clone
 }
 
 // getPackageHandles gets package handles for all given ids and their
-// dependencies, recursively.
+// dependencies, recursively. The resulting [packageHandle] values are fully
+// evaluated (their state will be at least validKey).
 func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[PackageID]*packageHandle, error) {
 	// perform a two-pass traversal.
 	//
 	// On the first pass, build up a bidirectional graph of handle nodes, and collect leaves.
 	// Then build package handles from bottom up.
-
-	s.mu.Lock() // guard s.meta and s.packages below
 	b := &packageHandleBuilder{
 		s:              s,
 		transitiveRefs: make(map[typerefs.IndexID]*partialRefs),
 		nodes:          make(map[typerefs.IndexID]*handleNode),
 	}
 
+	meta := s.MetadataGraph()
+
 	var leaves []*handleNode
 	var makeNode func(*handleNode, PackageID) *handleNode
 	makeNode = func(from *handleNode, id PackageID) *handleNode {
-		idxID := b.s.pkgIndex.IndexID(id)
+		idxID := s.view.pkgIndex.IndexID(id)
 		n, ok := b.nodes[idxID]
 		if !ok {
-			mp := s.meta.Packages[id]
+			mp := meta.Packages[id]
 			if mp == nil {
 				panic(fmt.Sprintf("nil metadata for %q", id))
 			}
@@ -950,9 +964,6 @@ func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[
 				idxID:           idxID,
 				unfinishedSuccs: int32(len(mp.DepsByPkgPath)),
 			}
-			if entry, hit := b.s.packages.Get(mp.ID); hit {
-				n.ph = entry
-			}
 			if n.unfinishedSuccs == 0 {
 				leaves = append(leaves, n)
 			} else {
@@ -971,12 +982,10 @@ func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[
 	}
 	for _, id := range ids {
 		if ctx.Err() != nil {
-			s.mu.Unlock()
 			return nil, ctx.Err()
 		}
 		makeNode(nil, id)
 	}
-	s.mu.Unlock()
 
 	g, ctx := errgroup.WithContext(ctx)
 
@@ -997,15 +1006,16 @@ func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[
 				return ctx.Err()
 			}
 
-			b.buildPackageHandle(ctx, n)
+			if err := b.evaluatePackageHandle(ctx, n); err != nil {
+				return err
+			}
 
 			for _, pred := range n.preds {
 				if atomic.AddInt32(&pred.unfinishedSuccs, -1) == 0 {
 					enqueue(pred)
 				}
 			}
-
-			return n.err
+			return nil
 		})
 	}
 	for _, leaf := range leaves {
@@ -1047,7 +1057,6 @@ type handleNode struct {
 	mp              *metadata.Package
 	idxID           typerefs.IndexID
 	ph              *packageHandle
-	err             error
 	preds           []*handleNode
 	succs           map[PackageID]*handleNode
 	unfinishedSuccs int32
@@ -1073,7 +1082,7 @@ func (b *packageHandleBuilder) getTransitiveRefs(pkgID PackageID) map[string]*ty
 	b.transitiveRefsMu.Lock()
 	defer b.transitiveRefsMu.Unlock()
 
-	idxID := b.s.pkgIndex.IndexID(pkgID)
+	idxID := b.s.view.pkgIndex.IndexID(pkgID)
 	trefs, ok := b.transitiveRefs[idxID]
 	if !ok {
 		trefs = &partialRefs{
@@ -1084,12 +1093,12 @@ func (b *packageHandleBuilder) getTransitiveRefs(pkgID PackageID) map[string]*ty
 
 	if !trefs.complete {
 		trefs.complete = true
-		ph := b.nodes[idxID].ph
-		for name := range ph.refs {
+		node := b.nodes[idxID]
+		for name := range node.ph.refs {
 			if ('A' <= name[0] && name[0] <= 'Z') || token.IsExported(name) {
 				if _, ok := trefs.refs[name]; !ok {
-					pkgs := b.s.pkgIndex.NewSet()
-					for _, sym := range ph.refs[name] {
+					pkgs := b.s.view.pkgIndex.NewSet()
+					for _, sym := range node.ph.refs[name] {
 						pkgs.Add(sym.Package)
 						otherSet := b.getOneTransitiveRefLocked(sym)
 						pkgs.Union(otherSet)
@@ -1140,7 +1149,7 @@ func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *t
 			// point release.
 			//
 			// TODO(rfindley): in the future, we should turn this into an assertion.
-			bug.Reportf("missing reference to package %s", b.s.pkgIndex.PackageID(sym.Package))
+			bug.Reportf("missing reference to package %s", b.s.view.pkgIndex.PackageID(sym.Package))
 			return nil
 		}
 
@@ -1152,7 +1161,7 @@ func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *t
 		// See the "cycle detected" bug report above.
 		trefs.refs[sym.Name] = nil
 
-		pkgs := b.s.pkgIndex.NewSet()
+		pkgs := b.s.view.pkgIndex.NewSet()
 		for _, sym2 := range n.ph.refs[sym.Name] {
 			pkgs.Add(sym2.Package)
 			otherSet := b.getOneTransitiveRefLocked(sym2)
@@ -1164,153 +1173,152 @@ func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *t
 	return pkgs
 }
 
-// buildPackageHandle gets or builds a package handle for the given id, storing
-// its result in the snapshot.packages map.
+// evaluatePackageHandle recomputes the derived information in the package handle.
+// On success, the handle's state is validKey.
 //
-// buildPackageHandle must only be called from getPackageHandles.
-func (b *packageHandleBuilder) buildPackageHandle(ctx context.Context, n *handleNode) {
-	var prevPH *packageHandle
-	if n.ph != nil {
-		// Existing package handle: if it is valid, return it. Otherwise, create a
-		// copy to update.
-		if n.ph.validated {
-			return
-		}
-		prevPH = n.ph
-		// Either prevPH is still valid, or we will update the key and depKeys of
-		// this copy. In either case, the result will be valid.
-		n.ph = prevPH.clone(true)
+// evaluatePackageHandle must only be called from getPackageHandles.
+func (b *packageHandleBuilder) evaluatePackageHandle(ctx context.Context, n *handleNode) (err error) {
+	// Initialize n.ph.
+	var hit bool
+	b.s.mu.Lock()
+	n.ph, hit = b.s.packages.Get(n.mp.ID)
+	b.s.mu.Unlock()
+
+	if hit && n.ph.state >= validKey {
+		return nil // already valid
 	} else {
+		// We'll need to update the package handle. Since this could happen
+		// concurrently, make a copy.
+		if hit {
+			n.ph = n.ph.clone()
+		} else {
+			n.ph = &packageHandle{
+				mp:    n.mp,
+				state: validMetadata,
+			}
+		}
+	}
+
+	defer func() {
+		if err == nil {
+			assert(n.ph.state == validKey, "invalid handle")
+			// Record the now valid key in the snapshot.
+			// There may be a race, so avoid the write if the recorded handle is
+			// already valid.
+			b.s.mu.Lock()
+			if alt, ok := b.s.packages.Get(n.mp.ID); !ok || alt.state < n.ph.state {
+				b.s.packages.Set(n.mp.ID, n.ph, nil)
+			} else {
+				n.ph = alt
+			}
+			b.s.mu.Unlock()
+		}
+	}()
+
+	// Invariant: n.ph is either
+	// - a new handle in state validMetadata, or
+	// - a clone of an existing handle in state validMetadata or validLocalData.
+
+	// State transition: validMetadata -> validLocalInputs.
+	localKeyChanged := false
+	if n.ph.state < validLocalData {
+		prevLocalKey := n.ph.localKey // may be zero
 		// No package handle: read and analyze the package syntax.
 		inputs, err := b.s.typeCheckInputs(ctx, n.mp)
 		if err != nil {
-			n.err = err
-			return
+			return err
 		}
 		refs, err := b.s.typerefs(ctx, n.mp, inputs.compiledGoFiles)
 		if err != nil {
-			n.err = err
-			return
-		}
-		n.ph = &packageHandle{
-			mp:              n.mp,
-			loadDiagnostics: computeLoadDiagnostics(ctx, b.s, n.mp),
-			localInputs:     inputs,
-			localKey:        localPackageKey(inputs),
-			refs:            refs,
-			validated:       true,
+			return err
 		}
+		n.ph.loadDiagnostics = computeLoadDiagnostics(ctx, b.s, n.mp)
+		n.ph.localInputs = inputs
+		n.ph.localKey = localPackageKey(inputs)
+		n.ph.refs = refs
+		n.ph.state = validLocalData
+		localKeyChanged = n.ph.localKey != prevLocalKey
 	}
 
-	// ph either did not exist, or was invalid. We must re-evaluate deps and key.
-	if err := b.evaluatePackageHandle(prevPH, n); err != nil {
-		n.err = err
-		return
-	}
-
-	assert(n.ph.validated, "unvalidated handle")
+	assert(n.ph.state == validLocalData, "unexpected handle state")
 
-	// Ensure the result (or an equivalent) is recorded in the snapshot.
-	b.s.mu.Lock()
-	defer b.s.mu.Unlock()
-
-	// Check that the metadata has not changed
-	// (which should invalidate this handle).
+	// Optimization: if the local package information did not change, nor did any
+	// of the dependencies, we don't need to re-run the reachability algorithm.
 	//
-	// TODO(rfindley): eventually promote this to an assert.
-	// TODO(rfindley): move this to after building the package handle graph?
-	if b.s.meta.Packages[n.mp.ID] != n.mp {
-		bug.Reportf("stale metadata for %s", n.mp.ID)
-	}
-
-	// Check the packages map again in case another goroutine got there first.
-	if alt, ok := b.s.packages.Get(n.mp.ID); ok && alt.validated {
-		if alt.mp != n.mp {
-			bug.Reportf("existing package handle does not match for %s", n.mp.ID)
-		}
-		n.ph = alt
-	} else {
-		b.s.packages.Set(n.mp.ID, n.ph, nil)
-	}
-}
-
-// evaluatePackageHandle validates and/or computes the key of ph, setting key,
-// depKeys, and the validated flag on ph.
-//
-// It uses prevPH to avoid recomputing keys that can't have changed, since
-// their depKeys did not change.
-//
-// See the documentation for packageHandle for more details about packageHandle
-// state, and see the documentation for the typerefs package for more details
-// about precise reachability analysis.
-func (b *packageHandleBuilder) evaluatePackageHandle(prevPH *packageHandle, n *handleNode) error {
-	// Opt: if no dep keys have changed, we need not re-evaluate the key.
-	if prevPH != nil {
-		depsChanged := false
-		assert(len(prevPH.depKeys) == len(n.succs), "mismatching dep count")
+	// Concretely: suppose A -> B -> C -> D, where '->' means "imports". If I
+	// type in a function body of D, I will probably invalidate types in D that C
+	// uses, because positions change, and therefore the package key of C will
+	// change. But B probably doesn't reach any types in D, and therefore the
+	// package key of B will not change. We still need to re-run the reachability
+	// algorithm on B to confirm. But if the key of B did not change, we don't
+	// even need to run the reachability algorithm on A.
+	if !localKeyChanged &&
+		n.ph.depKeys != nil && // n.ph was previously evaluated
+		len(n.ph.depKeys) == len(n.succs) {
+
+		unchanged := true
 		for id, succ := range n.succs {
-			oldKey, ok := prevPH.depKeys[id]
+			oldKey, ok := n.ph.depKeys[id]
 			assert(ok, "missing dep")
 			if oldKey != succ.ph.key {
-				depsChanged = true
+				unchanged = false
 				break
 			}
 		}
-		if !depsChanged {
-			return nil // key cannot have changed
+		if unchanged {
+			n.ph.state = validKey
+			return nil
 		}
 	}
 
-	// Deps have changed, so we must re-evaluate the key.
+	// State transition: validLocalInputs -> validKey
+	//
+	// If we get here, it must be the case that deps have changed, so we must
+	// run the reachability algorithm.
 	n.ph.depKeys = make(map[PackageID]file.Hash)
 
 	// See the typerefs package: the reachable set of packages is defined to be
 	// the set of packages containing syntax that is reachable through the
 	// exported symbols in the dependencies of n.ph.
-	reachable := b.s.pkgIndex.NewSet()
+	reachable := b.s.view.pkgIndex.NewSet()
 	for depID, succ := range n.succs {
 		n.ph.depKeys[depID] = succ.ph.key
 		reachable.Add(succ.idxID)
 		trefs := b.getTransitiveRefs(succ.mp.ID)
-		if trefs == nil {
-			// A predecessor failed to build due to e.g. context cancellation.
-			return fmt.Errorf("missing transitive refs for %s", succ.mp.ID)
-		}
+		assert(trefs != nil, "nil trefs")
 		for _, set := range trefs {
 			reachable.Union(set)
 		}
 	}
 
-	// Collect reachable handles.
-	var reachableHandles []*packageHandle
+	// Collect reachable nodes.
+	var reachableNodes []*handleNode
 	// In the presence of context cancellation, any package may be missing.
 	// We need all dependencies to produce a valid key.
-	missingReachablePackage := false
 	reachable.Elems(func(id typerefs.IndexID) {
 		dh := b.nodes[id]
 		if dh == nil {
-			missingReachablePackage = true
+			// Previous code reported an error (not a bug) here.
+			bug.Reportf("missing reachable node for %q", id)
 		} else {
-			assert(dh.ph.validated, "unvalidated dependency")
-			reachableHandles = append(reachableHandles, dh.ph)
+			reachableNodes = append(reachableNodes, dh)
 		}
 	})
-	if missingReachablePackage {
-		return fmt.Errorf("missing reachable package")
-	}
+
 	// Sort for stability.
-	sort.Slice(reachableHandles, func(i, j int) bool {
-		return reachableHandles[i].mp.ID < reachableHandles[j].mp.ID
+	sort.Slice(reachableNodes, func(i, j int) bool {
+		return reachableNodes[i].mp.ID < reachableNodes[j].mp.ID
 	})
 
 	// Key is the hash of the local key, and the local key of all reachable
 	// packages.
 	depHasher := sha256.New()
 	depHasher.Write(n.ph.localKey[:])
-	for _, rph := range reachableHandles {
-		depHasher.Write(rph.localKey[:])
+	for _, dh := range reachableNodes {
+		depHasher.Write(dh.ph.localKey[:])
 	}
 	depHasher.Sum(n.ph.key[:0])
+	n.ph.state = validKey
 
 	return nil
 }
@@ -1329,7 +1337,7 @@ func (s *Snapshot) typerefs(ctx context.Context, mp *metadata.Package, cgfs []fi
 	if err != nil {
 		return nil, err
 	}
-	classes := typerefs.Decode(s.pkgIndex, data)
+	classes := typerefs.Decode(s.view.pkgIndex, data)
 	refs := make(map[string][]typerefs.Symbol)
 	for _, class := range classes {
 		for _, decl := range class.Decls {
@@ -1419,7 +1427,7 @@ type typeCheckInputs struct {
 	viewType                   ViewType
 }
 
-func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (typeCheckInputs, error) {
+func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (*typeCheckInputs, error) {
 	// Read both lists of files of this package.
 	//
 	// Parallelism is not necessary here as the files will have already been
@@ -1432,11 +1440,11 @@ func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (t
 	// The need should be rare.
 	goFiles, err := readFiles(ctx, s, mp.GoFiles)
 	if err != nil {
-		return typeCheckInputs{}, err
+		return nil, err
 	}
 	compiledGoFiles, err := readFiles(ctx, s, mp.CompiledGoFiles)
 	if err != nil {
-		return typeCheckInputs{}, err
+		return nil, err
 	}
 
 	goVersion := ""
@@ -1444,7 +1452,7 @@ func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (t
 		goVersion = mp.Module.GoVersion
 	}
 
-	return typeCheckInputs{
+	return &typeCheckInputs{
 		id:              mp.ID,
 		pkgPath:         mp.PkgPath,
 		name:            mp.Name,
@@ -1475,7 +1483,7 @@ func readFiles(ctx context.Context, fs file.Source, uris []protocol.DocumentURI)
 
 // localPackageKey returns a key for local inputs into type-checking, excluding
 // dependency information: files, metadata, and configuration.
-func localPackageKey(inputs typeCheckInputs) file.Hash {
+func localPackageKey(inputs *typeCheckInputs) file.Hash {
 	hasher := sha256.New()
 
 	// In principle, a key must be the hash of an
@@ -1669,7 +1677,7 @@ func (b *typeCheckBatch) checkPackage(ctx context.Context, ph *packageHandle) (*
 // e.g. "go1" or "go1.2" or "go1.2.3"
 var goVersionRx = regexp.MustCompile(`^go[1-9][0-9]*(?:\.(0|[1-9][0-9]*)){0,2}$`)
 
-func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs, onError func(e error)) *types.Config {
+func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs *typeCheckInputs, onError func(e error)) *types.Config {
 	cfg := &types.Config{
 		Sizes: inputs.sizes,
 		Error: onError,
@@ -1914,7 +1922,7 @@ func missingPkgError(from PackageID, pkgPath string, viewType ViewType) error {
 // sequence to a terminal).
 //
 // Fields in typeCheckInputs may affect the resulting diagnostics.
-func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs typeCheckInputs, errs []types.Error) []*Diagnostic {
+func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs []types.Error) []*Diagnostic {
 	var result []*Diagnostic
 
 	// batch records diagnostics for a set of related types.Errors.
diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go
index c5e9aab98a5..65ba7e69d0a 100644
--- a/gopls/internal/cache/session.go
+++ b/gopls/internal/cache/session.go
@@ -230,6 +230,7 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
 		initialWorkspaceLoad: make(chan struct{}),
 		initializationSema:   make(chan struct{}, 1),
 		baseCtx:              baseCtx,
+		pkgIndex:             typerefs.NewPackageIndex(),
 		parseCache:           s.parseCache,
 		ignoreFilter:         ignoreFilter,
 		fs:                   s.overlayFS,
@@ -257,7 +258,6 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
 		modTidyHandles:   new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
 		modVulnHandles:   new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
 		modWhyHandles:    new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
-		pkgIndex:         typerefs.NewPackageIndex(),
 		moduleUpgrades:   new(persistent.Map[protocol.DocumentURI, map[string]string]),
 		vulns:            new(persistent.Map[protocol.DocumentURI, *vulncheck.Result]),
 	}
diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go
index 566131773fb..004dc5279c0 100644
--- a/gopls/internal/cache/snapshot.go
+++ b/gopls/internal/cache/snapshot.go
@@ -32,7 +32,6 @@ import (
 	"golang.org/x/tools/gopls/internal/cache/methodsets"
 	"golang.org/x/tools/gopls/internal/cache/parsego"
 	"golang.org/x/tools/gopls/internal/cache/testfuncs"
-	"golang.org/x/tools/gopls/internal/cache/typerefs"
 	"golang.org/x/tools/gopls/internal/cache/xrefs"
 	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/filecache"
@@ -191,9 +190,6 @@ type Snapshot struct {
 	importGraphDone chan struct{} // closed when importGraph is set; may be nil
 	importGraph     *importGraph  // copied from preceding snapshot and re-evaluated
 
-	// pkgIndex is an index of package IDs, for efficient storage of typerefs.
-	pkgIndex *typerefs.PackageIndex
-
 	// moduleUpgrades tracks known upgrades for module paths in each modfile.
 	// Each modfile has a map of module name to upgrade version.
 	moduleUpgrades *persistent.Map[protocol.DocumentURI, map[string]string]
@@ -1686,7 +1682,6 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f
 		modWhyHandles:     cloneWithout(s.modWhyHandles, changedFiles, &needsDiagnosis),
 		modVulnHandles:    cloneWithout(s.modVulnHandles, changedFiles, &needsDiagnosis),
 		importGraph:       s.importGraph,
-		pkgIndex:          s.pkgIndex,
 		moduleUpgrades:    cloneWith(s.moduleUpgrades, changed.ModuleUpgrades),
 		vulns:             cloneWith(s.vulns, changed.Vulns),
 	}
@@ -1950,14 +1945,21 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f
 
 	// Invalidated package information.
 	for id, invalidateMetadata := range idsToInvalidate {
-		if _, ok := directIDs[id]; ok || invalidateMetadata {
-			if result.packages.Delete(id) {
-				needsDiagnosis = true
-			}
-		} else {
-			if entry, hit := result.packages.Get(id); hit {
-				needsDiagnosis = true
-				ph := entry.clone(false)
+		// See the [packageHandle] documentation for more details about this
+		// invalidation.
+		if ph, ok := result.packages.Get(id); ok {
+			needsDiagnosis = true
+			if invalidateMetadata {
+				result.packages.Delete(id)
+			} else {
+				// If the package was just invalidated by a dependency, its local
+				// inputs are still valid.
+				ph = ph.clone()
+				if _, ok := directIDs[id]; ok {
+					ph.state = validMetadata // local inputs changed
+				} else {
+					ph.state = min(ph.state, validLocalData) // a dependency changed
+				}
 				result.packages.Set(id, ph, nil)
 			}
 		}
diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go
index 8a5a701d890..93612a763fb 100644
--- a/gopls/internal/cache/view.go
+++ b/gopls/internal/cache/view.go
@@ -27,6 +27,7 @@ import (
 	"time"
 
 	"golang.org/x/tools/gopls/internal/cache/metadata"
+	"golang.org/x/tools/gopls/internal/cache/typerefs"
 	"golang.org/x/tools/gopls/internal/file"
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/settings"
@@ -106,6 +107,9 @@ type View struct {
 
 	importsState *importsState
 
+	// pkgIndex is an index of package IDs, for efficient storage of typerefs.
+	pkgIndex *typerefs.PackageIndex
+
 	// parseCache holds an LRU cache of recently parsed files.
 	parseCache *parseCache
 

From d0d0d9ebc2175ffb592761462bd3a5c2ceab354f Mon Sep 17 00:00:00 2001
From: Rob Findley 
Date: Wed, 2 Oct 2024 13:27:13 +0000
Subject: [PATCH 099/102] gopls/internal/cache: memoize dependent hash on
 analysisNode

Benchmarking demonstrated a nontrivial amount of time spent hashing
dependency information in the computation of analysisNode.cacheKey.
Memoize this hash to reduce cost.

Change-Id: Ic123202fbdf00c9de7b3f697c40f593ef798cd42
Reviewed-on: https://go-review.googlesource.com/c/tools/+/617395
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Alan Donovan 
---
 gopls/internal/cache/analysis.go | 60 +++++++++++++++++++++-----------
 1 file changed, 39 insertions(+), 21 deletions(-)

diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go
index 0d7fc46237d..9debc609048 100644
--- a/gopls/internal/cache/analysis.go
+++ b/gopls/internal/cache/analysis.go
@@ -539,6 +539,9 @@ type analysisNode struct {
 	typesOnce sync.Once      // guards lazy population of types and typesErr fields
 	types     *types.Package // type information lazily imported from summary
 	typesErr  error          // an error producing type information
+
+	depHashOnce sync.Once
+	_depHash    file.Hash // memoized hash of data affecting dependents
 }
 
 func (an *analysisNode) String() string { return string(an.mp.ID) }
@@ -597,6 +600,40 @@ func (an *analysisNode) _import() (*types.Package, error) {
 	return an.types, an.typesErr
 }
 
+// depHash computes the hash of node information that may affect other nodes
+// depending on this node: the package path, export hash, and action results.
+//
+// The result is memoized to avoid redundant work when analysing multiple
+// dependents.
+func (an *analysisNode) depHash() file.Hash {
+	an.depHashOnce.Do(func() {
+		hasher := sha256.New()
+		fmt.Fprintf(hasher, "dep: %s\n", an.mp.PkgPath)
+		fmt.Fprintf(hasher, "export: %s\n", an.summary.DeepExportHash)
+
+		// action results: errors and facts
+		actions := an.summary.Actions
+		names := make([]string, 0, len(actions))
+		for name := range actions {
+			names = append(names, name)
+		}
+		sort.Strings(names)
+		for _, name := range names {
+			summary := actions[name]
+			fmt.Fprintf(hasher, "action %s\n", name)
+			if summary.Err != "" {
+				fmt.Fprintf(hasher, "error %s\n", summary.Err)
+			} else {
+				fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash)
+				// We can safely omit summary.diagnostics
+				// from the key since they have no downstream effect.
+			}
+		}
+		hasher.Sum(an._depHash[:0])
+	})
+	return an._depHash
+}
+
 // analyzeSummary is a gob-serializable summary of successfully
 // applying a list of analyzers to a package.
 type analyzeSummary struct {
@@ -770,27 +807,8 @@ func (an *analysisNode) cacheKey() [sha256.Size]byte {
 
 	// vdeps, in PackageID order
 	for _, vdep := range moremaps.Sorted(an.succs) {
-		fmt.Fprintf(hasher, "dep: %s\n", vdep.mp.PkgPath)
-		fmt.Fprintf(hasher, "export: %s\n", vdep.summary.DeepExportHash)
-
-		// action results: errors and facts
-		actions := vdep.summary.Actions
-		names := make([]string, 0, len(actions))
-		for name := range actions {
-			names = append(names, name)
-		}
-		sort.Strings(names)
-		for _, name := range names {
-			summary := actions[name]
-			fmt.Fprintf(hasher, "action %s\n", name)
-			if summary.Err != "" {
-				fmt.Fprintf(hasher, "error %s\n", summary.Err)
-			} else {
-				fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash)
-				// We can safely omit summary.diagnostics
-				// from the key since they have no downstream effect.
-			}
-		}
+		hash := vdep.depHash()
+		hasher.Write(hash[:])
 	}
 
 	var hash [sha256.Size]byte

From efd951d80771c4af4c6ce5848ad8484a4a8de6f6 Mon Sep 17 00:00:00 2001
From: Alan Donovan 
Date: Mon, 30 Sep 2024 14:27:05 -0400
Subject: [PATCH 100/102] gopls/internal/analysis/stubmethods: merge into
 CodeAction

This change removes the stubmethods analyzer, a roundabout
implementation, and instead computes it directly from the
protocol.QuickFix code action producer.

This is simpler, more efficient, and has noticeably lower
latency (being type-based not analysis based).
We should consider this for the other type-error analyzers.
However, the documentation, formerly generated to analyzers.md,
is now maintained in the Quick Fixes section of diagnostics.md.
More importantly, the `analyzers[stubmethods]` config setting
no longer exists.

Also:
- addEditAction et al: pass Diagnostics as a parameter
  instead of returning a pointer to a mutable CodeAction.
- protocol.Intersect: clarify how its treatment differs from
  mathematical convention in its handling of empty ranges,
  and fix a bug where by [1,2) and [2,3) were considered
  to intersect. (Only abutting empty ranges intersect by
  our definition.)
- Upgrade a marker test from @diag to @suggestedfixerr,
  now that the latter exists.

Change-Id: I010b2d4730596cac6f376c631839bfda159bf433
Reviewed-on: https://go-review.googlesource.com/c/tools/+/617035
Auto-Submit: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
Reviewed-by: Robert Findley 
---
 gopls/doc/analyzers.md                        | 36 --------
 gopls/doc/features/diagnostics.md             | 46 ++++++++++
 gopls/doc/features/transformation.md          |  3 -
 gopls/internal/analysis/stubmethods/doc.go    | 38 --------
 .../analysis/stubmethods/stubmethods.go       | 91 +------------------
 .../analysis/stubmethods/stubmethods_test.go  | 17 ----
 .../testdata/src/typeparams/implement.go      | 15 ---
 gopls/internal/doc/api.json                   | 11 ---
 gopls/internal/golang/change_quote.go         |  2 +-
 gopls/internal/golang/codeaction.go           | 79 +++++++++++-----
 gopls/internal/golang/fix.go                  |  4 +-
 gopls/internal/golang/stub.go                 |  1 +
 gopls/internal/protocol/span.go               | 35 ++++++-
 gopls/internal/settings/analysis.go           |  2 -
 .../test/integration/misc/codeactions_test.go | 10 ++
 gopls/internal/test/marker/doc.go             |  6 +-
 gopls/internal/test/marker/marker_test.go     |  6 +-
 .../marker/testdata/suggestedfix/stub.txt     |  5 +-
 18 files changed, 158 insertions(+), 249 deletions(-)
 delete mode 100644 gopls/internal/analysis/stubmethods/doc.go
 delete mode 100644 gopls/internal/analysis/stubmethods/stubmethods_test.go
 delete mode 100644 gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go

diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md
index f78f1bdf732..ec2b6316374 100644
--- a/gopls/doc/analyzers.md
+++ b/gopls/doc/analyzers.md
@@ -796,42 +796,6 @@ Default: on.
 
 Package documentation: [structtag](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/structtag)
 
-
-## `stubmethods`: detect missing methods and fix with stub implementations
-
-
-This analyzer detects type-checking errors due to missing methods
-in assignments from concrete types to interface types, and offers
-a suggested fix that will create a set of stub methods so that
-the concrete type satisfies the interface.
-
-For example, this function will not compile because the value
-NegativeErr{} does not implement the "error" interface:
-
-	func sqrt(x float64) (float64, error) {
-		if x < 0 {
-			return 0, NegativeErr{} // error: missing method
-		}
-		...
-	}
-
-	type NegativeErr struct{}
-
-This analyzer will suggest a fix to declare this method:
-
-	// Error implements error.Error.
-	func (NegativeErr) Error() string {
-		panic("unimplemented")
-	}
-
-(At least, it appears to behave that way, but technically it
-doesn't use the SuggestedFix mechanism and the stub is created by
-logic in gopls's golang.stub function.)
-
-Default: on.
-
-Package documentation: [stubmethods](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods)
-
 
 ## `testinggoroutine`: report calls to (*testing.T).Fatal from goroutines started by a test
 
diff --git a/gopls/doc/features/diagnostics.md b/gopls/doc/features/diagnostics.md
index f58a6465d1d..b667f69a080 100644
--- a/gopls/doc/features/diagnostics.md
+++ b/gopls/doc/features/diagnostics.md
@@ -119,6 +119,52 @@ Client support:
 - **Vim + coc.nvim**: ??
 - **CLI**: `gopls check file.go`
 
+
+
+
+### `stubMethods`: Declare missing methods of type
+
+When a value of a concrete type is assigned to a variable of an
+interface type, but the concrete type does not possess all the
+necessary methods, the type checker will report a "missing method"
+error.
+
+In this situation, gopls offers a quick fix to add stub declarations
+of all the missing methods to the concrete type so that it implements
+the interface.
+
+For example, this function will not compile because the value
+`NegativeErr{}` does not implement the "error" interface:
+
+```go
+func sqrt(x float64) (float64, error) {
+	if x < 0 {
+		return 0, NegativeErr{} // error: missing method
+	}
+	...
+}
+
+type NegativeErr struct{}
+```
+
+Gopls will offer a quick fix to declare this method:
+
+```go
+
+// Error implements error.Error.
+func (NegativeErr) Error() string {
+	panic("unimplemented")
+}
+```
+
+Beware that the new declarations appear alongside the concrete type,
+which may be in a different file or even package from the cursor
+position.
+(Perhaps gopls should send a `showDocument` request to navigate the
+client there, or a progress notification indicating that something
+happened.)
+
 
-
 - **`refactor.extract.function`** replaces one or more complete statements by a
   call to a new function named `newFunction` whose body contains the
   statements. The selection must enclose fewer statements than the
diff --git a/gopls/internal/analysis/stubmethods/doc.go b/gopls/internal/analysis/stubmethods/doc.go
deleted file mode 100644
index e1383cfc7e7..00000000000
--- a/gopls/internal/analysis/stubmethods/doc.go
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright 2023 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 stubmethods defines a code action for missing interface methods.
-//
-// # Analyzer stubmethods
-//
-// stubmethods: detect missing methods and fix with stub implementations
-//
-// This analyzer detects type-checking errors due to missing methods
-// in assignments from concrete types to interface types, and offers
-// a suggested fix that will create a set of stub methods so that
-// the concrete type satisfies the interface.
-//
-// For example, this function will not compile because the value
-// NegativeErr{} does not implement the "error" interface:
-//
-//	func sqrt(x float64) (float64, error) {
-//		if x < 0 {
-//			return 0, NegativeErr{} // error: missing method
-//		}
-//		...
-//	}
-//
-//	type NegativeErr struct{}
-//
-// This analyzer will suggest a fix to declare this method:
-//
-//	// Error implements error.Error.
-//	func (NegativeErr) Error() string {
-//		panic("unimplemented")
-//	}
-//
-// (At least, it appears to behave that way, but technically it
-// doesn't use the SuggestedFix mechanism and the stub is created by
-// logic in gopls's golang.stub function.)
-package stubmethods
diff --git a/gopls/internal/analysis/stubmethods/stubmethods.go b/gopls/internal/analysis/stubmethods/stubmethods.go
index c79a50d51e7..bfb68a44753 100644
--- a/gopls/internal/analysis/stubmethods/stubmethods.go
+++ b/gopls/internal/analysis/stubmethods/stubmethods.go
@@ -4,101 +4,16 @@
 
 package stubmethods
 
+// TODO(adonovan): rename package to golang/stubmethods or move
+// functions to golang/stub.go.
+
 import (
-	"bytes"
-	_ "embed"
 	"fmt"
 	"go/ast"
-	"go/format"
 	"go/token"
 	"go/types"
-	"strings"
-
-	"golang.org/x/tools/go/analysis"
-	"golang.org/x/tools/go/ast/astutil"
-	"golang.org/x/tools/gopls/internal/util/typesutil"
-	"golang.org/x/tools/internal/analysisinternal"
-	"golang.org/x/tools/internal/typesinternal"
 )
 
-//go:embed doc.go
-var doc string
-
-var Analyzer = &analysis.Analyzer{
-	Name:             "stubmethods",
-	Doc:              analysisinternal.MustExtractDoc(doc, "stubmethods"),
-	Run:              run,
-	RunDespiteErrors: true,
-	URL:              "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods",
-}
-
-// TODO(rfindley): remove this thin wrapper around the stubmethods refactoring,
-// and eliminate the stubmethods analyzer.
-//
-// Previous iterations used the analysis framework for computing refactorings,
-// which proved inefficient.
-func run(pass *analysis.Pass) (interface{}, error) {
-	for _, err := range pass.TypeErrors {
-		var file *ast.File
-		for _, f := range pass.Files {
-			if f.Pos() <= err.Pos && err.Pos < f.End() {
-				file = f
-				break
-			}
-		}
-		// Get the end position of the error.
-		_, _, end, ok := typesinternal.ReadGo116ErrorData(err)
-		if !ok {
-			var buf bytes.Buffer
-			if err := format.Node(&buf, pass.Fset, file); err != nil {
-				continue
-			}
-			end = analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos)
-		}
-		if diag, ok := DiagnosticForError(pass.Fset, file, err.Pos, end, err.Msg, pass.TypesInfo); ok {
-			pass.Report(diag)
-		}
-	}
-
-	return nil, nil
-}
-
-// MatchesMessage reports whether msg matches the error message sought after by
-// the stubmethods fix.
-func MatchesMessage(msg string) bool {
-	return strings.Contains(msg, "missing method") || strings.HasPrefix(msg, "cannot convert") || strings.Contains(msg, "not implement")
-}
-
-// DiagnosticForError computes a diagnostic suggesting to implement an
-// interface to fix the type checking error defined by (start, end, msg).
-//
-// If no such fix is possible, the second result is false.
-func DiagnosticForError(fset *token.FileSet, file *ast.File, start, end token.Pos, msg string, info *types.Info) (analysis.Diagnostic, bool) {
-	if !MatchesMessage(msg) {
-		return analysis.Diagnostic{}, false
-	}
-
-	path, _ := astutil.PathEnclosingInterval(file, start, end)
-	si := GetStubInfo(fset, info, path, start)
-	if si == nil {
-		return analysis.Diagnostic{}, false
-	}
-	qf := typesutil.FileQualifier(file, si.Concrete.Obj().Pkg(), info)
-	iface := types.TypeString(si.Interface.Type(), qf)
-	return analysis.Diagnostic{
-		Pos:      start,
-		End:      end,
-		Message:  msg,
-		Category: FixCategory,
-		SuggestedFixes: []analysis.SuggestedFix{{
-			Message: fmt.Sprintf("Declare missing methods of %s", iface),
-			// No TextEdits => computed later by gopls.
-		}},
-	}, true
-}
-
-const FixCategory = "stubmethods" // recognized by gopls ApplyFix
-
 // StubInfo represents a concrete type
 // that wants to stub out an interface type
 type StubInfo struct {
diff --git a/gopls/internal/analysis/stubmethods/stubmethods_test.go b/gopls/internal/analysis/stubmethods/stubmethods_test.go
deleted file mode 100644
index 9c744c9b7a3..00000000000
--- a/gopls/internal/analysis/stubmethods/stubmethods_test.go
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright 2023 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 stubmethods_test
-
-import (
-	"testing"
-
-	"golang.org/x/tools/go/analysis/analysistest"
-	"golang.org/x/tools/gopls/internal/analysis/stubmethods"
-)
-
-func Test(t *testing.T) {
-	testdata := analysistest.TestData()
-	analysistest.Run(t, testdata, stubmethods.Analyzer, "typeparams")
-}
diff --git a/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go b/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go
deleted file mode 100644
index 7b6f2911ea9..00000000000
--- a/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright 2023 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 stubmethods
-
-var _ I = Y{} // want "does not implement I"
-
-type I interface{ F() }
-
-type X struct{}
-
-func (X) F(string) {}
-
-type Y struct{ X }
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json
index d294ea0197d..b076abd26b0 100644
--- a/gopls/internal/doc/api.json
+++ b/gopls/internal/doc/api.json
@@ -567,11 +567,6 @@
 							"Doc": "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.",
 							"Default": "true"
 						},
-						{
-							"Name": "\"stubmethods\"",
-							"Doc": "detect missing methods and fix with stub implementations\n\nThis analyzer detects type-checking errors due to missing methods\nin assignments from concrete types to interface types, and offers\na suggested fix that will create a set of stub methods so that\nthe concrete type satisfies the interface.\n\nFor example, this function will not compile because the value\nNegativeErr{} does not implement the \"error\" interface:\n\n\tfunc sqrt(x float64) (float64, error) {\n\t\tif x \u003c 0 {\n\t\t\treturn 0, NegativeErr{} // error: missing method\n\t\t}\n\t\t...\n\t}\n\n\ttype NegativeErr struct{}\n\nThis analyzer will suggest a fix to declare this method:\n\n\t// Error implements error.Error.\n\tfunc (NegativeErr) Error() string {\n\t\tpanic(\"unimplemented\")\n\t}\n\n(At least, it appears to behave that way, but technically it\ndoesn't use the SuggestedFix mechanism and the stub is created by\nlogic in gopls's golang.stub function.)",
-							"Default": "true"
-						},
 						{
 							"Name": "\"testinggoroutine\"",
 							"Doc": "report calls to (*testing.T).Fatal from goroutines started by a test\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t    go func() {\n\t        t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t    }()\n\t}",
@@ -1238,12 +1233,6 @@
 			"URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/structtag",
 			"Default": true
 		},
-		{
-			"Name": "stubmethods",
-			"Doc": "detect missing methods and fix with stub implementations\n\nThis analyzer detects type-checking errors due to missing methods\nin assignments from concrete types to interface types, and offers\na suggested fix that will create a set of stub methods so that\nthe concrete type satisfies the interface.\n\nFor example, this function will not compile because the value\nNegativeErr{} does not implement the \"error\" interface:\n\n\tfunc sqrt(x float64) (float64, error) {\n\t\tif x \u003c 0 {\n\t\t\treturn 0, NegativeErr{} // error: missing method\n\t\t}\n\t\t...\n\t}\n\n\ttype NegativeErr struct{}\n\nThis analyzer will suggest a fix to declare this method:\n\n\t// Error implements error.Error.\n\tfunc (NegativeErr) Error() string {\n\t\tpanic(\"unimplemented\")\n\t}\n\n(At least, it appears to behave that way, but technically it\ndoesn't use the SuggestedFix mechanism and the stub is created by\nlogic in gopls's golang.stub function.)",
-			"URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods",
-			"Default": true
-		},
 		{
 			"Name": "testinggoroutine",
 			"Doc": "report calls to (*testing.T).Fatal from goroutines started by a test\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t    go func() {\n\t        t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t    }()\n\t}",
diff --git a/gopls/internal/golang/change_quote.go b/gopls/internal/golang/change_quote.go
index e793ec72c89..67f29430700 100644
--- a/gopls/internal/golang/change_quote.go
+++ b/gopls/internal/golang/change_quote.go
@@ -68,5 +68,5 @@ func convertStringLiteral(req *codeActionsRequest) {
 		bug.Reportf("failed to convert diff.Edit to protocol.TextEdit:%v", err)
 		return
 	}
-	req.addEditAction(title, protocol.DocumentChangeEdit(req.fh, textedits))
+	req.addEditAction(title, nil, protocol.DocumentChangeEdit(req.fh, textedits))
 }
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index f82abc6ce0d..b68b317b6db 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -19,6 +19,7 @@ import (
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
 	"golang.org/x/tools/gopls/internal/analysis/fillswitch"
+	"golang.org/x/tools/gopls/internal/analysis/stubmethods"
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/cache/parsego"
 	"golang.org/x/tools/gopls/internal/file"
@@ -26,6 +27,7 @@ import (
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/protocol/command"
 	"golang.org/x/tools/gopls/internal/settings"
+	"golang.org/x/tools/gopls/internal/util/typesutil"
 	"golang.org/x/tools/internal/event"
 	"golang.org/x/tools/internal/imports"
 	"golang.org/x/tools/internal/typesinternal"
@@ -135,13 +137,13 @@ type codeActionsRequest struct {
 }
 
 // addApplyFixAction adds an ApplyFix command-based CodeAction to the result.
-func (req *codeActionsRequest) addApplyFixAction(title, fix string, loc protocol.Location) *protocol.CodeAction {
+func (req *codeActionsRequest) addApplyFixAction(title, fix string, loc protocol.Location) {
 	cmd := command.NewApplyFixCommand(title, command.ApplyFixArgs{
 		Fix:          fix,
 		Location:     loc,
 		ResolveEdits: req.resolveEdits(),
 	})
-	return req.addCommandAction(cmd, true)
+	req.addCommandAction(cmd, true)
 }
 
 // addCommandAction adds a CodeAction to the result based on the provided command.
@@ -153,7 +155,7 @@ func (req *codeActionsRequest) addApplyFixAction(title, fix string, loc protocol
 // Set allowResolveEdits only for actions that generate edits.
 //
 // Otherwise, the command is set as the code action operation.
-func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command, allowResolveEdits bool) *protocol.CodeAction {
+func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command, allowResolveEdits bool) {
 	act := protocol.CodeAction{
 		Title: cmd.Title,
 		Kind:  req.kind,
@@ -168,24 +170,22 @@ func (req *codeActionsRequest) addCommandAction(cmd *protocol.Command, allowReso
 	} else {
 		act.Command = cmd
 	}
-	return req.addAction(act)
+	req.addAction(act)
 }
 
 // addCommandAction adds an edit-based CodeAction to the result.
-func (req *codeActionsRequest) addEditAction(title string, changes ...protocol.DocumentChange) *protocol.CodeAction {
-	return req.addAction(protocol.CodeAction{
-		Title: title,
-		Kind:  req.kind,
-		Edit:  protocol.NewWorkspaceEdit(changes...),
+func (req *codeActionsRequest) addEditAction(title string, fixedDiagnostics []protocol.Diagnostic, changes ...protocol.DocumentChange) {
+	req.addAction(protocol.CodeAction{
+		Title:       title,
+		Kind:        req.kind,
+		Diagnostics: fixedDiagnostics,
+		Edit:        protocol.NewWorkspaceEdit(changes...),
 	})
 }
 
 // addAction adds a code action to the response.
-// It returns an ephemeral pointer to the action in situ.
-// which may be used (but only immediately) for further mutation.
-func (req *codeActionsRequest) addAction(act protocol.CodeAction) *protocol.CodeAction {
+func (req *codeActionsRequest) addAction(act protocol.CodeAction) {
 	*req.actions = append(*req.actions, act)
-	return &(*req.actions)[len(*req.actions)-1]
 }
 
 // resolveEdits reports whether the client can resolve edits lazily.
@@ -225,7 +225,7 @@ type codeActionProducer struct {
 }
 
 var codeActionProducers = [...]codeActionProducer{
-	{kind: protocol.QuickFix, fn: quickFix},
+	{kind: protocol.QuickFix, fn: quickFix, needPkg: true},
 	{kind: protocol.SourceOrganizeImports, fn: sourceOrganizeImports},
 	{kind: settings.GoAssembly, fn: goAssembly, needPkg: true},
 	{kind: settings.GoDoc, fn: goDoc, needPkg: true},
@@ -257,13 +257,15 @@ func sourceOrganizeImports(ctx context.Context, req *codeActionsRequest) error {
 	// Send all of the import edits as one code action
 	// if the file is being organized.
 	if len(res.allFixEdits) > 0 {
-		req.addEditAction("Organize Imports", protocol.DocumentChangeEdit(req.fh, res.allFixEdits))
+		req.addEditAction("Organize Imports", nil, protocol.DocumentChangeEdit(req.fh, res.allFixEdits))
 	}
 
 	return nil
 }
 
-// quickFix produces code actions that add/delete/rename imports to fix type errors.
+// quickFix produces code actions that fix errors,
+// for example by adding/deleting/renaming imports,
+// or declaring the missing methods of a type.
 func quickFix(ctx context.Context, req *codeActionsRequest) error {
 	// Only compute quick fixes if there are any diagnostics to fix.
 	if len(req.diagnostics) == 0 {
@@ -279,14 +281,42 @@ func quickFix(ctx context.Context, req *codeActionsRequest) error {
 	// Separate this into a set of codeActions per diagnostic, where
 	// each action is the addition, removal, or renaming of one import.
 	for _, importFix := range res.editsPerFix {
-		fixed := fixedByImportFix(importFix.fix, req.diagnostics)
-		if len(fixed) == 0 {
+		fixedDiags := fixedByImportFix(importFix.fix, req.diagnostics)
+		if len(fixedDiags) == 0 {
 			continue
 		}
-		act := req.addEditAction(
-			importFixTitle(importFix.fix),
-			protocol.DocumentChangeEdit(req.fh, importFix.edits))
-		act.Diagnostics = fixed
+		req.addEditAction(importFixTitle(importFix.fix), fixedDiags, protocol.DocumentChangeEdit(req.fh, importFix.edits))
+	}
+
+	// Quick fixes for type errors.
+	info := req.pkg.TypesInfo()
+	for _, typeError := range req.pkg.TypeErrors() {
+		// Does type error overlap with CodeAction range?
+		start, end := typeError.Pos, typeError.Pos
+		if _, _, endPos, ok := typesinternal.ReadGo116ErrorData(typeError); ok {
+			end = endPos
+		}
+		typeErrorRange, err := req.pgf.PosRange(start, end)
+		if err != nil || !protocol.Intersect(typeErrorRange, req.loc.Range) {
+			continue
+		}
+
+		// "Missing method" error? (stubmethods)
+		// Offer a "Declare missing methods of INTERFACE" code action.
+		// See [stubMethodsFixer] for command implementation.
+		msg := typeError.Error()
+		if strings.Contains(msg, "missing method") ||
+			strings.HasPrefix(msg, "cannot convert") ||
+			strings.Contains(msg, "not implement") {
+			path, _ := astutil.PathEnclosingInterval(req.pgf.File, start, end)
+			si := stubmethods.GetStubInfo(req.pkg.FileSet(), info, path, start)
+			if si != nil {
+				qf := typesutil.FileQualifier(req.pgf.File, si.Concrete.Obj().Pkg(), info)
+				iface := types.TypeString(si.Interface.Type(), qf)
+				msg := fmt.Sprintf("Declare missing methods of %s", iface)
+				req.addApplyFixAction(msg, fixStubMethods, req.loc)
+			}
+		}
 	}
 
 	return nil
@@ -473,7 +503,7 @@ func refactorRewriteJoinLines(ctx context.Context, req *codeActionsRequest) erro
 	return nil
 }
 
-// refactorRewriteFillStruct produces "Join ITEMS into one line" code actions.
+// refactorRewriteFillStruct produces "Fill STRUCT" code actions.
 // See [fillstruct.SuggestedFix] for command implementation.
 func refactorRewriteFillStruct(ctx context.Context, req *codeActionsRequest) error {
 	// fillstruct.Diagnose is a lazy analyzer: all it gives us is
@@ -498,7 +528,7 @@ func refactorRewriteFillSwitch(ctx context.Context, req *codeActionsRequest) err
 		if err != nil {
 			return err
 		}
-		req.addEditAction(diag.Message, changes...)
+		req.addEditAction(diag.Message, nil, changes...)
 	}
 
 	return nil
@@ -560,6 +590,7 @@ func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Rang
 func refactorInlineCall(ctx context.Context, req *codeActionsRequest) error {
 	// To avoid distraction (e.g. VS Code lightbulb), offer "inline"
 	// only after a selection or explicit menu operation.
+	// TODO(adonovan): remove this (and req.trigger); see comment at TestVSCodeIssue65167.
 	if req.trigger == protocol.CodeActionAutomatic && req.loc.Empty() {
 		return nil
 	}
diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go
index 3844fc0d65c..7c44aa4d273 100644
--- a/gopls/internal/golang/fix.go
+++ b/gopls/internal/golang/fix.go
@@ -14,7 +14,6 @@ import (
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/gopls/internal/analysis/embeddirective"
 	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
-	"golang.org/x/tools/gopls/internal/analysis/stubmethods"
 	"golang.org/x/tools/gopls/internal/analysis/undeclaredname"
 	"golang.org/x/tools/gopls/internal/analysis/unusedparams"
 	"golang.org/x/tools/gopls/internal/cache"
@@ -66,6 +65,7 @@ const (
 	fixInvertIfCondition = "invert_if_condition"
 	fixSplitLines        = "split_lines"
 	fixJoinLines         = "join_lines"
+	fixStubMethods       = "stub_methods"
 )
 
 // ApplyFix applies the specified kind of suggested fix to the given
@@ -98,7 +98,6 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
 		// These match the Diagnostic.Category.
 		embeddirective.FixCategory: addEmbedImport,
 		fillstruct.FixCategory:     singleFile(fillstruct.SuggestedFix),
-		stubmethods.FixCategory:    stubMethodsFixer,
 		undeclaredname.FixCategory: singleFile(undeclaredname.SuggestedFix),
 
 		// Ad-hoc fixers: these are used when the command is
@@ -110,6 +109,7 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file
 		fixInvertIfCondition: singleFile(invertIfCondition),
 		fixSplitLines:        singleFile(splitLines),
 		fixJoinLines:         singleFile(joinLines),
+		fixStubMethods:       stubMethodsFixer,
 	}
 	fixer, ok := fixers[fix]
 	if !ok {
diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go
index 84299171fe4..c8a47b609c4 100644
--- a/gopls/internal/golang/stub.go
+++ b/gopls/internal/golang/stub.go
@@ -40,6 +40,7 @@ func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.
 
 	// A function-local type cannot be stubbed
 	// since there's nowhere to put the methods.
+	// TODO(adonovan): move this check into GetStubInfo instead of offering a bad fix.
 	conc := si.Concrete.Obj()
 	if conc.Parent() != conc.Pkg().Scope() {
 		return nil, nil, fmt.Errorf("local type %q cannot be stubbed", conc.Name())
diff --git a/gopls/internal/protocol/span.go b/gopls/internal/protocol/span.go
index 213251b1adb..2911d4aa29b 100644
--- a/gopls/internal/protocol/span.go
+++ b/gopls/internal/protocol/span.go
@@ -58,12 +58,37 @@ func ComparePosition(a, b Position) int {
 	return 0
 }
 
-func Intersect(a, b Range) bool {
-	if a.Start.Line > b.End.Line || a.End.Line < b.Start.Line {
-		return false
+// Intersect reports whether x and y intersect.
+//
+// Two non-empty half-open integer intervals intersect iff:
+//
+//	y.start < x.end && x.start < y.end
+//
+// Mathematical conventional views an interval as a set of integers.
+// An empty interval is the empty set, so its intersection with any
+// other interval is empty, and thus an empty interval does not
+// intersect any other interval.
+//
+// However, this function uses a looser definition appropriate for
+// text selections: if either x or y is empty, it uses <= operators
+// instead, so an empty range within or abutting a non-empty range is
+// considered to overlap it, and an empty range overlaps itself.
+//
+// This handles the common case in which there is no selection, but
+// the cursor is at the start or end of an expression and the caller
+// wants to know whether the cursor intersects the range of the
+// expression. The answer in this case should be yes, even though the
+// selection is empty. Similarly the answer should also be yes if the
+// cursor is properly within the range of the expression. But a
+// non-empty selection abutting the expression should not be
+// considered to intersect it.
+func Intersect(x, y Range) bool {
+	r1 := ComparePosition(x.Start, y.End)
+	r2 := ComparePosition(y.Start, x.End)
+	if r1 < 0 && r2 < 0 {
+		return true // mathematical intersection
 	}
-	return !((a.Start.Line == b.End.Line) && a.Start.Character > b.End.Character ||
-		(a.End.Line == b.Start.Line) && a.End.Character < b.Start.Character)
+	return (x.Empty() || y.Empty()) && r1 <= 0 && r2 <= 0
 }
 
 // Format implements fmt.Formatter.
diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go
index 65ecb215c02..86fa4766b51 100644
--- a/gopls/internal/settings/analysis.go
+++ b/gopls/internal/settings/analysis.go
@@ -55,7 +55,6 @@ import (
 	"golang.org/x/tools/gopls/internal/analysis/simplifycompositelit"
 	"golang.org/x/tools/gopls/internal/analysis/simplifyrange"
 	"golang.org/x/tools/gopls/internal/analysis/simplifyslice"
-	"golang.org/x/tools/gopls/internal/analysis/stubmethods"
 	"golang.org/x/tools/gopls/internal/analysis/undeclaredname"
 	"golang.org/x/tools/gopls/internal/analysis/unusedparams"
 	"golang.org/x/tools/gopls/internal/analysis/unusedvariable"
@@ -202,7 +201,6 @@ func init() {
 		{analyzer: fillreturns.Analyzer, enabled: true},
 		{analyzer: nonewvars.Analyzer, enabled: true},
 		{analyzer: noresultvalues.Analyzer, enabled: true},
-		{analyzer: stubmethods.Analyzer, enabled: true},
 		{analyzer: undeclaredname.Analyzer, enabled: true},
 		// TODO(rfindley): why isn't the 'unusedvariable' analyzer enabled, if it
 		// is only enhancing type errors with suggested fixes?
diff --git a/gopls/internal/test/integration/misc/codeactions_test.go b/gopls/internal/test/integration/misc/codeactions_test.go
index 354921afc01..7e5ac9aba62 100644
--- a/gopls/internal/test/integration/misc/codeactions_test.go
+++ b/gopls/internal/test/integration/misc/codeactions_test.go
@@ -80,6 +80,16 @@ func g() {}
 
 // Test refactor.inline.call is not included in automatically triggered code action
 // unless users want refactoring.
+//
+// (The mechanism behind this behavior has changed. It was added when
+// we used to interpret CodeAction(Only=[]) as "all kinds", which was
+// a distracting nuisance (too many lightbulbs); this was fixed by
+// adding special logic to refactor.inline.call to respect the trigger
+// kind; but now we do this for all actions (for similar reasons) and
+// interpret Only=[] as Only=[quickfix] unless triggerKind=invoked;
+// except that the test client always requests CodeAction(Only=[""]).
+// So, we should remove the special logic from refactorInlineCall
+// and vary the Only parameter used by the test client.)
 func TestVSCodeIssue65167(t *testing.T) {
 	const vim1 = `package main
 
diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go
index 5bcb31b46de..bd23a4f12ef 100644
--- a/gopls/internal/test/marker/doc.go
+++ b/gopls/internal/test/marker/doc.go
@@ -220,12 +220,12 @@ The following markers are supported within marker tests:
     the active parameter (an index) highlighted.
 
   - suggestedfix(location, regexp, golden): like diag, the location and
-    regexp identify an expected diagnostic. This diagnostic must
-    to have exactly one associated code action of the specified kind.
+    regexp identify an expected diagnostic, which must have exactly one
+    associated "quickfix" code action.
     This action is executed for its editing effects on the source files.
     Like rename, the golden directory contains the expected transformed files.
 
-  - suggestedfixerr(location, regexp, kind, wantError): specifies that the
+  - suggestedfixerr(location, regexp, wantError): specifies that the
     suggestedfix operation should fail with an error that matches the expectation.
     (Failures in the computation to offer a fix do not generally result
     in LSP errors, so this marker is not appropriate for testing them.)
diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go
index a128bcb7f18..1478fe631c7 100644
--- a/gopls/internal/test/marker/marker_test.go
+++ b/gopls/internal/test/marker/marker_test.go
@@ -2028,8 +2028,10 @@ func (mark marker) consumeExtraNotes(name string, f func(marker)) {
 
 // suggestedfixMarker implements the @suggestedfix(location, regexp,
 // kind, golden) marker. It acts like @diag(location, regexp), to set
-// the expectation of a diagnostic, but then it applies the first code
-// action of the specified kind suggested by the matched diagnostic.
+// the expectation of a diagnostic, but then it applies the "quickfix"
+// code action (which must be unique) suggested by the matched diagnostic.
+//
+// TODO(adonovan): rename to @quickfix, since that's the LSP term.
 func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) {
 	loc.Range.End = loc.Range.Start // diagnostics ignore end position.
 	// Find and remove the matching diagnostic.
diff --git a/gopls/internal/test/marker/testdata/suggestedfix/stub.txt b/gopls/internal/test/marker/testdata/suggestedfix/stub.txt
index e31494ae461..fc10d8e58ad 100644
--- a/gopls/internal/test/marker/testdata/suggestedfix/stub.txt
+++ b/gopls/internal/test/marker/testdata/suggestedfix/stub.txt
@@ -347,9 +347,10 @@ type (
 
 func _() {
 	// Local types can't be stubbed as there's nowhere to put the methods.
-	// The suggestedfix assertion can't express this yet. TODO(adonovan): support it.
+	// Check that executing the code action causes an error, not file corruption.
+	// TODO(adonovan): it would be better not to offer the quick fix in this case.
 	type local struct{}
-	var _ io.ReadCloser = local{} //@diag("local", re"does not implement")
+	var _ io.ReadCloser = local{} //@suggestedfixerr("local", re"does not implement", "local type \"local\" cannot be stubbed")
 }
 -- @typedecl_group/typedecl_group.go --
 @@ -18 +18,10 @@

From 2683c792b49bd10d92da20a689ef2d5ddbf73460 Mon Sep 17 00:00:00 2001
From: Alan Donovan 
Date: Tue, 1 Oct 2024 15:24:23 -0400
Subject: [PATCH 101/102] gopls/internal/golang/stubmethods: rename
 analysis/stubmethods

Change-Id: I378afe9f961e6a6b3363f464cde198f2ca0bb525
Reviewed-on: https://go-review.googlesource.com/c/tools/+/617255
Reviewed-by: Robert Findley 
Auto-Submit: Alan Donovan 
LUCI-TryBot-Result: Go LUCI 
---
 gopls/internal/golang/codeaction.go                      | 2 +-
 gopls/internal/golang/hover.go                           | 4 ++++
 gopls/internal/golang/stub.go                            | 2 +-
 .../{analysis => golang}/stubmethods/stubmethods.go      | 9 ++++++---
 4 files changed, 12 insertions(+), 5 deletions(-)
 rename gopls/internal/{analysis => golang}/stubmethods/stubmethods.go (97%)

diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index b68b317b6db..3c916628a1a 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -19,10 +19,10 @@ import (
 	"golang.org/x/tools/go/ast/astutil"
 	"golang.org/x/tools/gopls/internal/analysis/fillstruct"
 	"golang.org/x/tools/gopls/internal/analysis/fillswitch"
-	"golang.org/x/tools/gopls/internal/analysis/stubmethods"
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/cache/parsego"
 	"golang.org/x/tools/gopls/internal/file"
+	"golang.org/x/tools/gopls/internal/golang/stubmethods"
 	"golang.org/x/tools/gopls/internal/label"
 	"golang.org/x/tools/gopls/internal/protocol"
 	"golang.org/x/tools/gopls/internal/protocol/command"
diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go
index 016222a7c76..a0622fd764e 100644
--- a/gopls/internal/golang/hover.go
+++ b/gopls/internal/golang/hover.go
@@ -1109,6 +1109,10 @@ func chooseDocComment(decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.Comme
 // pos; the resulting File and Pos may belong to the same or a
 // different FileSet, such as one synthesized by the parser cache, if
 // parse-caching is enabled.
+//
+// TODO(adonovan): change this function to accept a filename and a
+// byte offset, and eliminate the confusing (fset, pos) parameters.
+// Then simplify stubmethods.StubInfo, which doesn't need a Fset.
 func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, pos token.Pos) (*parsego.File, token.Pos, error) {
 	f := fset.File(pos)
 	if f == nil {
diff --git a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go
index c8a47b609c4..ca5f0055c3b 100644
--- a/gopls/internal/golang/stub.go
+++ b/gopls/internal/golang/stub.go
@@ -18,10 +18,10 @@ import (
 
 	"golang.org/x/tools/go/analysis"
 	"golang.org/x/tools/go/ast/astutil"
-	"golang.org/x/tools/gopls/internal/analysis/stubmethods"
 	"golang.org/x/tools/gopls/internal/cache"
 	"golang.org/x/tools/gopls/internal/cache/metadata"
 	"golang.org/x/tools/gopls/internal/cache/parsego"
+	"golang.org/x/tools/gopls/internal/golang/stubmethods"
 	"golang.org/x/tools/gopls/internal/util/bug"
 	"golang.org/x/tools/gopls/internal/util/safetoken"
 	"golang.org/x/tools/internal/diff"
diff --git a/gopls/internal/analysis/stubmethods/stubmethods.go b/gopls/internal/golang/stubmethods/stubmethods.go
similarity index 97%
rename from gopls/internal/analysis/stubmethods/stubmethods.go
rename to gopls/internal/golang/stubmethods/stubmethods.go
index bfb68a44753..ee7b525a6a0 100644
--- a/gopls/internal/analysis/stubmethods/stubmethods.go
+++ b/gopls/internal/golang/stubmethods/stubmethods.go
@@ -2,11 +2,11 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// Package stubmethods provides the analysis logic for the quick fix
+// to "Declare missing methods of TYPE" errors. (The fix logic lives
+// in golang.stubMethodsFixer.)
 package stubmethods
 
-// TODO(adonovan): rename package to golang/stubmethods or move
-// functions to golang/stub.go.
-
 import (
 	"fmt"
 	"go/ast"
@@ -14,6 +14,9 @@ import (
 	"go/types"
 )
 
+// TODO(adonovan): eliminate the confusing Fset parameter; only the
+// file name and byte offset of Concrete are needed.
+
 // StubInfo represents a concrete type
 // that wants to stub out an interface type
 type StubInfo struct {

From 2ab3b5143581f36ca417dd73637437ef1df628a8 Mon Sep 17 00:00:00 2001
From: Gopher Robot 
Date: Fri, 4 Oct 2024 16:49:41 +0000
Subject: [PATCH 102/102] go.mod: update golang.org/x dependencies

Update golang.org/x dependencies to their latest tagged versions.

Change-Id: If6f30243f94e503d7457bc6f595e02366c5b7ec4
Reviewed-on: https://go-review.googlesource.com/c/tools/+/617660
Reviewed-by: David Chase 
LUCI-TryBot-Result: Go LUCI 
Auto-Submit: Gopher Robot 
Reviewed-by: Dmitri Shuralyov 
---
 go.mod       |  4 ++--
 go.sum       |  8 ++++----
 gopls/go.mod |  4 ++--
 gopls/go.sum | 14 +++++++-------
 4 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/go.mod b/go.mod
index 003d83773df..9715167f426 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,9 @@ require (
 	github.com/google/go-cmp v0.6.0
 	github.com/yuin/goldmark v1.4.13
 	golang.org/x/mod v0.21.0
-	golang.org/x/net v0.29.0
+	golang.org/x/net v0.30.0
 	golang.org/x/sync v0.8.0
 	golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457
 )
 
-require golang.org/x/sys v0.25.0 // indirect
+require golang.org/x/sys v0.26.0 // indirect
diff --git a/go.sum b/go.sum
index eae2ee53cc7..459786d0b91 100644
--- a/go.sum
+++ b/go.sum
@@ -4,11 +4,11 @@ github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
 golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
-golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
-golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
diff --git a/gopls/go.mod b/gopls/go.mod
index c54d5e96cd2..a7bc7404a65 100644
--- a/gopls/go.mod
+++ b/gopls/go.mod
@@ -9,9 +9,9 @@ require (
 	github.com/jba/templatecheck v0.7.0
 	golang.org/x/mod v0.21.0
 	golang.org/x/sync v0.8.0
-	golang.org/x/sys v0.25.0
+	golang.org/x/sys v0.26.0
 	golang.org/x/telemetry v0.0.0-20240927184629-19675431963b
-	golang.org/x/text v0.18.0
+	golang.org/x/text v0.19.0
 	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
 	golang.org/x/vuln v1.0.4
 	gopkg.in/yaml.v3 v3.0.1
diff --git a/gopls/go.sum b/gopls/go.sum
index e546853db07..2b92ae83594 100644
--- a/gopls/go.sum
+++ b/gopls/go.sum
@@ -16,7 +16,7 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
 golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
 golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
@@ -25,7 +25,7 @@ golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
 golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@@ -33,19 +33,19 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
 golang.org/x/telemetry v0.0.0-20240927184629-19675431963b h1:PfPrmVDHfPgLVpiYnf2R1uL8SCXBjkqT51+f/fQHR6Q=
 golang.org/x/telemetry v0.0.0-20240927184629-19675431963b/go.mod h1:PsFMgI0jiuY7j+qwXANuh9a/x5kQESTSnRow3gapUyk=
 golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
 golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
-golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
+golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I=
 golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=