From afd84280124f1e3c3d100fd40838306752d28c40 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Mon, 12 Feb 2024 17:01:14 -0500 Subject: [PATCH 01/47] gopls/internal/test/integration: slightly more ergonomic FolderSettings After setting CL 563475 for autosubmit, I realized we should use a slightly more ergonomic pattern for configuring folder settings. Change-Id: I3a4e9e3dca3f4865c3bca02258cd81ff2b7024b3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/563402 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley --- .../test/integration/misc/configuration_test.go | 4 ++-- gopls/internal/test/integration/options.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gopls/internal/test/integration/misc/configuration_test.go b/gopls/internal/test/integration/misc/configuration_test.go index fff4576bc5b..c6ed18041ae 100644 --- a/gopls/internal/test/integration/misc/configuration_test.go +++ b/gopls/internal/test/integration/misc/configuration_test.go @@ -78,13 +78,13 @@ type B struct { WithOptions( WorkspaceFolders("a"), - FolderSettings(map[string]Settings{ + FolderSettings{ "a": { "analyses": map[string]bool{ "composites": false, }, }, - }), + }, ).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("a/a.go") env.AfterChange(NoDiagnostics()) diff --git a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go index a6c394e3467..f1d5425d807 100644 --- a/gopls/internal/test/integration/options.go +++ b/gopls/internal/test/integration/options.go @@ -115,16 +115,16 @@ func WorkspaceFolders(relFolders ...string) RunOption { // // Use in conjunction with WorkspaceFolders to have different settings for // different folders. -func FolderSettings(folderSettings map[string]Settings) RunOption { +type FolderSettings map[string]Settings + +func (fs FolderSettings) set(opts *runConfig) { // Re-use the Settings type, for symmetry, but translate back into maps for // the editor config. folders := make(map[string]map[string]any) - for k, v := range folderSettings { + for k, v := range fs { folders[k] = v } - return optionSetter(func(opts *runConfig) { - opts.editor.FolderSettings = folders - }) + opts.editor.FolderSettings = folders } // EnvVars sets environment variables for the LSP session. When applying these From 1b39a8b6a9ba38b140eb9b84196ddcb4e8f53ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Mart=C3=AD?= Date: Tue, 13 Feb 2024 13:41:59 +0000 Subject: [PATCH 02/47] internal/testenv: always Cleanup to appease go vet vet from Go 1.22 complains about cancelCtx being potentially leaked: exec.go:140:4: the cancelCtx function is not used on all paths (possible context leak) exec.go:192:2: this return statement may be reached without using the cancelCtx var defined on line 140 The code was fine with that, and in practice the leak does not happen, but having the vet warning around is still distracting. Thankfully, Go 1.14 is very old at this point, and per go.mod, this module now requires Go 1.18 or later, so we can clean this up. Change-Id: I4c402ad233e9b33a6d51c98ba0833c67fe01b209 Reviewed-on: https://go-review.googlesource.com/c/tools/+/563675 Reviewed-by: Than McIntosh Auto-Submit: Robert Findley Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- internal/testenv/exec.go | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/internal/testenv/exec.go b/internal/testenv/exec.go index 43aad5899fc..3b0cb6fcfa2 100644 --- a/internal/testenv/exec.go +++ b/internal/testenv/exec.go @@ -92,8 +92,7 @@ func NeedsExec(t testing.TB) { // for an arbitrary grace period before the test's deadline expires, // - if Cmd has the Cancel field, fails the test if the command is canceled // due to the test's deadline, and -// - if supported, sets a Cleanup function that verifies that the test did not -// leak a subprocess. +// - sets a Cleanup function that verifies that the test did not leak a subprocess. func CommandContext(t testing.TB, ctx context.Context, name string, args ...string) *exec.Cmd { t.Helper() NeedsExec(t) @@ -173,21 +172,14 @@ func CommandContext(t testing.TB, ctx context.Context, name string, args ...stri rWaitDelay.Set(reflect.ValueOf(gracePeriod)) } - // t.Cleanup was added in Go 1.14; for earlier Go versions, - // we just let the Context leak. - type Cleanupper interface { - Cleanup(func()) - } - if ct, ok := t.(Cleanupper); ok { - ct.Cleanup(func() { - if cancelCtx != nil { - cancelCtx() - } - if cmd.Process != nil && cmd.ProcessState == nil { - t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) - } - }) - } + t.Cleanup(func() { + if cancelCtx != nil { + cancelCtx() + } + if cmd.Process != nil && cmd.ProcessState == nil { + t.Errorf("command was started, but test did not wait for it to complete: %v", cmd) + } + }) return cmd } From e1a62614acd5cb989f00ad7615a31d0b29b15b05 Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Thu, 8 Feb 2024 11:38:17 +1030 Subject: [PATCH 03/47] internal/jsonrpc2_v2: export WireError type This makes it possible for users of the package to us the optional error data field in error construction and error handling logic. Updates golang/go#64882 Change-Id: Iaa554f42ff42a0b7355605ba0b49ac67f623da15 Reviewed-on: https://go-review.googlesource.com/c/tools/+/562575 Reviewed-by: Alan Donovan Reviewed-by: Than McIntosh LUCI-TryBot-Result: Go LUCI --- internal/jsonrpc2_v2/messages.go | 8 ++++---- internal/jsonrpc2_v2/wire.go | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/jsonrpc2_v2/messages.go b/internal/jsonrpc2_v2/messages.go index af145641d6a..f02b879c3f2 100644 --- a/internal/jsonrpc2_v2/messages.go +++ b/internal/jsonrpc2_v2/messages.go @@ -96,17 +96,17 @@ func (msg *Response) marshal(to *wireCombined) { to.Result = msg.Result } -func toWireError(err error) *wireError { +func toWireError(err error) *WireError { if err == nil { // no error, the response is complete return nil } - if err, ok := err.(*wireError); ok { + if err, ok := err.(*WireError); ok { // already a wire error, just use it return err } - result := &wireError{Message: err.Error()} - var wrapped *wireError + result := &WireError{Message: err.Error()} + var wrapped *WireError if errors.As(err, &wrapped) { // if we wrapped a wire error, keep the code from the wrapped error // but the message from the outer error diff --git a/internal/jsonrpc2_v2/wire.go b/internal/jsonrpc2_v2/wire.go index c8dc9ebf1bf..8f60fc62766 100644 --- a/internal/jsonrpc2_v2/wire.go +++ b/internal/jsonrpc2_v2/wire.go @@ -49,11 +49,11 @@ type wireCombined struct { Method string `json:"method,omitempty"` Params json.RawMessage `json:"params,omitempty"` Result json.RawMessage `json:"result,omitempty"` - Error *wireError `json:"error,omitempty"` + Error *WireError `json:"error,omitempty"` } -// wireError represents a structured error in a Response. -type wireError struct { +// WireError represents a structured error in a Response. +type WireError struct { // Code is an error code indicating the type of failure. Code int64 `json:"code"` // Message is a short description of the error. @@ -67,18 +67,18 @@ type wireError struct { // only be used to build errors for application specific codes as allowed by the // specification. func NewError(code int64, message string) error { - return &wireError{ + return &WireError{ Code: code, Message: message, } } -func (err *wireError) Error() string { +func (err *WireError) Error() string { return err.Message } -func (err *wireError) Is(other error) bool { - w, ok := other.(*wireError) +func (err *WireError) Is(other error) bool { + w, ok := other.(*WireError) if !ok { return false } From 945a754d4db23e0ce0682100d12dc8a0c8b529e7 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Tue, 25 Oct 2022 10:48:07 -0400 Subject: [PATCH 04/47] gopls/internal/golang: remove a use of panic for flow control FindDeclAndField in hover.go used panic() and recover() for flow control. This CL replaces that with tests for early successful completion. The new code is somewhat fiddlier. When running all the gopls tests, the old code returned 32,339 non-nil values. The new code returns exactly the same results in all these cases. The new code is generally faster than the old code. As they use wall-clock times, the timings are somewhat noisy, but the median and 99th percentiles were 1520ns, 12070ns for the new code, and 7870ns, 26500ns for the old. For 270 of the 32,339 calls, the old code was faster. The stack is preallocated to capacity 20. The 99th percentile of stack size is 80, which is only 2 reallocations. The large stacks appear to happend looking in the go source tree while processing deep completions (This change was written by pjw in https://go.dev/cl/445337 but it went stale.) Change-Id: If23c756d0d671b70ad6286d5e0487c78ed3eb277 Reviewed-on: https://go-review.googlesource.com/c/tools/+/563935 LUCI-TryBot-Result: Go LUCI Reviewed-by: Peter Weinberger --- gopls/internal/golang/hover.go | 37 ++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index ef486018786..7e613f77b7b 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -1096,22 +1096,16 @@ func formatDoc(h *hoverJSON, options *settings.Options) string { // TODO(rfindley): this function has tricky semantics, and may be worth unit // testing and/or refactoring. func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field) { - // panic(found{}) breaks off the traversal and - // causes the function to return normally. - type found struct{} - defer func() { - switch x := recover().(type) { - case nil: - case found: - default: - panic(x) - } - }() + found := false // Visit the files in search of the node at pos. stack := make([]ast.Node, 0, 20) + // Allocate the closure once, outside the loop. f := func(n ast.Node) bool { + if found { + return false + } if n != nil { stack = append(stack, n) // push } else { @@ -1144,7 +1138,8 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe if id.Pos() == pos { field = n findEnclosingDeclAndSpec() - panic(found{}) + found = true + return false } } @@ -1153,7 +1148,8 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe if n.Pos() == pos { field = n findEnclosingDeclAndSpec() - panic(found{}) + found = true + return false } // Also check "X" in "...X". This makes it easy to format variadic @@ -1164,13 +1160,15 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil && ell.Elt.Pos() == pos { field = n findEnclosingDeclAndSpec() - panic(found{}) + found = true + return false } case *ast.FuncDecl: if n.Name.Pos() == pos { decl = n - panic(found{}) + found = true + return false } case *ast.GenDecl: @@ -1180,14 +1178,16 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe if s.Name.Pos() == pos { decl = n spec = s - panic(found{}) + found = true + return false } case *ast.ValueSpec: for _, id := range s.Names { if id.Pos() == pos { decl = n spec = s - panic(found{}) + found = true + return false } } } @@ -1197,6 +1197,9 @@ func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spe } for _, file := range files { ast.Inspect(file, f) + if found { + return decl, spec, field + } } return nil, nil, nil From f1914ccb9b562e3fb47b38aa0153dc7774fe53a5 Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 13 Feb 2024 14:07:06 -0800 Subject: [PATCH 05/47] internal/aliases: use testing.T.Setenv in tests Change-Id: Ib6de3aee0348a3db37a75c63e118632384fa4891 Reviewed-on: https://go-review.googlesource.com/c/tools/+/563975 Run-TryBot: Tim King LUCI-TryBot-Result: Go LUCI TryBot-Result: Gopher Robot Reviewed-by: Robert Findley --- internal/aliases/aliases_test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go index fc4efb2cb27..d2d3464e19a 100644 --- a/internal/aliases/aliases_test.go +++ b/internal/aliases/aliases_test.go @@ -9,7 +9,6 @@ import ( "go/parser" "go/token" "go/types" - "os" "testing" "golang.org/x/tools/internal/aliases" @@ -49,9 +48,7 @@ func TestNewAlias(t *testing.T) { for _, godebug := range []string{"", "gotypesalias=1"} { t.Run(godebug, func(t *testing.T) { - saved := os.Getenv("GODEBUG") - defer os.Setenv("GODEBUG", saved) - os.Setenv("GODEBUG", godebug) // non parallel. + t.Setenv("GODEBUG", godebug) A := aliases.NewAlias(token.NoPos, pkg, "A", tv.Type) if got, want := A.Name(), "A"; got != want { From 0f0698e7aaac5049c0cb7974e7848df2287842b6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 12 Feb 2024 14:08:06 -0500 Subject: [PATCH 06/47] go/analysis/passes/nilness: improve "for range []T(nil)" error Previously, the error message referred to a "nil dereference in index operation", which is true, but the index operation is unreachable because the range loop over a nil slice is degenerate. Rephrase it to "index of nil slice", which doesn't promise that the operation will panic, or "range over nil slice" if we can infer that the IndexAddr operation came from range-over-slice lowering. Also, add cases for range over a nil map, *array, and chan, with tests. Fixes golang/go#65674 Change-Id: I1bf89bd1118b191a493bc2f230cb1388ff7a9b3b Reviewed-on: https://go-review.googlesource.com/c/tools/+/563476 Reviewed-by: Lasse Folger Reviewed-by: Tim King Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/nilness/nilness.go | 79 ++++++++++++++++--- .../passes/nilness/testdata/src/a/a.go | 63 ++++++++++++++- 2 files changed, 130 insertions(+), 12 deletions(-) diff --git a/go/analysis/passes/nilness/nilness.go b/go/analysis/passes/nilness/nilness.go index 5e14c096ab1..774f04c94a5 100644 --- a/go/analysis/passes/nilness/nilness.go +++ b/go/analysis/passes/nilness/nilness.go @@ -52,7 +52,7 @@ func runFunc(pass *analysis.Pass, fn *ssa.Function) { // notNil reports an error if v is provably nil. notNil := func(stack []fact, instr ssa.Instruction, v ssa.Value, descr string) { if nilnessOf(stack, v) == isnil { - reportf("nilderef", instr.Pos(), "nil dereference in "+descr) + reportf("nilderef", instr.Pos(), descr) } } @@ -77,29 +77,50 @@ func runFunc(pass *analysis.Pass, fn *ssa.Function) { // A nil receiver may be okay for type params. cc := instr.Common() if !(cc.IsInvoke() && typeparams.IsTypeParam(cc.Value.Type())) { - notNil(stack, instr, cc.Value, cc.Description()) + notNil(stack, instr, cc.Value, "nil dereference in "+cc.Description()) } case *ssa.FieldAddr: - notNil(stack, instr, instr.X, "field selection") + notNil(stack, instr, instr.X, "nil dereference in field selection") case *ssa.IndexAddr: - notNil(stack, instr, instr.X, "index operation") + switch typeparams.CoreType(instr.X.Type()).(type) { + case *types.Pointer: // *array + notNil(stack, instr, instr.X, "nil dereference in array index operation") + case *types.Slice: + // This is not necessarily a runtime error, because + // it is usually dominated by a bounds check. + if isRangeIndex(instr) { + notNil(stack, instr, instr.X, "range of nil slice") + } else { + notNil(stack, instr, instr.X, "index of nil slice") + } + } case *ssa.MapUpdate: - notNil(stack, instr, instr.Map, "map update") + notNil(stack, instr, instr.Map, "nil dereference in map update") + case *ssa.Range: + // (Not a runtime error, but a likely mistake.) + notNil(stack, instr, instr.X, "range over nil map") case *ssa.Slice: // A nilcheck occurs in ptr[:] iff ptr is a pointer to an array. - if _, ok := instr.X.Type().Underlying().(*types.Pointer); ok { - notNil(stack, instr, instr.X, "slice operation") + if is[*types.Pointer](instr.X.Type().Underlying()) { + notNil(stack, instr, instr.X, "nil dereference in slice operation") } case *ssa.Store: - notNil(stack, instr, instr.Addr, "store") + notNil(stack, instr, instr.Addr, "nil dereference in store") case *ssa.TypeAssert: if !instr.CommaOk { - notNil(stack, instr, instr.X, "type assertion") + notNil(stack, instr, instr.X, "nil dereference in type assertion") } case *ssa.UnOp: - if instr.Op == token.MUL { // *X - notNil(stack, instr, instr.X, "load") + switch instr.Op { + case token.MUL: // *X + notNil(stack, instr, instr.X, "nil dereference in load") + case token.ARROW: // <-ch + // (Not a runtime error, but a likely mistake.) + notNil(stack, instr, instr.X, "receive from nil channel") } + case *ssa.Send: + // (Not a runtime error, but a likely mistake.) + notNil(stack, instr, instr.Chan, "send to nil channel") } } @@ -416,3 +437,39 @@ func isNillable(t types.Type) bool { } return false } + +// isRangeIndex reports whether the instruction is a slice indexing +// operation slice[i] within a "for range slice" loop. The operation +// could be explicit, such as slice[i] within (or even after) the +// loop, or it could be implicit, such as "for i, v := range slice {}". +// (These cannot be reliably distinguished.) +func isRangeIndex(instr *ssa.IndexAddr) bool { + // Here we reverse-engineer the go/ssa lowering of range-over-slice: + // + // n = len(x) + // jump loop + // loop: "rangeindex.loop" + // phi = φ(-1, incr) #rangeindex + // incr = phi + 1 + // cond = incr < n + // if cond goto body else done + // body: "rangeindex.body" + // instr = &x[incr] + // ... + // done: + if incr, ok := instr.Index.(*ssa.BinOp); ok && incr.Op == token.ADD { + if b := incr.Block(); b.Comment == "rangeindex.loop" { + if If, ok := b.Instrs[len(b.Instrs)-1].(*ssa.If); ok { + if cond := If.Cond.(*ssa.BinOp); cond.X == incr && cond.Op == token.LSS { + if call, ok := cond.Y.(*ssa.Call); ok { + common := call.Common() + if blt, ok := common.Value.(*ssa.Builtin); ok && blt.Name() == "len" { + return common.Args[0] == instr.X + } + } + } + } + } + } + return false +} diff --git a/go/analysis/passes/nilness/testdata/src/a/a.go b/go/analysis/passes/nilness/testdata/src/a/a.go index 89bccbe65bd..73e1fd76f4d 100644 --- a/go/analysis/passes/nilness/testdata/src/a/a.go +++ b/go/analysis/passes/nilness/testdata/src/a/a.go @@ -54,7 +54,7 @@ func f2(ptr *[3]int, i interface{}) { } } -func g() error +func g() error { return nil } func f3() error { err := g() @@ -242,3 +242,64 @@ func f18(x any) { } println(*ptr) } + +// Regression test for https://github.com/golang/go/issues/65674: +// spurious "nil deference in slice index operation" when the +// index was subject to a range loop. +func f19(slice []int, array *[2]int, m map[string]int, ch chan int) { + if slice == nil { + // A range over a nil slice is dynamically benign, + // but still signifies a programmer mistake. + // + // Since SSA has melted down the control structure, + // so we can only report a diagnostic about the + // index operation, with heuristics for "range". + + for range slice { // nothing to report here + } + for _, v := range slice { // want "range of nil slice" + _ = v + } + for i := range slice { + _ = slice[i] // want "range of nil slice" + } + { + var i int + for i = range slice { + } + _ = slice[i] // want "index of nil slice" + } + for i := range slice { + if i < len(slice) { + _ = slice[i] // want "range of nil slice" + } + } + if len(slice) > 3 { + _ = slice[2] // want "index of nil slice" + } + for i := 0; i < len(slice); i++ { + _ = slice[i] // want "index of nil slice" + } + } + + if array == nil { + // (The v var is necessary, otherwise the SSA + // code doesn't dereference the pointer.) + for _, v := range array { // want "nil dereference in array index operation" + _ = v + } + } + + if m == nil { + for range m { // want "range over nil map" + } + m["one"] = 1 // want "nil dereference in map update" + } + + if ch == nil { + for range ch { // want "receive from nil channel" + } + <-ch // want "receive from nil channel" + ch <- 0 // want "send to nil channel" + } +} From 5de9cbe17be0b646da7ef290c861e70d0bf07cdd Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 14 Feb 2024 10:04:07 -0500 Subject: [PATCH 07/47] go/ssa: show instruction line numbers in -build=FS mode Change-Id: I7cf2083a3a34eb2f039f71162cc9e2348e7376cc Reviewed-on: https://go-review.googlesource.com/c/tools/+/563956 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI --- go/ssa/func.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/go/ssa/func.go b/go/ssa/func.go index 22f878d4ed4..0ac22046ebf 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -586,6 +586,12 @@ func WriteFunction(buf *bytes.Buffer, f *Function) { default: buf.WriteString(instr.String()) } + // -mode=S: show line numbers + if f.Prog.mode&LogSource != 0 { + if pos := instr.Pos(); pos.IsValid() { + fmt.Fprintf(buf, " L%d", f.Prog.Fset.Position(pos).Line) + } + } buf.WriteString("\n") } } From e325405222bad623d3fa529bccaa84fb9628d3da Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 14 Feb 2024 19:00:25 +0000 Subject: [PATCH 08/47] gopls/internal/test/integration: ignore telemetry prompt in assertion Fix a NoOutstandingWork assertion that was not ignoring the telemetry prompt. Fixes golang/go#65561 Change-Id: I194660965b681cc82e958dbbcf5df1404a576e09 Reviewed-on: https://go-review.googlesource.com/c/tools/+/563958 Auto-Submit: Robert Findley Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/test/integration/diagnostics/diagnostics_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go index dba9532dd6d..89c8a14bd37 100644 --- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go +++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go @@ -557,7 +557,7 @@ func f() { // AdHoc views are not critical errors, but their missing import // diagnostics should specifically mention GOROOT or GOPATH (and not // modules). - NoOutstandingWork(nil), + NoOutstandingWork(IgnoreTelemetryPromptWork), Diagnostics( env.AtRegexp("a.go", `"mod.com`), WithMessage("GOROOT or GOPATH"), From df9c1c7efa1a9c412265ae4eedf5e18b2f35f33f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 13 Feb 2024 16:35:15 -0500 Subject: [PATCH 09/47] gopls/internal/server: disambiguate diagnostics by OS,ARCH This change adds a disambiguating suffix such as " [windows,arm64]" to diagnostics that do not appear in the default build configuration. Fixes golang/go#65496 Change-Id: Ided7a7110ff630e57b7a96a311a240fef210ca93 Reviewed-on: https://go-review.googlesource.com/c/tools/+/563876 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/server/diagnostics.go | 80 +++++++++++++++---- .../testdata/diagnostics/osarch_suffix.txt | 46 +++++++++++ 2 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go index 6aa01aba127..4d8e3aae4d1 100644 --- a/gopls/internal/server/diagnostics.go +++ b/gopls/internal/server/diagnostics.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "sort" "strings" "sync" @@ -61,7 +62,9 @@ type ( diagMap = map[protocol.DocumentURI][]*cache.Diagnostic ) -// hashDiagnostics computes a hash to identify a diagnostic. +// hashDiagnostic computes a hash to identify a diagnostic. +// The hash is for deduplicating within a file, +// so it need not incorporate d.URI. func hashDiagnostic(d *cache.Diagnostic) file.Hash { h := sha256.New() for _, t := range d.Tags { @@ -822,23 +825,25 @@ func (s *server) updateOrphanedFileDiagnostics(ctx context.Context, modID uint64 // // If the publication succeeds, it updates f.publishedHash and f.mustPublish. func (s *server) publishFileDiagnosticsLocked(ctx context.Context, views viewSet, uri protocol.DocumentURI, version int32, f *fileDiagnostics) error { - // Check that the set of views is up-to-date, and de-dupe diagnostics - // across views. - var ( - diagHashes = make(map[file.Hash]unit) // unique diagnostic hashes - hash file.Hash // XOR of diagnostic hashes - unique []*cache.Diagnostic // unique diagnostics - ) - add := func(diag *cache.Diagnostic) { + + // We add a disambiguating suffix (e.g. " [darwin,arm64]") to + // each diagnostic that doesn't occur in the default view; + // see golang/go#65496. + type diagSuffix struct { + diag *cache.Diagnostic + suffix string // "" for default build (or orphans) + } + + // diagSuffixes records the set of view suffixes for a given diagnostic. + diagSuffixes := make(map[file.Hash][]diagSuffix) + add := func(diag *cache.Diagnostic, suffix string) { h := hashDiagnostic(diag) - if _, ok := diagHashes[h]; !ok { - diagHashes[h] = unit{} - unique = append(unique, diag) - hash.XORWith(h) - } + diagSuffixes[h] = append(diagSuffixes[h], diagSuffix{diag, suffix}) } + + // Construct the inverse mapping, from diagnostic (hash) to its suffixes (views). for _, diag := range f.orphanedFileDiagnostics { - add(diag) + add(diag, "") } for view, viewDiags := range f.byView { if _, ok := views[view]; !ok { @@ -848,10 +853,53 @@ func (s *server) publishFileDiagnosticsLocked(ctx context.Context, views viewSet if viewDiags.version != version { continue // a payload of diagnostics applies to a specific file version } + + // Compute the view's suffix (e.g. " [darwin,arm64]"). + var suffix string + { + var words []string + if view.GOOS() != runtime.GOOS { + words = append(words, view.GOOS()) + } + if view.GOARCH() != runtime.GOARCH { + words = append(words, view.GOARCH()) + } + if len(words) > 0 { + suffix = fmt.Sprintf(" [%s]", strings.Join(words, ",")) + } + } + for _, diag := range viewDiags.diagnostics { - add(diag) + add(diag, suffix) } } + + // De-dup diagnostics across views by hash, and sort. + var ( + hash file.Hash + unique []*cache.Diagnostic + ) + for h, items := range diagSuffixes { + // Sort the items by ascending suffix, so that the + // default view (if present) is first. + // (The others are ordered arbitrarily.) + sort.Slice(items, func(i, j int) bool { + return items[i].suffix < items[j].suffix + }) + + // If the diagnostic was not present in + // the default view, add the view suffix. + first := items[0] + if first.suffix != "" { + diag2 := *first.diag // shallow copy + diag2.Message += first.suffix + first.diag = &diag2 + h = hashDiagnostic(&diag2) // update the hash + } + + hash.XORWith(h) + unique = append(unique, first.diag) + } sortDiagnostics(unique) // Publish, if necessary. diff --git a/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt b/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt new file mode 100644 index 00000000000..95336085b2f --- /dev/null +++ b/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt @@ -0,0 +1,46 @@ +This test verifies that we add an [os,arch] suffix to each diagnostic +that doesn't appear in the default build (=runtime.{GOOS,GOARCH}). + +See golang/go#65496. + +The two p/*.go files below are written to trigger the same diagnostic +(range, message, source, etc) but varying only by URI. + +In the q test, a single location in the common code q.go has two +diagnostics, one of which is tagged. + +This test would fail on openbsd/mips64 because it will be +the same as the default build, so we skip that platform. + +-- flags -- +-skip_goos=openbsd + +-- go.mod -- +module example.com + +-- p/p.go -- +package p + +var _ fmt.Stringer //@diag("fmt", re"unde.*: fmt$") + +-- p/p_openbsd_mips64.go -- +package p + +var _ fmt.Stringer //@diag("fmt", re"unde.*: fmt \\[openbsd,mips64\\]") + +-- q/q_default.go -- +//+build !openbsd && !mips64 + +package q + +func f(int) int + +-- q/q_openbsd_mips64.go -- +package q + +func f(string) int + +-- q/q.go -- +package q + +var _ = f() //@ diag(")", re`.*want \(string\) \[openbsd,mips64\]`), diag(")", re`.*want \(int\)$`) From fef8b627151404b3b80fc2a1cf155dc7e81c69f8 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 14 Feb 2024 18:50:54 +0000 Subject: [PATCH 10/47] gopls/internal/server: fix a (mostly) benign race in diagnostics In golang/go#65312, we observed a test flake due to what I believe is a mostly benign race in diagnostics: 1. diagnoseSnapshot requests session.Views() 2. diagnoseSnapshot computes diagnostics 3. diagnoseSnapshot calls updateDiagnostics for the views from (1) This means that if a view V were created and diagnosed between (1) and (3), the call to updateDiagnostics will prune the diagnostics for V, because it doesn't think it's an active view. This is fundamentally a flaw in the design of multi-view diagnostics: we don't a priori know which views apply to a given file, so we have to do this complicated join. Nevertheless, I think this race is avoidable by requesting session.Views() inside the critical section of updateDiagnostics. In this way, it's not possible to overwrite diagnostics reported by a different View, without also observing that View. Fixes golang/go#65312 Change-Id: Ie4116fbfac2837f7d142c7865758ff6bac92fce4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/563957 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/server/diagnostics.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go index 4d8e3aae4d1..7db8d32761d 100644 --- a/gopls/internal/server/diagnostics.go +++ b/gopls/internal/server/diagnostics.go @@ -178,7 +178,6 @@ func (s *server) diagnoseSnapshot(snapshot *cache.Snapshot, changedURIs []protoc ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", snapshot.Labels()...) defer done() - allViews := s.session.Views() if delay > 0 { // 2-phase diagnostics. // @@ -207,7 +206,7 @@ func (s *server) diagnoseSnapshot(snapshot *cache.Snapshot, changedURIs []protoc } return } - s.updateDiagnostics(ctx, allViews, snapshot, diagnostics, false) + s.updateDiagnostics(ctx, snapshot, diagnostics, false) } if delay < minDelay { @@ -230,7 +229,7 @@ func (s *server) diagnoseSnapshot(snapshot *cache.Snapshot, changedURIs []protoc } return } - s.updateDiagnostics(ctx, allViews, snapshot, diagnostics, true) + s.updateDiagnostics(ctx, snapshot, diagnostics, true) } func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snapshot, uris []protocol.DocumentURI) (diagMap, error) { @@ -671,10 +670,7 @@ func (s *server) updateCriticalErrorStatus(ctx context.Context, snapshot *cache. // updateDiagnostics records the result of diagnosing a snapshot, and publishes // any diagnostics that need to be updated on the client. -// -// The allViews argument should be the current set of views present in the -// session, for the purposes of trimming diagnostics produced by deleted views. -func (s *server) updateDiagnostics(ctx context.Context, allViews []*cache.View, snapshot *cache.Snapshot, diagnostics diagMap, final bool) { +func (s *server) updateDiagnostics(ctx context.Context, snapshot *cache.Snapshot, diagnostics diagMap, final bool) { ctx, done := event.Start(ctx, "Server.publishDiagnostics") defer done() @@ -697,8 +693,13 @@ func (s *server) updateDiagnostics(ctx context.Context, allViews []*cache.View, return } + // golang/go#65312: since the set of diagnostics depends on the set of views, + // we get the views *after* locking diagnosticsMu. This ensures that + // updateDiagnostics does not incorrectly delete diagnostics that have been + // set for an existing view that was created between the call to + // s.session.Views() and updateDiagnostics. viewMap := make(viewSet) - for _, v := range allViews { + for _, v := range s.session.Views() { viewMap[v] = unit{} } From ea9e542160393a4034064d04d36ac5bb08b8c01c Mon Sep 17 00:00:00 2001 From: Tim King Date: Fri, 9 Feb 2024 09:36:22 -0800 Subject: [PATCH 11/47] internal/versions: fix package to accept go1.21.0-bigcorp Adds support for suffixes to x/tools copy of go/version. Brings in go.dev/cl/559796. Updates golang/go#65061 Change-Id: Iaa0c98a73d8ddd8a42f0c4d3df7d4d79eb7aeb0a Reviewed-on: https://go-review.googlesource.com/c/tools/+/562838 LUCI-TryBot-Result: Go LUCI Run-TryBot: Tim King TryBot-Result: Gopher Robot Reviewed-by: Bryan Mills --- internal/versions/versions.go | 3 +++ internal/versions/versions_test.go | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/internal/versions/versions.go b/internal/versions/versions.go index e16f6c33a52..f8982841b7b 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -4,6 +4,8 @@ package versions +import "strings" + // Note: If we use build tags to use go/versions when go >=1.22, // we run into go.dev/issue/53737. Under some operations users would see an // import of "go/versions" even if they would not compile the file. @@ -45,6 +47,7 @@ func IsValid(x string) bool { return isValid(stripGo(x)) } // stripGo converts from a "go1.21" version to a "1.21" version. // If v does not start with "go", stripGo returns the empty string (a known invalid version). func stripGo(v string) string { + v, _, _ = strings.Cut(v, "-") // strip -bigcorp suffix. if len(v) < 2 || v[:2] != "go" { return "" } diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go index 1222ba7942a..997de2a8a61 100644 --- a/internal/versions/versions_test.go +++ b/internal/versions/versions_test.go @@ -20,6 +20,7 @@ func TestIsValid(t *testing.T) { "go0.0", // ?? "go1", "go2", + "go1.20.0-bigcorp", } { if !versions.IsValid(x) { t.Errorf("expected versions.IsValid(%q) to hold", x) @@ -40,6 +41,7 @@ func TestIsValid(t *testing.T) { "go1.21.2_2", "go1.21rc_2", "go1.21rc2_", + "go1.600+auto", } { if versions.IsValid(x) { t.Errorf("expected versions.IsValid(%q) to not hold", x) @@ -52,6 +54,7 @@ func TestVersionComparisons(t *testing.T) { x, y string want int }{ + // All comparisons of go2, go1.21.2, go1.21rc2, go1.21rc2, go1, go0.0, "", bad {"go2", "go2", 0}, {"go2", "go1.21.2", +1}, {"go2", "go1.21rc2", +1}, @@ -97,6 +100,11 @@ func TestVersionComparisons(t *testing.T) { {"", "", 0}, {"", "bad", 0}, {"bad", "bad", 0}, + // Other tests. + {"go1.20", "go1.20.0-bigcorp", 0}, + {"go1.21", "go1.21.0-bigcorp", -1}, // Starting in Go 1.21, patch missing is different from explicit .0. + {"go1.21.0", "go1.21.0-bigcorp", 0}, // Starting in Go 1.21, patch missing is different from explicit .0. + {"go1.19rc1", "go1.19", -1}, } { got := versions.Compare(item.x, item.y) if got != item.want { From babbbed1953d886c09879549f55a27d1b518b603 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 9 Feb 2024 14:28:18 +0000 Subject: [PATCH 12/47] gopls/release: remove obsolete validateHardCodedVersion We no longer hard-code the gopls version. Change-Id: I6f91f6e8f97732803aa5dfe05203ceee0611c489 Reviewed-on: https://go-review.googlesource.com/c/tools/+/562677 LUCI-TryBot-Result: Go LUCI Reviewed-by: Hyang-Ah Hana Kim --- gopls/release/release.go | 48 ---------------------------------------- 1 file changed, 48 deletions(-) diff --git a/gopls/release/release.go b/gopls/release/release.go index 77b0aaf40d6..26ce5f7870a 100644 --- a/gopls/release/release.go +++ b/gopls/release/release.go @@ -14,17 +14,14 @@ package main import ( "flag" "fmt" - "go/types" "log" "os" "os/exec" "path/filepath" - "strconv" "strings" "golang.org/x/mod/modfile" "golang.org/x/mod/semver" - "golang.org/x/tools/go/packages" ) var versionFlag = flag.String("version", "", "version to tag") @@ -52,10 +49,6 @@ func main() { if filepath.Base(wd) != "gopls" { log.Fatalf("must run from the gopls module") } - // Confirm that they have updated the hardcoded version. - if err := validateHardcodedVersion(*versionFlag); err != nil { - log.Fatal(err) - } // Confirm that the versions in the go.mod file are correct. if err := validateGoModFile(wd); err != nil { log.Fatal(err) @@ -64,47 +57,6 @@ func main() { os.Exit(0) } -// validateHardcodedVersion reports whether the version hardcoded in the gopls -// binary is equivalent to the version being published. It reports an error if -// not. -func validateHardcodedVersion(version string) error { - const debugPkg = "golang.org/x/tools/gopls/internal/debug" - pkgs, err := packages.Load(&packages.Config{ - Mode: packages.NeedName | packages.NeedFiles | - packages.NeedCompiledGoFiles | packages.NeedImports | - packages.NeedTypes | packages.NeedTypesSizes, - }, debugPkg) - if err != nil { - return err - } - if len(pkgs) != 1 { - return fmt.Errorf("expected 1 package, got %v", len(pkgs)) - } - pkg := pkgs[0] - if len(pkg.Errors) > 0 { - return fmt.Errorf("failed to load %q: first error: %w", debugPkg, pkg.Errors[0]) - } - obj := pkg.Types.Scope().Lookup("Version") - c, ok := obj.(*types.Const) - if !ok { - return fmt.Errorf("no constant named Version") - } - hardcodedVersion, err := strconv.Unquote(c.Val().ExactString()) - if err != nil { - return err - } - if semver.Prerelease(hardcodedVersion) != "" { - return fmt.Errorf("unexpected pre-release for hardcoded version: %s", hardcodedVersion) - } - // Don't worry about pre-release tags and expect that there is no build - // suffix. - version = strings.TrimSuffix(version, semver.Prerelease(version)) - if hardcodedVersion != version { - return fmt.Errorf("expected version to be %s, got %s", *versionFlag, hardcodedVersion) - } - return nil -} - func validateGoModFile(goplsDir string) error { filename := filepath.Join(goplsDir, "go.mod") data, err := os.ReadFile(filename) From 7240af8beadeacfbcfc9b04934e258300461c6a1 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 15 Feb 2024 11:33:49 -0500 Subject: [PATCH 13/47] gopls/internal/cache: remove parsego.* aliases Also, remove redundant "Parse" prefix from Full, Header. Change-Id: Iba9e1d0f128438232e5afb8b9e2a979a61136a83 Reviewed-on: https://go-review.googlesource.com/c/tools/+/564336 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/cache/analysis.go | 13 ++++----- gopls/internal/cache/check.go | 13 ++++----- gopls/internal/cache/errors.go | 5 ++-- gopls/internal/cache/mod_tidy.go | 9 ++++--- gopls/internal/cache/parse.go | 6 ++--- gopls/internal/cache/parse_cache.go | 12 ++++----- gopls/internal/cache/parse_cache_test.go | 27 ++++++++++--------- gopls/internal/cache/parsego/parse.go | 8 +++--- gopls/internal/cache/parsego/parse_test.go | 2 +- gopls/internal/cache/pkg.go | 24 +++++++---------- gopls/internal/cache/snapshot.go | 21 ++++++++------- gopls/internal/cache/symbols.go | 3 ++- gopls/internal/cache/typerefs/pkgrefs_test.go | 9 +++---- gopls/internal/cache/typerefs/refs_test.go | 2 +- gopls/internal/golang/add_import.go | 3 ++- gopls/internal/golang/call_hierarchy.go | 3 ++- gopls/internal/golang/change_quote.go | 3 ++- gopls/internal/golang/change_signature.go | 4 +-- gopls/internal/golang/code_lens.go | 11 ++++---- gopls/internal/golang/codeaction.go | 12 ++++----- .../internal/golang/completion/definition.go | 4 +-- gopls/internal/golang/completion/package.go | 5 ++-- gopls/internal/golang/definition.go | 10 +++---- gopls/internal/golang/folding_range.go | 7 ++--- gopls/internal/golang/format.go | 13 ++++----- gopls/internal/golang/hover.go | 10 +++---- gopls/internal/golang/inline.go | 2 +- gopls/internal/golang/inline_all.go | 5 ++-- gopls/internal/golang/linkname.go | 3 ++- gopls/internal/golang/references.go | 7 ++--- gopls/internal/golang/rename.go | 18 ++++++------- gopls/internal/golang/semtok.go | 3 ++- gopls/internal/golang/snapshot.go | 13 +++------ gopls/internal/golang/symbols.go | 3 ++- gopls/internal/golang/util.go | 3 ++- gopls/internal/protocol/mapper.go | 2 +- gopls/internal/server/command.go | 2 +- gopls/internal/server/link.go | 2 +- gopls/internal/server/selection_range.go | 2 +- 39 files changed, 155 insertions(+), 149 deletions(-) diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index 0b7bc08d378..4fe5f1103cc 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go @@ -32,11 +32,12 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/filecache" - "golang.org/x/tools/gopls/internal/cache/metadata" - "golang.org/x/tools/gopls/internal/protocol" "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/astutil" "golang.org/x/tools/gopls/internal/util/bug" @@ -785,7 +786,7 @@ func (an *analysisNode) cacheKey() [sha256.Size]byte { func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { // Parse only the "compiled" Go files. // Do the computation in parallel. - parsed := make([]*ParsedGoFile, len(an.files)) + parsed := make([]*parsego.File, len(an.files)) { var group errgroup.Group group.SetLimit(4) // not too much: run itself is already called in parallel @@ -796,7 +797,7 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { // as cached ASTs require the global FileSet. // ast.Object resolution is unfortunately an implied part of the // go/analysis contract. - pgf, err := parseGoImpl(ctx, an.fset, fh, ParseFull&^parser.SkipObjectResolution, false) + pgf, err := parseGoImpl(ctx, an.fset, fh, parsego.Full&^parser.SkipObjectResolution, false) parsed[i] = pgf return err }) @@ -910,7 +911,7 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { } // Postcondition: analysisPackage.types and an.exportDeps are populated. -func (an *analysisNode) typeCheck(parsed []*ParsedGoFile) *analysisPackage { +func (an *analysisNode) typeCheck(parsed []*parsego.File) *analysisPackage { mp := an.mp if false { // debugging @@ -1101,7 +1102,7 @@ func readShallowManifest(export []byte) ([]PackagePath, error) { type analysisPackage struct { mp *metadata.Package fset *token.FileSet // local to this package - parsed []*ParsedGoFile + parsed []*parsego.File files []*ast.File // same as parsed[i].File types *types.Package compiles bool // package is transitively free of list/parse/type errors diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index 0af59655ab1..7b700ea9a75 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -23,6 +23,7 @@ import ( "golang.org/x/sync/errgroup" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/cache/typerefs" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/filecache" @@ -675,7 +676,7 @@ func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageH // Parse the compiled go files, bypassing the parse cache as packages checked // for import are unlikely to get cache hits. Additionally, we can optimize // parsing slightly by not passing parser.ParseComments. - pgfs := make([]*ParsedGoFile, len(ph.localInputs.compiledGoFiles)) + pgfs := make([]*parsego.File, len(ph.localInputs.compiledGoFiles)) { var group errgroup.Group // Set an arbitrary concurrency limit; we want some parallelism but don't @@ -1271,7 +1272,7 @@ func (s *Snapshot) typerefData(ctx context.Context, id PackageID, imports map[Im bug.Reportf("internal error reading typerefs data: %v", err) } - pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), ParseFull&^parser.ParseComments, true, cgfs...) + pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full&^parser.ParseComments, true, cgfs...) if err != nil { return nil, err } @@ -1471,11 +1472,11 @@ func (b *typeCheckBatch) checkPackage(ctx context.Context, ph *packageHandle) (* // Collect parsed files from the type check pass, capturing parse errors from // compiled files. var err error - pkg.goFiles, err = b.parseCache.parseFiles(ctx, b.fset, ParseFull, false, inputs.goFiles...) + pkg.goFiles, err = b.parseCache.parseFiles(ctx, b.fset, parsego.Full, false, inputs.goFiles...) if err != nil { return nil, err } - pkg.compiledGoFiles, err = b.parseCache.parseFiles(ctx, b.fset, ParseFull, false, inputs.compiledGoFiles...) + pkg.compiledGoFiles, err = b.parseCache.parseFiles(ctx, b.fset, parsego.Full, false, inputs.compiledGoFiles...) if err != nil { return nil, err } @@ -1670,12 +1671,12 @@ func depsErrors(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) ( // Build an index of all imports in the package. type fileImport struct { - cgf *ParsedGoFile + cgf *parsego.File imp *ast.ImportSpec } allImports := map[string][]fileImport{} for _, uri := range mp.CompiledGoFiles { - pgf, err := parseGoURI(ctx, snapshot, uri, ParseHeader) + pgf, err := parseGoURI(ctx, snapshot, uri, parsego.Header) if err != nil { return nil, err } diff --git a/gopls/internal/cache/errors.go b/gopls/internal/cache/errors.go index 83382b0aad2..6c95526d1ea 100644 --- a/gopls/internal/cache/errors.go +++ b/gopls/internal/cache/errors.go @@ -22,6 +22,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/cache/metadata" + "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/protocol/command" @@ -464,7 +465,7 @@ func parseGoListImportCycleError(ctx context.Context, e packages.Error, mp *meta // Imports have quotation marks around them. circImp := strconv.Quote(importList[1]) for _, uri := range mp.CompiledGoFiles { - pgf, err := parseGoURI(ctx, fs, uri, ParseHeader) + pgf, err := parseGoURI(ctx, fs, uri, parsego.Header) if err != nil { return nil, err } @@ -497,7 +498,7 @@ func parseGoListImportCycleError(ctx context.Context, e packages.Error, mp *meta // It returns an error if the file could not be read. // // TODO(rfindley): eliminate this helper. -func parseGoURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI, mode parser.Mode) (*ParsedGoFile, error) { +func parseGoURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI, mode parser.Mode) (*parsego.File, error) { fh, err := fs.ReadFile(ctx, uri) if err != nil { return nil, err diff --git a/gopls/internal/cache/mod_tidy.go b/gopls/internal/cache/mod_tidy.go index 6dbe9820182..79867855e0c 100644 --- a/gopls/internal/cache/mod_tidy.go +++ b/gopls/internal/cache/mod_tidy.go @@ -16,9 +16,10 @@ import ( "strings" "golang.org/x/mod/modfile" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/tag" @@ -281,7 +282,7 @@ func missingModuleDiagnostics(ctx context.Context, snapshot *Snapshot, pm *Parse continue } for _, goFile := range compiledGoFiles { - pgf, err := snapshot.ParseGo(ctx, goFile, ParseHeader) + pgf, err := snapshot.ParseGo(ctx, goFile, parsego.Header) if err != nil { continue } @@ -461,7 +462,7 @@ func switchDirectness(req *modfile.Require, m *protocol.Mapper) ([]protocol.Text // missingModuleForImport creates an error for a given import path that comes // from a missing module. -func missingModuleForImport(pgf *ParsedGoFile, imp *ast.ImportSpec, req *modfile.Require, fixes []SuggestedFix) (*Diagnostic, error) { +func missingModuleForImport(pgf *parsego.File, imp *ast.ImportSpec, req *modfile.Require, fixes []SuggestedFix) (*Diagnostic, error) { if req.Syntax == nil { return nil, fmt.Errorf("no syntax for %v", req) } @@ -488,7 +489,7 @@ func missingModuleForImport(pgf *ParsedGoFile, imp *ast.ImportSpec, req *modfile // // TODO(rfindley): this should key off ImportPath. func parseImports(ctx context.Context, s *Snapshot, files []file.Handle) (map[string]bool, error) { - pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), ParseHeader, false, files...) + pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Header, false, files...) if err != nil { // e.g. context cancellation return nil, err } diff --git a/gopls/internal/cache/parse.go b/gopls/internal/cache/parse.go index c8da20eed13..56130c6e1fb 100644 --- a/gopls/internal/cache/parse.go +++ b/gopls/internal/cache/parse.go @@ -11,14 +11,14 @@ import ( "go/token" "path/filepath" - "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/cache/parsego" + "golang.org/x/tools/gopls/internal/file" ) // ParseGo parses the file whose contents are provided by fh. // The resulting tree may have been fixed up. // If the file is not available, returns nil and an error. -func (s *Snapshot) ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode) (*ParsedGoFile, error) { +func (s *Snapshot) ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode) (*parsego.File, error) { pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh) if err != nil { return nil, err @@ -27,7 +27,7 @@ func (s *Snapshot) ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode } // parseGoImpl parses the Go source file whose content is provided by fh. -func parseGoImpl(ctx context.Context, fset *token.FileSet, fh file.Handle, mode parser.Mode, purgeFuncBodies bool) (*ParsedGoFile, error) { +func parseGoImpl(ctx context.Context, fset *token.FileSet, fh file.Handle, mode parser.Mode, purgeFuncBodies bool) (*parsego.File, error) { ext := filepath.Ext(fh.URI().Path()) if ext != ".go" && ext != "" { // files generated by cgo have no extension return nil, fmt.Errorf("cannot parse non-Go file %s", fh.URI()) diff --git a/gopls/internal/cache/parse_cache.go b/gopls/internal/cache/parse_cache.go index 55eced51403..8586f655d28 100644 --- a/gopls/internal/cache/parse_cache.go +++ b/gopls/internal/cache/parse_cache.go @@ -17,8 +17,8 @@ import ( "time" "golang.org/x/sync/errgroup" - "golang.org/x/tools/gopls/internal/file" "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/internal/memoize" "golang.org/x/tools/internal/tokeninternal" @@ -135,7 +135,7 @@ type parseKey struct { type parseCacheEntry struct { key parseKey hash file.Hash - promise *memoize.Promise // memoize.Promise[*ParsedGoFile] + promise *memoize.Promise // memoize.Promise[*parsego.File] atime uint64 // clock time of last access, for use in LRU sorting walltime time.Time // actual time of last access, for use in time-based eviction; too coarse for LRU on some systems lruIndex int // owned by the queue implementation @@ -303,7 +303,7 @@ func (c *parseCache) allocateSpace(size int) (int, int) { return base, c.nextBase } -// parseFiles returns a ParsedGoFile for each file handle in fhs, in the +// parseFiles returns a parsego.File for each file handle in fhs, in the // requested parse mode. // // For parsed files that already exists in the cache, access time will be @@ -317,8 +317,8 @@ func (c *parseCache) allocateSpace(size int) (int, int) { // // If parseFiles returns an error, it still returns a slice, // but with a nil entry for each file that could not be parsed. -func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode parser.Mode, purgeFuncBodies bool, fhs ...file.Handle) ([]*ParsedGoFile, error) { - pgfs := make([]*ParsedGoFile, len(fhs)) +func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode parser.Mode, purgeFuncBodies bool, fhs ...file.Handle) ([]*parsego.File, error) { + pgfs := make([]*parsego.File, len(fhs)) // Temporary fall-back for 32-bit systems, where reservedForParsing is too // small to be viable. We don't actually support 32-bit systems, so this @@ -351,7 +351,7 @@ func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode p if err != nil { return err } - pgfs[i] = result.(*ParsedGoFile) + pgfs[i] = result.(*parsego.File) return nil }) } diff --git a/gopls/internal/cache/parse_cache_test.go b/gopls/internal/cache/parse_cache_test.go index eee7ded39af..7aefac77c38 100644 --- a/gopls/internal/cache/parse_cache_test.go +++ b/gopls/internal/cache/parse_cache_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" ) @@ -31,12 +32,12 @@ func TestParseCache(t *testing.T) { fset := token.NewFileSet() cache := newParseCache(0) - pgfs1, err := cache.parseFiles(ctx, fset, ParseFull, false, fh) + pgfs1, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) if err != nil { t.Fatal(err) } pgf1 := pgfs1[0] - pgfs2, err := cache.parseFiles(ctx, fset, ParseFull, false, fh) + pgfs2, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) pgf2 := pgfs2[0] if err != nil { t.Fatal(err) @@ -50,7 +51,7 @@ func TestParseCache(t *testing.T) { files := []file.Handle{fh} files = append(files, dummyFileHandles(parseCacheMinFiles-1)...) - pgfs3, err := cache.parseFiles(ctx, fset, ParseFull, false, files...) + pgfs3, err := cache.parseFiles(ctx, fset, parsego.Full, false, files...) if err != nil { t.Fatal(err) } @@ -68,13 +69,13 @@ func TestParseCache(t *testing.T) { // Now overwrite the cache, after which we should get new results. cache.gcOnce() files = dummyFileHandles(parseCacheMinFiles) - _, err = cache.parseFiles(ctx, fset, ParseFull, false, files...) + _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) if err != nil { t.Fatal(err) } // force a GC, which should collect the recently parsed files cache.gcOnce() - pgfs4, err := cache.parseFiles(ctx, fset, ParseFull, false, fh) + pgfs4, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) if err != nil { t.Fatal(err) } @@ -98,7 +99,7 @@ func TestParseCache_Reparsing(t *testing.T) { // Parsing should succeed even though we overflow the padding. cache := newParseCache(0) - _, err := cache.parseFiles(context.Background(), token.NewFileSet(), ParseFull, false, files...) + _, err := cache.parseFiles(context.Background(), token.NewFileSet(), parsego.Full, false, files...) if err != nil { t.Fatal(err) } @@ -118,7 +119,7 @@ func TestParseCache_Issue59097(t *testing.T) { // Parsing should succeed even though we overflow the padding. cache := newParseCache(0) - _, err := cache.parseFiles(context.Background(), token.NewFileSet(), ParseFull, false, files...) + _, err := cache.parseFiles(context.Background(), token.NewFileSet(), parsego.Full, false, files...) if err != nil { t.Fatal(err) } @@ -136,19 +137,19 @@ func TestParseCache_TimeEviction(t *testing.T) { cache := newParseCache(gcDuration) cache.stop() // we'll manage GC manually, for testing. - pgfs0, err := cache.parseFiles(ctx, fset, ParseFull, false, fh, fh) + pgfs0, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) if err != nil { t.Fatal(err) } files := dummyFileHandles(parseCacheMinFiles) - _, err = cache.parseFiles(ctx, fset, ParseFull, false, files...) + _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) if err != nil { t.Fatal(err) } // Even after filling up the 'min' files, we get a cache hit for our original file. - pgfs1, err := cache.parseFiles(ctx, fset, ParseFull, false, fh, fh) + pgfs1, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) if err != nil { t.Fatal(err) } @@ -158,14 +159,14 @@ func TestParseCache_TimeEviction(t *testing.T) { } // But after GC, we get a cache miss. - _, err = cache.parseFiles(ctx, fset, ParseFull, false, files...) // mark dummy files as newer + _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) // mark dummy files as newer if err != nil { t.Fatal(err) } time.Sleep(gcDuration) cache.gcOnce() - pgfs2, err := cache.parseFiles(ctx, fset, ParseFull, false, fh, fh) + pgfs2, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) if err != nil { t.Fatal(err) } @@ -183,7 +184,7 @@ func TestParseCache_Duplicates(t *testing.T) { fh := makeFakeFileHandle(uri, []byte("package p\n\nconst _ = \"foo\"")) cache := newParseCache(0) - pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), ParseFull, false, fh, fh) + pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), parsego.Full, false, fh, fh) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go index 89fc15fb279..739ee1386bd 100644 --- a/gopls/internal/cache/parsego/parse.go +++ b/gopls/internal/cache/parsego/parse.go @@ -25,14 +25,14 @@ import ( // Common parse modes; these should be reused wherever possible to increase // cache hits. const ( - // ParseHeader specifies that the main package declaration and imports are needed. + // Header specifies that the main package declaration and imports are needed. // This is the mode used when attempting to examine the package graph structure. - ParseHeader = parser.AllErrors | parser.ParseComments | parser.ImportsOnly | parser.SkipObjectResolution + Header = parser.AllErrors | parser.ParseComments | parser.ImportsOnly | parser.SkipObjectResolution - // ParseFull specifies the full AST is needed. + // Full specifies the full AST is needed. // This is used for files of direct interest where the entire contents must // be considered. - ParseFull = parser.AllErrors | parser.ParseComments | parser.SkipObjectResolution + Full = parser.AllErrors | parser.ParseComments | parser.SkipObjectResolution ) // Parse parses a buffer of Go source, repairing the tree if necessary. diff --git a/gopls/internal/cache/parsego/parse_test.go b/gopls/internal/cache/parsego/parse_test.go index 4018e9ed886..c64125427b1 100644 --- a/gopls/internal/cache/parsego/parse_test.go +++ b/gopls/internal/cache/parsego/parse_test.go @@ -32,7 +32,7 @@ func _() { } ` - pgf, _ := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", []byte(src), parsego.ParseFull, false) + pgf, _ := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", []byte(src), parsego.Full, false) fset := tokeninternal.FileSetFor(pgf.Tok) ast.Inspect(pgf.File, func(n ast.Node) bool { if n != nil { diff --git a/gopls/internal/cache/pkg.go b/gopls/internal/cache/pkg.go index 821b1cc48e8..c0fa37fc225 100644 --- a/gopls/internal/cache/pkg.go +++ b/gopls/internal/cache/pkg.go @@ -21,16 +21,10 @@ import ( // Convenient aliases for very heavily used types. type ( - PackageID = metadata.PackageID - PackagePath = metadata.PackagePath - PackageName = metadata.PackageName - ImportPath = metadata.ImportPath - ParsedGoFile = parsego.File -) - -const ( - ParseHeader = parsego.ParseHeader - ParseFull = parsego.ParseFull + PackageID = metadata.PackageID + PackagePath = metadata.PackagePath + PackageName = metadata.PackageName + ImportPath = metadata.ImportPath ) // A Package is the union of package metadata and type checking results. @@ -51,8 +45,8 @@ type syntaxPackage struct { // -- outputs -- fset *token.FileSet // for now, same as the snapshot's FileSet - goFiles []*ParsedGoFile - compiledGoFiles []*ParsedGoFile + goFiles []*parsego.File + compiledGoFiles []*parsego.File diagnostics []*Diagnostic parseErrors []scanner.ErrorList typeErrors []types.Error @@ -108,15 +102,15 @@ func (packageLoadScope) aScope() {} func (moduleLoadScope) aScope() {} func (viewLoadScope) aScope() {} -func (p *Package) CompiledGoFiles() []*ParsedGoFile { +func (p *Package) CompiledGoFiles() []*parsego.File { return p.pkg.compiledGoFiles } -func (p *Package) File(uri protocol.DocumentURI) (*ParsedGoFile, error) { +func (p *Package) File(uri protocol.DocumentURI) (*parsego.File, error) { return p.pkg.File(uri) } -func (pkg *syntaxPackage) File(uri protocol.DocumentURI) (*ParsedGoFile, error) { +func (pkg *syntaxPackage) File(uri protocol.DocumentURI) (*parsego.File, error) { for _, cgf := range pkg.compiledGoFiles { if cgf.URI == uri { return cgf, nil diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 3d97ed47ccb..6ac8c031c5b 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -30,6 +30,7 @@ import ( "golang.org/x/tools/go/types/objectpath" "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/typerefs" "golang.org/x/tools/gopls/internal/cache/xrefs" "golang.org/x/tools/gopls/internal/file" @@ -116,7 +117,7 @@ type Snapshot struct { // builtin is the location of builtin.go in GOROOT. // // TODO(rfindley): would it make more sense to eagerly parse builtin, and - // instead store a *ParsedGoFile here? + // instead store a *parsego.File here? builtin protocol.DocumentURI // meta holds loaded metadata. @@ -1615,8 +1616,8 @@ https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-str // orphanedFileDiagnosticRange returns the position to use for orphaned file diagnostics. // We only warn about an orphaned file if it is well-formed enough to actually // be part of a package. Otherwise, we need more information. -func orphanedFileDiagnosticRange(ctx context.Context, cache *parseCache, fh file.Handle) (*ParsedGoFile, protocol.Range, bool) { - pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), ParseHeader, false, fh) +func orphanedFileDiagnosticRange(ctx context.Context, cache *parseCache, fh file.Handle) (*parsego.File, protocol.Range, bool) { + pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), parsego.Header, false, fh) if err != nil { return nil, protocol.Range{}, false } @@ -2176,8 +2177,8 @@ func metadataChanges(ctx context.Context, lockedSnapshot *Snapshot, oldFH, newFH fset := token.NewFileSet() // Parse headers to compare package names and imports. - oldHeads, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, ParseHeader, false, oldFH) - newHeads, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, ParseHeader, false, newFH) + oldHeads, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Header, false, oldFH) + newHeads, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Header, false, newFH) if oldErr != nil || newErr != nil { errChanged := (oldErr == nil) != (newErr == nil) @@ -2223,12 +2224,12 @@ func metadataChanges(ctx context.Context, lockedSnapshot *Snapshot, oldFH, newFH // Note: if this affects performance we can probably avoid parsing in the // common case by first scanning the source for potential comments. if !invalidate { - origFulls, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, ParseFull, false, oldFH) - newFulls, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, ParseFull, false, newFH) + origFulls, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Full, false, oldFH) + newFulls, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Full, false, newFH) if oldErr == nil && newErr == nil { invalidate = magicCommentsChanged(origFulls[0].File, newFulls[0].File) } else { - // At this point, we shouldn't ever fail to produce a ParsedGoFile, as + // At this point, we shouldn't ever fail to produce a parsego.File, as // we're already past header parsing. bug.Reportf("metadataChanges: unparseable file %v (old error: %v, new error: %v)", oldFH.URI(), oldErr, newErr) } @@ -2292,7 +2293,7 @@ func extractMagicComments(f *ast.File) []string { } // BuiltinFile returns information about the special builtin package. -func (s *Snapshot) BuiltinFile(ctx context.Context) (*ParsedGoFile, error) { +func (s *Snapshot) BuiltinFile(ctx context.Context) (*parsego.File, error) { s.AwaitInitialized(ctx) s.mu.Lock() @@ -2309,7 +2310,7 @@ func (s *Snapshot) BuiltinFile(ctx context.Context) (*ParsedGoFile, error) { } // For the builtin file only, we need syntactic object resolution // (since we can't type check). - mode := ParseFull &^ parser.SkipObjectResolution + mode := parsego.Full &^ parser.SkipObjectResolution pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh) if err != nil { return nil, err diff --git a/gopls/internal/cache/symbols.go b/gopls/internal/cache/symbols.go index 5dce87df223..9954c747798 100644 --- a/gopls/internal/cache/symbols.go +++ b/gopls/internal/cache/symbols.go @@ -11,6 +11,7 @@ import ( "go/types" "strings" + "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/util/astutil" @@ -68,7 +69,7 @@ func (s *Snapshot) symbolize(ctx context.Context, uri protocol.DocumentURI) ([]S // symbolizeImpl reads and parses a file and extracts symbols from it. func symbolizeImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) ([]Symbol, error) { - pgfs, err := snapshot.view.parseCache.parseFiles(ctx, token.NewFileSet(), ParseFull, false, fh) + pgfs, err := snapshot.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full, false, fh) if err != nil { return nil, err } diff --git a/gopls/internal/cache/typerefs/pkgrefs_test.go b/gopls/internal/cache/typerefs/pkgrefs_test.go index da14b90cba3..9d4b5c011d3 100644 --- a/gopls/internal/cache/typerefs/pkgrefs_test.go +++ b/gopls/internal/cache/typerefs/pkgrefs_test.go @@ -42,7 +42,6 @@ type ( PackagePath = metadata.PackagePath Metadata = metadata.Package MetadataSource = metadata.Source - ParsedGoFile = parsego.File ) // TestBuildPackageGraph tests the BuildPackageGraph constructor, which uses @@ -277,7 +276,7 @@ type memoizedParser struct { type futureParse struct { done chan struct{} - pgf *ParsedGoFile + pgf *parsego.File err error } @@ -287,15 +286,15 @@ func newParser() *memoizedParser { } } -func (p *memoizedParser) parse(ctx context.Context, uri protocol.DocumentURI) (*ParsedGoFile, error) { - doParse := func(ctx context.Context, uri protocol.DocumentURI) (*ParsedGoFile, error) { +func (p *memoizedParser) parse(ctx context.Context, uri protocol.DocumentURI) (*parsego.File, error) { + doParse := func(ctx context.Context, uri protocol.DocumentURI) (*parsego.File, error) { // TODO(adonovan): hoist this operation outside the benchmark critsec. content, err := os.ReadFile(uri.Path()) if err != nil { return nil, err } content = astutil.PurgeFuncBodies(content) - pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, content, parsego.ParseFull, false) + pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, content, parsego.Full, false) return pgf, nil } diff --git a/gopls/internal/cache/typerefs/refs_test.go b/gopls/internal/cache/typerefs/refs_test.go index 9bb9ec5bdfa..1e98fb585ed 100644 --- a/gopls/internal/cache/typerefs/refs_test.go +++ b/gopls/internal/cache/typerefs/refs_test.go @@ -507,7 +507,7 @@ type Z map[ext.A]ext.B var pgfs []*parsego.File for i, src := range test.srcs { uri := protocol.DocumentURI(fmt.Sprintf("file:///%d.go", i)) - pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, []byte(src), parsego.ParseFull, false) + pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, []byte(src), parsego.Full, false) if !test.allowErrs && pgf.ParseErr != nil { t.Fatalf("ParseGoSrc(...) returned parse errors: %v", pgf.ParseErr) } diff --git a/gopls/internal/golang/add_import.go b/gopls/internal/golang/add_import.go index 201edce5306..a43256a6a08 100644 --- a/gopls/internal/golang/add_import.go +++ b/gopls/internal/golang/add_import.go @@ -8,6 +8,7 @@ import ( "context" "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/protocol" "golang.org/x/tools/internal/imports" @@ -15,7 +16,7 @@ import ( // AddImport adds a single import statement to the given file func AddImport(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, importPath string) ([]protocol.TextEdit, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } diff --git a/gopls/internal/golang/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go index 87a6a54e458..7e88df1a1cf 100644 --- a/gopls/internal/golang/call_hierarchy.go +++ b/gopls/internal/golang/call_hierarchy.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "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/protocol" "golang.org/x/tools/gopls/internal/util/bug" @@ -116,7 +117,7 @@ func enclosingNodeCallItem(ctx context.Context, snapshot *cache.Snapshot, pkgPat // that don't contain the reference, using either a scanner-based // implementation such as https://go.dev/play/p/KUrObH1YkX8 // (~31% speedup), or a byte-oriented implementation (2x speedup). - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return protocol.CallHierarchyItem{}, err } diff --git a/gopls/internal/golang/change_quote.go b/gopls/internal/golang/change_quote.go index 98a60e11ae0..919b935e79c 100644 --- a/gopls/internal/golang/change_quote.go +++ b/gopls/internal/golang/change_quote.go @@ -11,6 +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/util/bug" @@ -24,7 +25,7 @@ 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 *ParsedGoFile, fh file.Handle, rng protocol.Range) (protocol.CodeAction, bool) { +func ConvertStringLiteral(pgf *parsego.File, fh file.Handle, rng protocol.Range) (protocol.CodeAction, bool) { startPos, endPos, err := pgf.RangePos(rng) if err != nil { return protocol.CodeAction{}, false // e.g. invalid range diff --git a/gopls/internal/golang/change_signature.go b/gopls/internal/golang/change_signature.go index e0a829e128e..81d2b78b039 100644 --- a/gopls/internal/golang/change_signature.go +++ b/gopls/internal/golang/change_signature.go @@ -246,7 +246,7 @@ type ParamInfo struct { } // FindParam finds the parameter information spanned by the given range. -func FindParam(pgf *ParsedGoFile, rng protocol.Range) (*ParamInfo, error) { +func FindParam(pgf *parsego.File, rng protocol.Range) (*ParamInfo, error) { start, end, err := pgf.RangePos(rng) if err != nil { return nil, err @@ -542,7 +542,7 @@ func remove[T any](s []T, i int) []T { // replaceFileDecl replaces old with new in the file described by pgf. // // TODO(rfindley): generalize, and combine with rewriteSignature. -func replaceFileDecl(pgf *ParsedGoFile, old, new ast.Decl) ([]byte, error) { +func replaceFileDecl(pgf *parsego.File, old, new ast.Decl) ([]byte, error) { i := findDecl(pgf.File, old) if i == -1 { return nil, bug.Errorf("didn't find old declaration") diff --git a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go index c4a7e5f82c8..53684a58c79 100644 --- a/gopls/internal/golang/code_lens.go +++ b/gopls/internal/golang/code_lens.go @@ -13,6 +13,7 @@ import ( "strings" "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/protocol" "golang.org/x/tools/gopls/internal/protocol/command" @@ -66,7 +67,7 @@ func runTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand } if len(fns.Benchmarks) > 0 { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -98,7 +99,7 @@ type TestFns struct { Benchmarks []TestFn } -func TestsAndBenchmarks(pkg *cache.Package, pgf *ParsedGoFile) (TestFns, error) { +func TestsAndBenchmarks(pkg *cache.Package, pgf *parsego.File) (TestFns, error) { var out TestFns if !strings.HasSuffix(pgf.URI.Path(), "_test.go") { @@ -167,7 +168,7 @@ func matchTestFunc(fn *ast.FuncDecl, pkg *cache.Package, nameRe *regexp.Regexp, } func goGenerateCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -201,7 +202,7 @@ func goGenerateCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.H } func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -227,7 +228,7 @@ func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Ha } func toggleDetailsCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 5c6163cef12..df4ca513ce2 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -34,7 +34,7 @@ func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, // Code actions requiring syntax information alone. if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] { - pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -179,7 +179,7 @@ func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) } // getExtractCodeActions returns any refactor.extract code actions for the selection. -func getExtractCodeActions(pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { +func getExtractCodeActions(pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { if rng.Start == rng.End { return nil, nil } @@ -253,7 +253,7 @@ func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Com } // getRewriteCodeActions returns refactor.rewrite code actions available at the specified range. -func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { +func getRewriteCodeActions(pkg *cache.Package, 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. // @@ -344,7 +344,7 @@ func getRewriteCodeActions(pkg *cache.Package, pgf *ParsedGoFile, fh file.Handle // // (Note that the unusedparam analyzer also computes this property, but // much more precisely, allowing it to report its findings as diagnostics.) -func canRemoveParameter(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) bool { +func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) bool { info, err := FindParam(pgf, rng) if err != nil { return false // e.g. invalid range @@ -381,7 +381,7 @@ func canRemoveParameter(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Rang } // getInlineCodeActions returns refactor.inline actions available at the specified range. -func getInlineCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { +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 @@ -411,7 +411,7 @@ func getInlineCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Ra } // getGoTestCodeActions returns any "run this test/benchmark" code actions for the selection. -func getGoTestCodeActions(pkg *cache.Package, pgf *ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) { +func getGoTestCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) { fns, err := TestsAndBenchmarks(pkg, pgf) if err != nil { return nil, err diff --git a/gopls/internal/golang/completion/definition.go b/gopls/internal/golang/completion/definition.go index 1e3852bffdb..fc8b0ae5c69 100644 --- a/gopls/internal/golang/completion/definition.go +++ b/gopls/internal/golang/completion/definition.go @@ -11,7 +11,7 @@ import ( "unicode" "unicode/utf8" - "golang.org/x/tools/gopls/internal/golang" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" ) @@ -21,7 +21,7 @@ import ( // BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F) // path[0] is known to be *ast.Ident -func definition(path []ast.Node, obj types.Object, pgf *golang.ParsedGoFile) ([]CompletionItem, *Selection) { +func definition(path []ast.Node, obj types.Object, pgf *parsego.File) ([]CompletionItem, *Selection) { if _, ok := obj.(*types.Func); !ok { return nil, nil // not a function at all } diff --git a/gopls/internal/golang/completion/package.go b/gopls/internal/golang/completion/package.go index 709d65ce3a7..12d4ff0be36 100644 --- a/gopls/internal/golang/completion/package.go +++ b/gopls/internal/golang/completion/package.go @@ -19,6 +19,7 @@ import ( "unicode" "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" "golang.org/x/tools/gopls/internal/protocol" @@ -32,7 +33,7 @@ func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh // We know that the AST for this file will be empty due to the missing // package declaration, but parse it anyway to get a mapper. // TODO(adonovan): opt: there's no need to parse just to get a mapper. - pgf, err := snapshot.ParseGo(ctx, fh, golang.ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, nil, err } @@ -68,7 +69,7 @@ func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh // packageCompletionSurrounding returns surrounding for package completion if a // package completions can be suggested at a given cursor offset. A valid location // for package completion is above any declarations or import statements. -func packageCompletionSurrounding(pgf *golang.ParsedGoFile, offset int) (*Selection, error) { +func packageCompletionSurrounding(pgf *parsego.File, offset int) (*Selection, error) { m := pgf.Mapper // If the file lacks a package declaration, the parser will return an empty // AST. As a work-around, try to parse an expression from the file contents. diff --git a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go index 4cc522978e6..c3d791c9443 100644 --- a/gopls/internal/golang/definition.go +++ b/gopls/internal/golang/definition.go @@ -130,7 +130,7 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object } var ( - pgf *ParsedGoFile + pgf *parsego.File decl ast.Node err error ) @@ -148,7 +148,7 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object if err != nil { return nil, nil, err } - pgf, err = snapshot.ParseGo(ctx, fh, ParseFull&^parser.SkipObjectResolution) + pgf, err = snapshot.ParseGo(ctx, fh, parsego.Full&^parser.SkipObjectResolution) if err != nil { return nil, nil, err } @@ -202,7 +202,7 @@ func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object // TODO(rfindley): this function exists to preserve the pre-existing behavior // of golang.Identifier. Eliminate this helper in favor of sharing // functionality with objectsAt, after choosing suitable primitives. -func referencedObject(pkg *cache.Package, pgf *ParsedGoFile, pos token.Pos) (*ast.Ident, types.Object, types.Type) { +func referencedObject(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (*ast.Ident, types.Object, types.Type) { path := pathEnclosingObjNode(pgf.File, pos) if len(path) == 0 { return nil, nil, nil @@ -242,7 +242,7 @@ func referencedObject(pkg *cache.Package, pgf *ParsedGoFile, pos token.Pos) (*as // import spec containing pos. // // If pos is not inside an import spec, it returns nil, nil. -func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, pos token.Pos) ([]protocol.Location, error) { +func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) { var imp *ast.ImportSpec for _, spec := range pgf.File.Imports { // We use "<= End" to accept a query immediately after an ImportSpec. @@ -273,7 +273,7 @@ func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package } continue } - pgf, err := s.ParseGo(ctx, fh, ParseHeader) + pgf, err := s.ParseGo(ctx, fh, parsego.Header) if err != nil { if ctx.Err() != nil { return nil, ctx.Err() diff --git a/gopls/internal/golang/folding_range.go b/gopls/internal/golang/folding_range.go index c856f0a1184..85faea5e31a 100644 --- a/gopls/internal/golang/folding_range.go +++ b/gopls/internal/golang/folding_range.go @@ -12,6 +12,7 @@ import ( "strings" "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/protocol" "golang.org/x/tools/gopls/internal/util/bug" @@ -28,7 +29,7 @@ type FoldingRangeInfo struct { func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) { // TODO(suzmue): consider limiting the number of folding ranges returned, and // implement a way to prioritize folding ranges in that case. - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -68,7 +69,7 @@ func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, } // foldingRangeFunc calculates the line folding range for ast.Node n -func foldingRangeFunc(pgf *ParsedGoFile, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { +func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { // TODO(suzmue): include trailing empty lines before the closing // parenthesis/brace. var kind protocol.FoldingRangeKind @@ -165,7 +166,7 @@ func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Po // commentsFoldingRange returns the folding ranges for all comment blocks in file. // The folding range starts at the end of the first line of the comment block, and ends at the end of the // comment block and has kind protocol.Comment. -func commentsFoldingRange(pgf *ParsedGoFile) (comments []*FoldingRangeInfo) { +func commentsFoldingRange(pgf *parsego.File) (comments []*FoldingRangeInfo) { tokFile := pgf.Tok for _, commentGrp := range pgf.File.Comments { startGrpLine, endGrpLine := safetoken.Line(tokFile, commentGrp.Pos()), safetoken.Line(tokFile, commentGrp.End()) diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index 2eb2b8b01e4..3e5668b32fe 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go @@ -18,6 +18,7 @@ import ( "text/scanner" "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/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" @@ -37,7 +38,7 @@ func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]pr return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Path()) } - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } @@ -111,7 +112,7 @@ type importFix struct { // In addition to returning the result of applying all edits, // it returns a list of fixes that could be applied to the file, with the // corresponding TextEdits that would be needed to apply that fix. -func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { +func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { ctx, done := event.Start(ctx, "golang.AllImportsFixes") defer done() @@ -126,7 +127,7 @@ func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedG // computeImportEdits computes a set of edits that perform one or all of the // necessary import fixes. -func computeImportEdits(ctx context.Context, pgf *ParsedGoFile, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { +func computeImportEdits(ctx context.Context, pgf *parsego.File, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { filename := pgf.URI.Path() // Build up basic information about the original file. @@ -156,7 +157,7 @@ func computeImportEdits(ctx context.Context, pgf *ParsedGoFile, options *imports } // ComputeOneImportFixEdits returns text edits for a single import fix. -func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *ParsedGoFile, fix *imports.ImportFix) ([]protocol.TextEdit, error) { +func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *parsego.File, fix *imports.ImportFix) ([]protocol.TextEdit, error) { options := &imports.Options{ LocalPrefix: snapshot.Options().Local, // Defaults. @@ -170,7 +171,7 @@ func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *ParsedGoFile, fix * return computeFixEdits(pgf, options, []*imports.ImportFix{fix}) } -func computeFixEdits(pgf *ParsedGoFile, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { +func computeFixEdits(pgf *parsego.File, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { // trim the original data to match fixedData left, err := importPrefix(pgf.Src) if err != nil { @@ -303,7 +304,7 @@ func scanForCommentEnd(src []byte) int { return 0 } -func computeTextEdits(ctx context.Context, pgf *ParsedGoFile, formatted string) ([]protocol.TextEdit, error) { +func computeTextEdits(ctx context.Context, pgf *parsego.File, formatted string) ([]protocol.TextEdit, error) { _, done := event.Start(ctx, "golang.computeTextEdits") defer done() diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 7e613f77b7b..05cc5d98fd3 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -530,7 +530,7 @@ func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Objec // imp in the file pgf of pkg. // // If we do not have metadata for the hovered import, it returns _ -func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, imp *ast.ImportSpec) (protocol.Range, *hoverJSON, error) { +func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, imp *ast.ImportSpec) (protocol.Range, *hoverJSON, error) { rng, err := pgf.NodeRange(imp.Path) if err != nil { return protocol.Range{}, nil, err @@ -559,7 +559,7 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa } continue } - pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { if ctx.Err() != nil { return protocol.Range{}, nil, ctx.Err() @@ -581,7 +581,7 @@ func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Packa // hoverPackageName computes hover information for the package name of the file // pgf in pkg. -func hoverPackageName(pkg *cache.Package, pgf *ParsedGoFile) (protocol.Range, *hoverJSON, error) { +func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *hoverJSON, error) { var comment *ast.CommentGroup for _, pgf := range pkg.CompiledGoFiles() { if pgf.File.Doc != nil { @@ -609,7 +609,7 @@ func hoverPackageName(pkg *cache.Package, pgf *ParsedGoFile) (protocol.Range, *h // For example, hovering over "\u2211" in "foo \u2211 bar" yields: // // '∑', U+2211, N-ARY SUMMATION -func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverJSON, error) { +func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverJSON, error) { var ( value string // if non-empty, a constant value to format in hover r rune // if non-zero, format a description of this rune in hover @@ -968,7 +968,7 @@ func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSe return nil, 0, err } - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, 0, err } diff --git a/gopls/internal/golang/inline.go b/gopls/internal/golang/inline.go index de2a6ef75ae..f731e6b2120 100644 --- a/gopls/internal/golang/inline.go +++ b/gopls/internal/golang/inline.go @@ -27,7 +27,7 @@ import ( // EnclosingStaticCall returns the innermost function call enclosing // the selected range, along with the callee. -func EnclosingStaticCall(pkg *cache.Package, pgf *ParsedGoFile, start, end token.Pos) (*ast.CallExpr, *types.Func, error) { +func EnclosingStaticCall(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*ast.CallExpr, *types.Func, error) { path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) var call *ast.CallExpr diff --git a/gopls/internal/golang/inline_all.go b/gopls/internal/golang/inline_all.go index f2bab9d6d12..cbb50c3a2ff 100644 --- a/gopls/internal/golang/inline_all.go +++ b/gopls/internal/golang/inline_all.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/refactor/inline" @@ -43,7 +44,7 @@ import ( // // The code below notes where are assumptions are made that only hold true in // the case of parameter removal (annotated with 'Assumption:') -func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *cache.Snapshot, pkg *cache.Package, pgf *ParsedGoFile, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[protocol.DocumentURI][]byte, error) { +func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[protocol.DocumentURI][]byte, error) { // Collect references. var refs []protocol.Location { @@ -100,7 +101,7 @@ func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *ca type fileCalls struct { pkg *cache.Package - pgf *ParsedGoFile + pgf *parsego.File calls []*ast.CallExpr } diff --git a/gopls/internal/golang/linkname.go b/gopls/internal/golang/linkname.go index 2578f8d5485..c340484de57 100644 --- a/gopls/internal/golang/linkname.go +++ b/gopls/internal/golang/linkname.go @@ -13,6 +13,7 @@ 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/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" ) @@ -102,7 +103,7 @@ func parseLinkname(m *protocol.Mapper, pos protocol.Position) (pkgPath, name str // findLinkname searches dependencies of packages containing fh for an object // with linker name matching the given package path and name. -func findLinkname(ctx context.Context, snapshot *cache.Snapshot, pkgPath PackagePath, name string) (*cache.Package, *ParsedGoFile, token.Pos, error) { +func findLinkname(ctx context.Context, snapshot *cache.Snapshot, pkgPath PackagePath, name string) (*cache.Package, *parsego.File, token.Pos, error) { // Typically the linkname refers to a forward dependency // or a reverse dependency, but in general it may refer // to any package that is linked with this one. diff --git a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go index c448830a1d0..53b8174ed43 100644 --- a/gopls/internal/golang/references.go +++ b/gopls/internal/golang/references.go @@ -28,6 +28,7 @@ import ( "golang.org/x/tools/gopls/internal/cache" "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/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" @@ -164,7 +165,7 @@ func packageReferences(ctx context.Context, snapshot *cache.Snapshot, uri protoc if err != nil { return nil, err } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + f, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return nil, err } @@ -193,7 +194,7 @@ func packageReferences(ctx context.Context, snapshot *cache.Snapshot, uri protoc if err != nil { return nil, err } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + f, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return nil, err } @@ -685,7 +686,7 @@ func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Objec // which must belong to m.File. // // Safe for use only by references and implementations. -func mustLocation(pgf *ParsedGoFile, n ast.Node) protocol.Location { +func mustLocation(pgf *parsego.File, n ast.Node) protocol.Location { loc, err := pgf.NodeLocation(n) if err != nil { panic(err) // can't happen in references or implementations diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index 29485413865..c60e5a25240 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -151,7 +151,7 @@ func PrepareRename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, }, nil, nil } -func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile) (*PrepareItem, error) { +func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File) (*PrepareItem, error) { // Does the client support file renaming? fileRenameSupported := false for _, op := range snapshot.Options().SupportedResourceOperations { @@ -812,7 +812,7 @@ func renamePackageClause(ctx context.Context, mp *metadata.Package, snapshot *ca if err != nil { return err } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + f, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return err } @@ -853,7 +853,7 @@ func renameImports(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.P if err != nil { return err } - f, err := snapshot.ParseGo(ctx, fh, ParseHeader) + f, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return err } @@ -1350,7 +1350,7 @@ func (r *docLinkRenamer) update(pgf *parsego.File) (result []diff.Edit, err erro } // docComment returns the doc for an identifier within the specified file. -func docComment(pgf *ParsedGoFile, id *ast.Ident) *ast.CommentGroup { +func docComment(pgf *parsego.File, id *ast.Ident) *ast.CommentGroup { nodes, _ := astutil.PathEnclosingInterval(pgf.File, id.Pos(), id.End()) for _, node := range nodes { switch decl := node.(type) { @@ -1401,7 +1401,7 @@ func docComment(pgf *ParsedGoFile, id *ast.Ident) *ast.CommentGroup { // updatePkgName returns the updates to rename a pkgName in the import spec by // only modifying the package name portion of the import declaration. -func (r *renamer) updatePkgName(pgf *ParsedGoFile, pkgName *types.PkgName) (diff.Edit, error) { +func (r *renamer) updatePkgName(pgf *parsego.File, pkgName *types.PkgName) (diff.Edit, error) { // Modify ImportSpec syntax to add or remove the Name as needed. path, _ := astutil.PathEnclosingInterval(pgf.File, pkgName.Pos(), pkgName.Pos()) if len(path) < 2 { @@ -1428,19 +1428,19 @@ func (r *renamer) updatePkgName(pgf *ParsedGoFile, pkgName *types.PkgName) (diff // whether the position ppos lies within it. // // Note: also used by references. -func parsePackageNameDecl(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, ppos protocol.Position) (*ParsedGoFile, bool, error) { - pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) +func parsePackageNameDecl(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, ppos protocol.Position) (*parsego.File, bool, error) { + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return nil, false, err } - // Careful: because we used ParseHeader, + // Careful: because we used parsego.Header, // pgf.Pos(ppos) may be beyond EOF => (0, err). pos, _ := pgf.PositionPos(ppos) return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil } // enclosingFile returns the CompiledGoFile of pkg that contains the specified position. -func enclosingFile(pkg *cache.Package, pos token.Pos) (*ParsedGoFile, bool) { +func enclosingFile(pkg *cache.Package, pos token.Pos) (*parsego.File, bool) { for _, pgf := range pkg.CompiledGoFiles() { if pgf.File.Pos() <= pos && pos <= pgf.File.End() { return pgf, true diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index 181b8ce33ab..89845597ffb 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go @@ -21,6 +21,7 @@ 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/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/semtok" @@ -91,7 +92,7 @@ type tokenVisitor struct { ctx context.Context // for event logging // metadataSource is used to resolve imports metadataSource metadata.Source - pgf *ParsedGoFile + pgf *parsego.File start, end token.Pos // range of interest ti *types.Info pkg *cache.Package diff --git a/gopls/internal/golang/snapshot.go b/gopls/internal/golang/snapshot.go index adcbfb9a811..c381c962d08 100644 --- a/gopls/internal/golang/snapshot.go +++ b/gopls/internal/golang/snapshot.go @@ -44,7 +44,7 @@ func NarrowestMetadataForFile(ctx context.Context, snapshot *cache.Snapshot, uri // // Type-checking is expensive. Call snapshot.ParseGo if all you need is a parse // tree, or snapshot.MetadataForFile if you only need metadata. -func NarrowestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *ParsedGoFile, error) { +func NarrowestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *parsego.File, error) { return selectPackageForFile(ctx, snapshot, uri, func(metas []*metadata.Package) *metadata.Package { return metas[0] }) } @@ -62,11 +62,11 @@ func NarrowestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri // // Type-checking is expensive. Call snapshot.ParseGo if all you need is a parse // tree, or snapshot.MetadataForFile if you only need metadata. -func WidestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *ParsedGoFile, error) { +func WidestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *parsego.File, error) { return selectPackageForFile(ctx, snapshot, uri, func(metas []*metadata.Package) *metadata.Package { return metas[len(metas)-1] }) } -func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, selector func([]*metadata.Package) *metadata.Package) (*cache.Package, *ParsedGoFile, error) { +func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, selector func([]*metadata.Package) *metadata.Package) (*cache.Package, *parsego.File, error) { mps, err := snapshot.MetadataForFile(ctx, uri) if err != nil { return nil, nil, err @@ -88,13 +88,6 @@ func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri pro return pkg, pgf, err } -type ParsedGoFile = parsego.File - -const ( - ParseHeader = parsego.ParseHeader - ParseFull = parsego.ParseFull -) - type ( PackageID = metadata.PackageID PackagePath = metadata.PackagePath diff --git a/gopls/internal/golang/symbols.go b/gopls/internal/golang/symbols.go index 390b8275183..35959c2de7a 100644 --- a/gopls/internal/golang/symbols.go +++ b/gopls/internal/golang/symbols.go @@ -12,6 +12,7 @@ import ( "go/types" "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/protocol" "golang.org/x/tools/internal/event" @@ -21,7 +22,7 @@ func DocumentSymbols(ctx context.Context, snapshot *cache.Snapshot, fh file.Hand ctx, done := event.Start(ctx, "golang.DocumentSymbols") defer done() - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err) } diff --git a/gopls/internal/golang/util.go b/gopls/internal/golang/util.go index d6e71a964bb..5283a5b6207 100644 --- a/gopls/internal/golang/util.go +++ b/gopls/internal/golang/util.go @@ -15,6 +15,7 @@ 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/protocol" "golang.org/x/tools/gopls/internal/util/astutil" "golang.org/x/tools/gopls/internal/util/bug" @@ -33,7 +34,7 @@ func IsGenerated(ctx context.Context, snapshot *cache.Snapshot, uri protocol.Doc if err != nil { return false } - pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return false } diff --git a/gopls/internal/protocol/mapper.go b/gopls/internal/protocol/mapper.go index 69547979224..d1bd957a9e5 100644 --- a/gopls/internal/protocol/mapper.go +++ b/gopls/internal/protocol/mapper.go @@ -375,7 +375,7 @@ func (m *Mapper) NodeMappedRange(tf *token.File, node ast.Node) (MappedRange, er // // Construct one by calling Mapper.OffsetMappedRange with start/end offsets. // From the go/token domain, call safetoken.Offsets first, -// or use a helper such as ParsedGoFile.MappedPosRange. +// or use a helper such as parsego.File.MappedPosRange. // // Two MappedRanges produced the same Mapper are equal if and only if they // denote the same range. Two MappedRanges produced by different Mappers diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 8681db7e0ac..19ea884f45d 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -814,7 +814,7 @@ func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) ( if err != nil { return err } - pgf, err := deps.snapshot.ParseGo(ctx, fh, parsego.ParseHeader) + pgf, err := deps.snapshot.ParseGo(ctx, fh, parsego.Header) if err != nil { return err } diff --git a/gopls/internal/server/link.go b/gopls/internal/server/link.go index 6ac397627d9..c0a60f22601 100644 --- a/gopls/internal/server/link.go +++ b/gopls/internal/server/link.go @@ -109,7 +109,7 @@ func modLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([] // goLinks returns the set of hyperlink annotations for the specified Go file. func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentLink, error) { - pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } diff --git a/gopls/internal/server/selection_range.go b/gopls/internal/server/selection_range.go index 89142f42007..042812217f3 100644 --- a/gopls/internal/server/selection_range.go +++ b/gopls/internal/server/selection_range.go @@ -40,7 +40,7 @@ func (s *server) SelectionRange(ctx context.Context, params *protocol.SelectionR return nil, fmt.Errorf("SelectionRange not supported for file of type %s", kind) } - pgf, err := snapshot.ParseGo(ctx, fh, parsego.ParseFull) + pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) if err != nil { return nil, err } From 2bd794900741d0a1d0318443b9c428229dc1a4d9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 15 Feb 2024 14:17:27 -0500 Subject: [PATCH 14/47] x/tools: don't parse CombinedOutput This change fixes a bug in internal/testenv in which it would gather the combined output (2>&1) of the "go env" command and parse it as an environment variable. However, certain environment variables (e.g. GODEBUG) cause "go env" to log to stderr, so that the parser reads garbage. Use Output instead. Also, preemptively fix a number of similar occurrences in x/tools. CombinedOutput should be used only when the whole output is ultimately sent to stderr or a log for human eyes, or for tests that look for specific error messages in the unstructured combined log. In those cases, the scope of the 'out' variable can be reduced to avoid temptation. Fixes golang/go#65729 Change-Id: Ifc0fd494fcde0e339bb5283e39c7696a34f5a699 . Change-Id: I6eadd0e76498dc5f4d91e0904af2d52e610df683 Reviewed-on: https://go-review.googlesource.com/c/tools/+/564516 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/analysis/unitchecker/unitchecker_test.go | 7 +++++++ go/gcexportdata/gcexportdata.go | 2 +- go/internal/cgo/cgo_pkgconfig.go | 2 +- go/internal/gccgoimporter/importer_test.go | 8 +++----- internal/diff/difftest/difftest_test.go | 2 +- internal/gcimporter/gcimporter_test.go | 3 +-- internal/testenv/testenv.go | 7 +++++-- present/parse_test.go | 2 +- refactor/rename/rename.go | 2 +- 9 files changed, 21 insertions(+), 14 deletions(-) diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go index 9f41c71f9a3..3611d354ca3 100644 --- a/go/analysis/unitchecker/unitchecker_test.go +++ b/go/analysis/unitchecker/unitchecker_test.go @@ -169,6 +169,13 @@ func _() { cmd.Env = append(exported.Config.Env, "ENTRYPOINT=minivet") cmd.Dir = exported.Config.Dir + // TODO(golang/go#65729): this is unsound: any extra + // logging by the child process (e.g. due to GODEBUG + // options) will add noise to stderr, causing the + // CombinedOutput to be unparseable as JSON. But we + // can't simply use Output here as some of the tests + // look for substrings of stderr. Rework the test to + // be specific about which output stream to match. out, err := cmd.CombinedOutput() exitcode := 0 if exitErr, ok := err.(*exec.ExitError); ok { diff --git a/go/gcexportdata/gcexportdata.go b/go/gcexportdata/gcexportdata.go index 03543bd4bb8..137cc8df1d8 100644 --- a/go/gcexportdata/gcexportdata.go +++ b/go/gcexportdata/gcexportdata.go @@ -47,7 +47,7 @@ import ( func Find(importPath, srcDir string) (filename, path string) { cmd := exec.Command("go", "list", "-json", "-export", "--", importPath) cmd.Dir = srcDir - out, err := cmd.CombinedOutput() + out, err := cmd.Output() if err != nil { return "", "" } diff --git a/go/internal/cgo/cgo_pkgconfig.go b/go/internal/cgo/cgo_pkgconfig.go index b5bb95a63e5..4f0d3cf4338 100644 --- a/go/internal/cgo/cgo_pkgconfig.go +++ b/go/internal/cgo/cgo_pkgconfig.go @@ -15,7 +15,7 @@ import ( // pkgConfig runs pkg-config with the specified arguments and returns the flags it prints. func pkgConfig(mode string, pkgs []string) (flags []string, err error) { cmd := exec.Command("pkg-config", append([]string{mode}, pkgs...)...) - out, err := cmd.CombinedOutput() + out, err := cmd.Output() if err != nil { s := fmt.Sprintf("%s failed: %v", strings.Join(cmd.Args, " "), err) if len(out) > 0 { diff --git a/go/internal/gccgoimporter/importer_test.go b/go/internal/gccgoimporter/importer_test.go index 7adffd0df80..3014c6206d7 100644 --- a/go/internal/gccgoimporter/importer_test.go +++ b/go/internal/gccgoimporter/importer_test.go @@ -140,7 +140,7 @@ func TestObjImporter(t *testing.T) { t.Skip("no support yet for debug/xcoff") } - verout, err := exec.Command(gpath, "--version").CombinedOutput() + verout, err := exec.Command(gpath, "--version").Output() if err != nil { t.Logf("%s", verout) t.Fatal(err) @@ -182,8 +182,7 @@ func TestObjImporter(t *testing.T) { afile := filepath.Join(artmpdir, "lib"+test.pkgpath+".a") cmd := exec.Command(gpath, "-fgo-pkgpath="+test.pkgpath, "-c", "-o", ofile, gofile) - out, err := cmd.CombinedOutput() - if err != nil { + if out, err := cmd.CombinedOutput(); err != nil { t.Logf("%s", out) t.Fatalf("gccgo %s failed: %s", gofile, err) } @@ -191,8 +190,7 @@ func TestObjImporter(t *testing.T) { runImporterTest(t, imp, initmap, &test) cmd = exec.Command("ar", "cr", afile, ofile) - out, err = cmd.CombinedOutput() - if err != nil { + if out, err := cmd.CombinedOutput(); err != nil { t.Logf("%s", out) t.Fatalf("ar cr %s %s failed: %s", afile, ofile, err) } diff --git a/internal/diff/difftest/difftest_test.go b/internal/diff/difftest/difftest_test.go index c64a0fa0c9f..bd98d7304d3 100644 --- a/internal/diff/difftest/difftest_test.go +++ b/internal/diff/difftest/difftest_test.go @@ -64,7 +64,7 @@ func getDiffOutput(a, b string) (string, error) { } cmd := exec.Command("diff", "-u", fileA.Name(), fileB.Name()) cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8") - out, err := cmd.CombinedOutput() + out, err := cmd.Output() if err != nil { if _, ok := err.(*exec.ExitError); !ok { return "", fmt.Errorf("failed to run diff -u %v %v: %v\n%v", fileA.Name(), fileB.Name(), err, string(out)) diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 3af088b23d8..84f99400bcf 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -84,8 +84,7 @@ func compilePkg(t *testing.T, dirname, filename, outdirname string, packagefiles importreldir := strings.ReplaceAll(outdirname, string(os.PathSeparator), "/") cmd := exec.Command("go", "tool", "compile", "-p", pkg, "-D", importreldir, "-importcfg", importcfgfile, "-o", outname, filename) cmd.Dir = dirname - out, err := cmd.CombinedOutput() - if err != nil { + if out, err := cmd.CombinedOutput(); err != nil { t.Logf("%s", out) t.Fatalf("go tool compile %s failed: %s", filename, err) } diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index 511da9d7e85..0360d169b50 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -83,7 +83,7 @@ func hasTool(tool string) error { // GOROOT. Otherwise, 'some/path/go test ./...' will test against some // version of the 'go' binary other than 'some/path/go', which is almost // certainly not what the user intended. - out, err := exec.Command(tool, "env", "GOROOT").CombinedOutput() + out, err := exec.Command(tool, "env", "GOROOT").Output() if err != nil { checkGoBuild.err = err return @@ -141,7 +141,7 @@ func cgoEnabled(bypassEnvironment bool) (bool, error) { if bypassEnvironment { cmd.Env = append(append([]string(nil), os.Environ()...), "CGO_ENABLED=") } - out, err := cmd.CombinedOutput() + out, err := cmd.Output() if err != nil { return false, err } @@ -199,6 +199,9 @@ func NeedsTool(t testing.TB, tool string) { t.Helper() if allowMissingTool(tool) { + // TODO(adonovan): might silently skipping because of + // (e.g.) mismatched go env GOROOT and runtime.GOROOT + // risk some users not getting the coverage they expect? t.Skipf("skipping because %s tool not available: %v", tool, err) } else { t.Fatalf("%s tool not available: %v", tool, err) diff --git a/present/parse_test.go b/present/parse_test.go index 0e59857a3a0..4351c368201 100644 --- a/present/parse_test.go +++ b/present/parse_test.go @@ -79,7 +79,7 @@ func diff(prefix string, name1 string, b1 []byte, name2 string, b2 []byte) ([]by cmd = "/bin/ape/diff" } - data, err := exec.Command(cmd, "-u", f1, f2).CombinedOutput() + data, err := exec.Command(cmd, "-u", f1, f2).Output() if len(data) > 0 { // diff exits with a non-zero status when the files don't match. // Ignore that failure as long as we get output. diff --git a/refactor/rename/rename.go b/refactor/rename/rename.go index db925ca57b2..5415f2e8e82 100644 --- a/refactor/rename/rename.go +++ b/refactor/rename/rename.go @@ -588,7 +588,7 @@ func diff(filename string, content []byte) error { } defer os.Remove(renamed) - diff, err := exec.Command(DiffCmd, "-u", filename, renamed).CombinedOutput() + diff, err := exec.Command(DiffCmd, "-u", filename, renamed).Output() if len(diff) > 0 { // diff exits with a non-zero status when the files don't match. // Ignore that failure as long as we get output. From 0d171942e7f9774685ff075f68f434100f39e133 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 3 Feb 2024 09:10:23 -0500 Subject: [PATCH 15/47] gopls/internal/golang: SemanticTokens: edits for clarity This CL applies a number of readability enhancements: - eliminate unnecessary local variables - reorder and document struct fields - strength-reduce cache.Package to metadata.Package - more conventional variable names. e.g. items -> tokens lng -> length ti -> info x -> id y -> obj, ancestor nd -> parent def/use -> obj - rename unexpected() to errorf(), with printf semantics - remove unreachable cases (e.g. info == nil; impossible types.Object subtypes) - use is[T] to eliminate statements - eta-reduce inspector function - add doc comments - add TODOs for further simplification and suspected bugs. This is a pure refactoring; no behavior changes are intended. Change-Id: I97a8f0b76008b0f784328f155f41bea8799f8389 Reviewed-on: https://go-review.googlesource.com/c/tools/+/561056 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/internal/golang/semtok.go | 618 +++++++++++++++----------------- 1 file changed, 296 insertions(+), 322 deletions(-) diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index 89845597ffb..268459219f2 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go @@ -29,17 +29,11 @@ import ( "golang.org/x/tools/internal/event" ) -// to control comprehensive logging of decisions (gopls semtok foo.go > /dev/null shows log output) -// semDebug should NEVER be true in checked-in code +// semDebug enables comprehensive logging of decisions +// (gopls semtok foo.go > /dev/null shows log output). +// It should never be true in checked-in code. const semDebug = false -// The LSP says that errors for the semantic token requests should only be returned -// for exceptions (a word not otherwise defined). This code treats a too-large file -// as an exception. On parse errors, the code does what it can. - -// reject full semantic token requests for large files -const maxFullFileSize int = 100000 - func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng *protocol.Range) (*protocol.SemanticTokens, error) { pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) if err != nil { @@ -58,64 +52,69 @@ func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handl tok := pgf.Tok start, end = tok.Pos(0), tok.Pos(tok.Size()) // entire file } + + // Reject full semantic token requests for large files. + // + // The LSP says that errors for the semantic token requests + // should only be returned for exceptions (a word not + // otherwise defined). This code treats a too-large file as an + // exception. On parse errors, the code does what it can. + const maxFullFileSize = 100000 if int(end-start) > maxFullFileSize { - err := fmt.Errorf("semantic tokens: range %s too large (%d > %d)", + return nil, fmt.Errorf("semantic tokens: range %s too large (%d > %d)", fh.URI().Path(), end-start, maxFullFileSize) - return nil, err } - tv := &tokenVisitor{ + tv := tokenVisitor{ ctx: ctx, metadataSource: snapshot, + metadata: pkg.Metadata(), + info: pkg.GetTypesInfo(), + fset: pkg.FileSet(), pgf: pgf, start: start, end: end, - ti: pkg.GetTypesInfo(), - pkg: pkg, - fset: pkg.FileSet(), } tv.visit() return &protocol.SemanticTokens{ Data: semtok.Encode( - tv.items, + tv.tokens, snapshot.Options().NoSemanticString, snapshot.Options().NoSemanticNumber, snapshot.Options().SemanticTypes, snapshot.Options().SemanticMods), - // For delta requests, but we've never seen any. - ResultID: time.Now().String(), + ResultID: time.Now().String(), // for delta requests, but we've never seen any }, nil } type tokenVisitor struct { - items []semtok.Token - ctx context.Context // for event logging - // metadataSource is used to resolve imports - metadataSource metadata.Source + // inputs + ctx context.Context // for event logging + metadataSource metadata.Source // used to resolve imports + metadata *metadata.Package + info *types.Info + fset *token.FileSet pgf *parsego.File start, end token.Pos // range of interest - ti *types.Info - pkg *cache.Package - fset *token.FileSet - // path from the root of the parse tree, used for debugging - stack []ast.Node + + // working state + stack []ast.Node // path from root of the syntax tree + tokens []semtok.Token // computed sequence of semantic tokens } func (tv *tokenVisitor) visit() { f := tv.pgf.File // may not be in range, but harmless tv.token(f.Package, len("package"), semtok.TokKeyword, nil) - tv.token(f.Name.NamePos, len(f.Name.Name), semtok.TokNamespace, nil) - inspect := func(n ast.Node) bool { - return tv.inspector(n) - } - for _, d := range f.Decls { - // only look at the decls that overlap the range - start, end := d.Pos(), d.End() - if end <= tv.start || start >= tv.end { + if f.Name != nil { + tv.token(f.Name.NamePos, len(f.Name.Name), semtok.TokNamespace, nil) + } + for _, decl := range f.Decls { + // Only look at the decls that overlap the range. + if decl.End() <= tv.start || decl.Pos() >= tv.end { continue } - ast.Inspect(d, inspect) + ast.Inspect(decl, tv.inspect) } for _, cg := range f.Comments { for _, c := range cg.List { @@ -125,68 +124,70 @@ func (tv *tokenVisitor) visit() { } if !strings.Contains(c.Text, "\n") { tv.token(c.Pos(), len(c.Text), semtok.TokComment, nil) - continue + } else { + tv.multiline(c.Pos(), c.End(), semtok.TokComment) } - tv.multiline(c.Pos(), c.End(), c.Text, semtok.TokComment) } } } -func (tv *tokenVisitor) token(start token.Pos, leng int, typ semtok.TokenType, mods []string) { - if leng <= 0 { +// token emits a token of the specified extent and semantics. +func (tv *tokenVisitor) token(start token.Pos, length int, typ semtok.TokenType, modifiers []string) { + if length <= 0 { return // vscode doesn't like 0-length Tokens } if !start.IsValid() { // This is not worth reporting. TODO(pjw): does it still happen? return } - if start >= tv.end || start+token.Pos(leng) <= tv.start { + end := start + token.Pos(length) + if start >= tv.end || end <= tv.start { return } // want a line and column from start (in LSP coordinates). Ignore line directives. - lspRange, err := tv.pgf.PosRange(start, start+token.Pos(leng)) + rng, err := tv.pgf.PosRange(start, end) if err != nil { event.Error(tv.ctx, "failed to convert to range", err) return } - if lspRange.End.Line != lspRange.Start.Line { + if rng.End.Line != rng.Start.Line { // this happens if users are typing at the end of the file, but report nothing return } - tv.items = append(tv.items, semtok.Token{ - Line: lspRange.Start.Line, - Start: lspRange.Start.Character, - Len: lspRange.End.Character - lspRange.Start.Character, // all on one line + tv.tokens = append(tv.tokens, semtok.Token{ + Line: rng.Start.Line, + Start: rng.Start.Character, + Len: rng.End.Character - rng.Start.Character, // (on same line) Type: typ, - Modifiers: mods, + Modifiers: modifiers, }) } -// convert the stack to a string, for debugging +// strStack converts the stack to a string, for debugging and error messages. func (tv *tokenVisitor) strStack() string { msg := []string{"["} for i := len(tv.stack) - 1; i >= 0; i-- { - s := tv.stack[i] - msg = append(msg, fmt.Sprintf("%T", s)[5:]) + n := tv.stack[i] + msg = append(msg, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) } if len(tv.stack) > 0 { - loc := tv.stack[len(tv.stack)-1].Pos() - if _, err := safetoken.Offset(tv.pgf.Tok, loc); err != nil { - msg = append(msg, fmt.Sprintf("invalid position %v for %s", loc, tv.pgf.URI)) + pos := tv.stack[len(tv.stack)-1].Pos() + if _, err := safetoken.Offset(tv.pgf.Tok, pos); err != nil { + msg = append(msg, fmt.Sprintf("invalid position %v for %s", pos, tv.pgf.URI)) } else { - add := safetoken.Position(tv.pgf.Tok, loc) - nm := filepath.Base(add.Filename) - msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column)) + posn := safetoken.Position(tv.pgf.Tok, pos) + msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", + filepath.Base(posn.Filename), posn.Line, posn.Column)) } } msg = append(msg, "]") return strings.Join(msg, " ") } -// find the line in the source -func (tv *tokenVisitor) srcLine(x ast.Node) string { +// srcLine returns the source text for n (truncated at first newline). +func (tv *tokenVisitor) srcLine(n ast.Node) string { file := tv.pgf.Tok - line := safetoken.Line(file, x.Pos()) + line := safetoken.Line(file, n.Pos()) start, err := safetoken.Offset(file, file.LineStart(line)) if err != nil { return "" @@ -195,274 +196,256 @@ func (tv *tokenVisitor) srcLine(x ast.Node) string { for ; end < len(tv.pgf.Src) && tv.pgf.Src[end] != '\n'; end++ { } - ans := tv.pgf.Src[start:end] - return string(ans) + return string(tv.pgf.Src[start:end]) } -func (tv *tokenVisitor) inspector(n ast.Node) bool { - pop := func() { - tv.stack = tv.stack[:len(tv.stack)-1] - } +func (tv *tokenVisitor) inspect(n ast.Node) (descend bool) { if n == nil { - pop() + tv.stack = tv.stack[:len(tv.stack)-1] // pop return true } - tv.stack = append(tv.stack, n) - switch x := n.(type) { + tv.stack = append(tv.stack, n) // push + defer func() { + if !descend { + tv.stack = tv.stack[:len(tv.stack)-1] // pop + } + }() + + switch n := n.(type) { case *ast.ArrayType: case *ast.AssignStmt: - tv.token(x.TokPos, len(x.Tok.String()), semtok.TokOperator, nil) + tv.token(n.TokPos, len(n.Tok.String()), semtok.TokOperator, nil) case *ast.BasicLit: - if strings.Contains(x.Value, "\n") { + if strings.Contains(n.Value, "\n") { // has to be a string. - tv.multiline(x.Pos(), x.End(), x.Value, semtok.TokString) + tv.multiline(n.Pos(), n.End(), semtok.TokString) break } - ln := len(x.Value) what := semtok.TokNumber - if x.Kind == token.STRING { + if n.Kind == token.STRING { what = semtok.TokString } - tv.token(x.Pos(), ln, what, nil) + tv.token(n.Pos(), len(n.Value), what, nil) case *ast.BinaryExpr: - tv.token(x.OpPos, len(x.Op.String()), semtok.TokOperator, nil) + tv.token(n.OpPos, len(n.Op.String()), semtok.TokOperator, nil) case *ast.BlockStmt: case *ast.BranchStmt: - tv.token(x.TokPos, len(x.Tok.String()), semtok.TokKeyword, nil) + tv.token(n.TokPos, len(n.Tok.String()), semtok.TokKeyword, nil) // There's no semantic encoding for labels case *ast.CallExpr: - if x.Ellipsis != token.NoPos { - tv.token(x.Ellipsis, len("..."), semtok.TokOperator, nil) + if n.Ellipsis.IsValid() { + tv.token(n.Ellipsis, len("..."), semtok.TokOperator, nil) } case *ast.CaseClause: iam := "case" - if x.List == nil { + if n.List == nil { iam = "default" } - tv.token(x.Case, len(iam), semtok.TokKeyword, nil) + tv.token(n.Case, len(iam), semtok.TokKeyword, nil) case *ast.ChanType: // chan | chan <- | <- chan switch { - case x.Arrow == token.NoPos: - tv.token(x.Begin, len("chan"), semtok.TokKeyword, nil) - case x.Arrow == x.Begin: - tv.token(x.Arrow, 2, semtok.TokOperator, nil) - pos := tv.findKeyword("chan", x.Begin+2, x.Value.Pos()) + case n.Arrow == token.NoPos: + tv.token(n.Begin, len("chan"), semtok.TokKeyword, nil) + case n.Arrow == n.Begin: + tv.token(n.Arrow, 2, semtok.TokOperator, nil) + pos := tv.findKeyword("chan", n.Begin+2, n.Value.Pos()) tv.token(pos, len("chan"), semtok.TokKeyword, nil) - case x.Arrow != x.Begin: - tv.token(x.Begin, len("chan"), semtok.TokKeyword, nil) - tv.token(x.Arrow, 2, semtok.TokOperator, nil) + case n.Arrow != n.Begin: + tv.token(n.Begin, len("chan"), semtok.TokKeyword, nil) + tv.token(n.Arrow, 2, semtok.TokOperator, nil) } case *ast.CommClause: - iam := len("case") - if x.Comm == nil { - iam = len("default") + length := len("case") + if n.Comm == nil { + length = len("default") } - tv.token(x.Case, iam, semtok.TokKeyword, nil) + tv.token(n.Case, length, semtok.TokKeyword, nil) case *ast.CompositeLit: case *ast.DeclStmt: case *ast.DeferStmt: - tv.token(x.Defer, len("defer"), semtok.TokKeyword, nil) + tv.token(n.Defer, len("defer"), semtok.TokKeyword, nil) case *ast.Ellipsis: - tv.token(x.Ellipsis, len("..."), semtok.TokOperator, nil) + tv.token(n.Ellipsis, len("..."), semtok.TokOperator, nil) case *ast.EmptyStmt: case *ast.ExprStmt: case *ast.Field: case *ast.FieldList: case *ast.ForStmt: - tv.token(x.For, len("for"), semtok.TokKeyword, nil) + tv.token(n.For, len("for"), semtok.TokKeyword, nil) case *ast.FuncDecl: case *ast.FuncLit: case *ast.FuncType: - if x.Func != token.NoPos { - tv.token(x.Func, len("func"), semtok.TokKeyword, nil) + if n.Func != token.NoPos { + tv.token(n.Func, len("func"), semtok.TokKeyword, nil) } case *ast.GenDecl: - tv.token(x.TokPos, len(x.Tok.String()), semtok.TokKeyword, nil) + tv.token(n.TokPos, len(n.Tok.String()), semtok.TokKeyword, nil) case *ast.GoStmt: - tv.token(x.Go, len("go"), semtok.TokKeyword, nil) + tv.token(n.Go, len("go"), semtok.TokKeyword, nil) case *ast.Ident: - tv.ident(x) + tv.ident(n) case *ast.IfStmt: - tv.token(x.If, len("if"), semtok.TokKeyword, nil) - if x.Else != nil { + tv.token(n.If, len("if"), semtok.TokKeyword, nil) + if n.Else != nil { // x.Body.End() or x.Body.End()+1, not that it matters - pos := tv.findKeyword("else", x.Body.End(), x.Else.Pos()) + pos := tv.findKeyword("else", n.Body.End(), n.Else.Pos()) tv.token(pos, len("else"), semtok.TokKeyword, nil) } case *ast.ImportSpec: - tv.importSpec(x) - pop() + tv.importSpec(n) return false case *ast.IncDecStmt: - tv.token(x.TokPos, len(x.Tok.String()), semtok.TokOperator, nil) + tv.token(n.TokPos, len(n.Tok.String()), semtok.TokOperator, nil) case *ast.IndexExpr: case *ast.IndexListExpr: case *ast.InterfaceType: - tv.token(x.Interface, len("interface"), semtok.TokKeyword, nil) + tv.token(n.Interface, len("interface"), semtok.TokKeyword, nil) case *ast.KeyValueExpr: case *ast.LabeledStmt: case *ast.MapType: - tv.token(x.Map, len("map"), semtok.TokKeyword, nil) + tv.token(n.Map, len("map"), semtok.TokKeyword, nil) case *ast.ParenExpr: case *ast.RangeStmt: - tv.token(x.For, len("for"), semtok.TokKeyword, nil) + tv.token(n.For, len("for"), semtok.TokKeyword, nil) // x.TokPos == token.NoPos is legal (for range foo {}) - offset := x.TokPos + offset := n.TokPos if offset == token.NoPos { - offset = x.For + offset = n.For } - pos := tv.findKeyword("range", offset, x.X.Pos()) + pos := tv.findKeyword("range", offset, n.X.Pos()) tv.token(pos, len("range"), semtok.TokKeyword, nil) case *ast.ReturnStmt: - tv.token(x.Return, len("return"), semtok.TokKeyword, nil) + tv.token(n.Return, len("return"), semtok.TokKeyword, nil) case *ast.SelectStmt: - tv.token(x.Select, len("select"), semtok.TokKeyword, nil) + tv.token(n.Select, len("select"), semtok.TokKeyword, nil) case *ast.SelectorExpr: case *ast.SendStmt: - tv.token(x.Arrow, len("<-"), semtok.TokOperator, nil) + tv.token(n.Arrow, len("<-"), semtok.TokOperator, nil) case *ast.SliceExpr: case *ast.StarExpr: - tv.token(x.Star, len("*"), semtok.TokOperator, nil) + tv.token(n.Star, len("*"), semtok.TokOperator, nil) case *ast.StructType: - tv.token(x.Struct, len("struct"), semtok.TokKeyword, nil) + tv.token(n.Struct, len("struct"), semtok.TokKeyword, nil) case *ast.SwitchStmt: - tv.token(x.Switch, len("switch"), semtok.TokKeyword, nil) + tv.token(n.Switch, len("switch"), semtok.TokKeyword, nil) case *ast.TypeAssertExpr: - if x.Type == nil { - pos := tv.findKeyword("type", x.Lparen, x.Rparen) + if n.Type == nil { + pos := tv.findKeyword("type", n.Lparen, n.Rparen) tv.token(pos, len("type"), semtok.TokKeyword, nil) } case *ast.TypeSpec: case *ast.TypeSwitchStmt: - tv.token(x.Switch, len("switch"), semtok.TokKeyword, nil) + tv.token(n.Switch, len("switch"), semtok.TokKeyword, nil) case *ast.UnaryExpr: - tv.token(x.OpPos, len(x.Op.String()), semtok.TokOperator, nil) + tv.token(n.OpPos, len(n.Op.String()), semtok.TokOperator, nil) case *ast.ValueSpec: // things only seen with parsing or type errors, so ignore them case *ast.BadDecl, *ast.BadExpr, *ast.BadStmt: - return true + return false // not going to see these case *ast.File, *ast.Package: - tv.unexpected(fmt.Sprintf("implement %T %s", x, safetoken.Position(tv.pgf.Tok, x.Pos()))) + tv.errorf("implement %T %s", n, safetoken.Position(tv.pgf.Tok, n.Pos())) // other things we knowingly ignore case *ast.Comment, *ast.CommentGroup: - pop() return false default: - tv.unexpected(fmt.Sprintf("failed to implement %T", x)) + tv.errorf("failed to implement %T", n) } return true } -func (tv *tokenVisitor) ident(x *ast.Ident) { - if tv.ti == nil { - what, mods := tv.unkIdent(x) - if what != "" { - tv.token(x.Pos(), len(x.String()), what, mods) - } - if semDebug { - log.Printf(" nil %s/nil/nil %q %v %s", x.String(), what, mods, tv.strStack()) - } - return - } - def := tv.ti.Defs[x] - if def != nil { - what, mods := tv.definitionFor(x, def) - if what != "" { - tv.token(x.Pos(), len(x.String()), what, mods) - } - if semDebug { - log.Printf(" for %s/%T/%T got %s %v (%s)", x.String(), def, def.Type(), what, mods, tv.strStack()) - } - return - } - use := tv.ti.Uses[x] - tok := func(pos token.Pos, lng int, tok semtok.TokenType, mods []string) { - tv.token(pos, lng, tok, mods) - q := "nil" - if use != nil { - q = fmt.Sprintf("%T", use.Type()) - } +func (tv *tokenVisitor) ident(id *ast.Ident) { + var obj types.Object + + // emit emits a token for the identifier's extent. + emit := func(tok semtok.TokenType, modifiers ...string) { + tv.token(id.Pos(), len(id.Name), tok, modifiers) if semDebug { - log.Printf(" use %s/%T/%s got %s %v (%s)", x.String(), use, q, tok, mods, tv.strStack()) + q := "nil" + if obj != nil { + q = fmt.Sprintf("%T", obj.Type()) // e.g. "*types.Map" + } + log.Printf(" use %s/%T/%s got %s %v (%s)", + id.Name, obj, q, tok, modifiers, tv.strStack()) } } - switch y := use.(type) { - case nil: - what, mods := tv.unkIdent(x) - if what != "" { - tok(x.Pos(), len(x.String()), what, mods) + // definition? + obj = tv.info.Defs[id] + if obj != nil { + if tok, modifiers := tv.definitionFor(id, obj); tok != "" { + emit(tok, modifiers...) } else if semDebug { - // tok() wasn't called, so didn't log - log.Printf(" nil %s/%T/nil %q %v (%s)", x.String(), use, what, mods, tv.strStack()) + log.Printf(" for %s/%T/%T got '' %v (%s)", + id.Name, obj, obj.Type(), modifiers, tv.strStack()) } return + } + + // use? + obj = tv.info.Uses[id] + switch obj := obj.(type) { case *types.Builtin: - tok(x.NamePos, len(x.Name), semtok.TokFunction, []string{"defaultLibrary"}) + emit(semtok.TokFunction, "defaultLibrary") case *types.Const: - mods := []string{"readonly"} - tt := y.Type() - if _, ok := tt.(*types.Basic); ok { - tok(x.Pos(), len(x.String()), semtok.TokVariable, mods) - break - } - if ttx, ok := tt.(*types.Named); ok { - if x.String() == "iota" { - tv.unexpected(fmt.Sprintf("iota:%T", ttx)) + switch t := obj.Type().(type) { + // TODO(adonovan): set defaultLibrary modified for symbols in types.Universe. + case *types.Basic: + emit(semtok.TokVariable, "readonly") + case *types.Named: + if id.Name == "iota" { + // TODO(adonovan): this is almost certainly reachable. + // Add a test and do something sensible, e.g. + // emit(semtok.TokVariable, "readonly", "defaultLibrary") + tv.errorf("iota:%T", t) } - if _, ok := ttx.Underlying().(*types.Basic); ok { - tok(x.Pos(), len(x.String()), semtok.TokVariable, mods) - break + if is[*types.Basic](t.Underlying()) { + emit(semtok.TokVariable, "readonly") + } else { + tv.errorf("%q/%T", id.Name, t) } - tv.unexpected(fmt.Sprintf("%q/%T", x.String(), tt)) + default: + tv.errorf("%s %T %#v", id.Name, t, t) // can't happen } - // can this happen? Don't think so - tv.unexpected(fmt.Sprintf("%s %T %#v", x.String(), tt, tt)) case *types.Func: - tok(x.Pos(), len(x.Name), semtok.TokFunction, nil) + emit(semtok.TokFunction) case *types.Label: // nothing to map it to case *types.Nil: // nil is a predeclared identifier - tok(x.Pos(), len("nil"), semtok.TokVariable, []string{"readonly", "defaultLibrary"}) + emit(semtok.TokVariable, "readonly", "defaultLibrary") case *types.PkgName: - tok(x.Pos(), len(x.Name), semtok.TokNamespace, nil) - case *types.TypeName: // could be a TokTypeParam - var mods []string - if _, ok := y.Type().(*types.Basic); ok { - mods = []string{"defaultLibrary"} - } else if _, ok := y.Type().(*types.TypeParam); ok { - tok(x.Pos(), len(x.String()), semtok.TokTypeParam, mods) - break + emit(semtok.TokNamespace) + case *types.TypeName: // could be a TypeParam + if is[*types.TypeParam](obj.Type()) { + emit(semtok.TokTypeParam) + } else if is[*types.Basic](obj.Type()) { + emit(semtok.TokType, "defaultLibrary") + } else { + emit(semtok.TokType) } - tok(x.Pos(), len(x.String()), semtok.TokType, mods) case *types.Var: - if isSignature(y) { - tok(x.Pos(), len(x.Name), semtok.TokFunction, nil) - } else if tv.isParam(use.Pos()) { + if is[*types.Signature](obj.Type()) { + emit(semtok.TokFunction) + } else if tv.isParam(obj.Pos()) { // variable, unless use.pos is the pos of a Field in an ancestor FuncDecl // or FuncLit and then it's a parameter - tok(x.Pos(), len(x.Name), semtok.TokParameter, nil) + emit(semtok.TokParameter) } else { - tok(x.Pos(), len(x.Name), semtok.TokVariable, nil) - } - - default: - // can't happen - if use == nil { - msg := fmt.Sprintf("%#v %#v %#v", x, tv.ti.Defs[x], tv.ti.Uses[x]) - tv.unexpected(msg) + emit(semtok.TokVariable) } - if use.Type() != nil { - tv.unexpected(fmt.Sprintf("%s %T/%T,%#v", x.String(), use, use.Type(), use)) - } else { - tv.unexpected(fmt.Sprintf("%s %T", x.String(), use)) + case nil: + if tok, modifiers := tv.unkIdent(id); tok != "" { + emit(tok, modifiers...) } + default: + panic(obj) } } +// isParam reports whether the position is that of a parameter name of +// an enclosing function. func (tv *tokenVisitor) isParam(pos token.Pos) bool { for i := len(tv.stack) - 1; i >= 0; i-- { switch n := tv.stack[i].(type) { @@ -487,31 +470,19 @@ func (tv *tokenVisitor) isParam(pos token.Pos) bool { return false } -func isSignature(use types.Object) bool { - if _, ok := use.(*types.Var); !ok { - return false - } - v := use.Type() - if v == nil { - return false - } - if _, ok := v.(*types.Signature); ok { - return true - } - return false -} - -// both tv.ti.Defs and tv.ti.Uses are nil. use the parse stack. -// a lot of these only happen when the package doesn't compile -// but in that case it is all best-effort from the parse tree -func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { +// unkIdent handles identifiers with no types.Object (neither use nor +// def), use the parse stack. +// A lot of these only happen when the package doesn't compile, +// but in that case it is all best-effort from the parse tree. +func (tv *tokenVisitor) unkIdent(id *ast.Ident) (semtok.TokenType, []string) { def := []string{"definition"} n := len(tv.stack) - 2 // parent of Ident if n < 0 { - tv.unexpected("no stack?") + // TODO(adonovan): I suspect this reachable for the "package p" declaration. + tv.errorf("no stack?") return "", nil } - switch nd := tv.stack[n].(type) { + switch parent := tv.stack[n].(type) { case *ast.BinaryExpr, *ast.UnaryExpr, *ast.ParenExpr, *ast.StarExpr, *ast.IncDecStmt, *ast.SliceExpr, *ast.ExprStmt, *ast.IndexExpr, *ast.ReturnStmt, *ast.ChanType, *ast.SendStmt, @@ -523,14 +494,12 @@ func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { case *ast.Ellipsis: return semtok.TokType, nil case *ast.CaseClause: - if n-2 >= 0 { - if _, ok := tv.stack[n-2].(*ast.TypeSwitchStmt); ok { - return semtok.TokType, nil - } + if n-2 >= 0 && is[ast.TypeSwitchStmt](tv.stack[n-2]) { + return semtok.TokType, nil } return semtok.TokVariable, nil case *ast.ArrayType: - if x == nd.Len { + if id == parent.Len { // or maybe a Type Param, but we can't just from the parse tree return semtok.TokVariable, nil } else { @@ -539,26 +508,26 @@ func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { case *ast.MapType: return semtok.TokType, nil case *ast.CallExpr: - if x == nd.Fun { + if id == parent.Fun { return semtok.TokFunction, nil } return semtok.TokVariable, nil case *ast.SwitchStmt: return semtok.TokVariable, nil case *ast.TypeAssertExpr: - if x == nd.X { + if id == parent.X { return semtok.TokVariable, nil - } else if x == nd.Type { + } else if id == parent.Type { return semtok.TokType, nil } case *ast.ValueSpec: - for _, p := range nd.Names { - if p == x { + for _, p := range parent.Names { + if p == id { return semtok.TokVariable, def } } - for _, p := range nd.Values { - if p == x { + for _, p := range parent.Values { + if p == id { return semtok.TokVariable, nil } } @@ -567,17 +536,17 @@ func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { if n-1 >= 0 { if ce, ok := tv.stack[n-1].(*ast.CallExpr); ok { // ... CallExpr SelectorExpr Ident (_.x()) - if ce.Fun == nd && nd.Sel == x { + if ce.Fun == parent && parent.Sel == id { return semtok.TokFunction, nil } } } return semtok.TokVariable, nil case *ast.AssignStmt: - for _, p := range nd.Lhs { + for _, p := range parent.Lhs { // x := ..., or x = ... - if p == x { - if nd.Tok != token.DEFINE { + if p == id { + if parent.Tok != token.DEFINE { def = nil } return semtok.TokVariable, def // '_' in _ = ... @@ -586,104 +555,101 @@ func (tv *tokenVisitor) unkIdent(x *ast.Ident) (semtok.TokenType, []string) { // RHS, = x return semtok.TokVariable, nil case *ast.TypeSpec: // it's a type if it is either the Name or the Type - if x == nd.Type { + if id == parent.Type { def = nil } return semtok.TokType, def case *ast.Field: // ident could be type in a field, or a method in an interface type, or a variable - if x == nd.Type { + if id == parent.Type { return semtok.TokType, nil } - if n-2 >= 0 { - _, okit := tv.stack[n-2].(*ast.InterfaceType) - _, okfl := tv.stack[n-1].(*ast.FieldList) - if okit && okfl { - return semtok.TokMethod, def - } + if n > 2 && + is[*ast.InterfaceType](tv.stack[n-2]) && + is[*ast.FieldList](tv.stack[n-1]) { + + return semtok.TokMethod, def } return semtok.TokVariable, nil case *ast.LabeledStmt, *ast.BranchStmt: // nothing to report case *ast.CompositeLit: - if nd.Type == x { + if parent.Type == id { return semtok.TokType, nil } return semtok.TokVariable, nil case *ast.RangeStmt: - if nd.Tok != token.DEFINE { + if parent.Tok != token.DEFINE { def = nil } return semtok.TokVariable, def case *ast.FuncDecl: return semtok.TokFunction, def default: - msg := fmt.Sprintf("%T undexpected: %s %s%q", nd, x.Name, tv.strStack(), tv.srcLine(x)) - tv.unexpected(msg) + tv.errorf("%T unexpected: %s %s%q", parent, id.Name, tv.strStack(), tv.srcLine(id)) } return "", nil } func isDeprecated(n *ast.CommentGroup) bool { - if n == nil { - return false - } - for _, c := range n.List { - if strings.HasPrefix(c.Text, "// Deprecated") { - return true + if n != nil { + for _, c := range n.List { + if strings.HasPrefix(c.Text, "// Deprecated") { + return true + } } } return false } -func (tv *tokenVisitor) definitionFor(x *ast.Ident, def types.Object) (semtok.TokenType, []string) { - // PJW: def == types.Label? probably a nothing +// definitionFor handles a defining identifier. +func (tv *tokenVisitor) definitionFor(id *ast.Ident, obj types.Object) (semtok.TokenType, []string) { + // PJW: obj == types.Label? probably a nothing // PJW: look into replacing these syntactic tests with types more generally - mods := []string{"definition"} + modifiers := []string{"definition"} for i := len(tv.stack) - 1; i >= 0; i-- { - s := tv.stack[i] - switch y := s.(type) { + switch ancestor := tv.stack[i].(type) { case *ast.AssignStmt, *ast.RangeStmt: - if x.Name == "_" { + if id.Name == "_" { return "", nil // not really a variable } - return semtok.TokVariable, mods + return semtok.TokVariable, modifiers case *ast.GenDecl: - if isDeprecated(y.Doc) { - mods = append(mods, "deprecated") + if isDeprecated(ancestor.Doc) { + modifiers = append(modifiers, "deprecated") } - if y.Tok == token.CONST { - mods = append(mods, "readonly") + if ancestor.Tok == token.CONST { + modifiers = append(modifiers, "readonly") } - return semtok.TokVariable, mods + return semtok.TokVariable, modifiers case *ast.FuncDecl: // If x is immediately under a FuncDecl, it is a function or method if i == len(tv.stack)-2 { - if isDeprecated(y.Doc) { - mods = append(mods, "deprecated") + if isDeprecated(ancestor.Doc) { + modifiers = append(modifiers, "deprecated") } - if y.Recv != nil { - return semtok.TokMethod, mods + if ancestor.Recv != nil { + return semtok.TokMethod, modifiers } - return semtok.TokFunction, mods + return semtok.TokFunction, modifiers } // if x < ... < FieldList < FuncDecl, this is the receiver, a variable // PJW: maybe not. it might be a typeparameter in the type of the receiver - if _, ok := tv.stack[i+1].(*ast.FieldList); ok { - if _, ok := def.(*types.TypeName); ok { - return semtok.TokTypeParam, mods + if is[*ast.FieldList](tv.stack[i+1]) { + if is[*types.TypeName](obj) { + return semtok.TokTypeParam, modifiers } return semtok.TokVariable, nil } // if x < ... < FieldList < FuncType < FuncDecl, this is a param - return semtok.TokParameter, mods - case *ast.FuncType: // is it in the TypeParams? - if isTypeParam(x, y) { - return semtok.TokTypeParam, mods + return semtok.TokParameter, modifiers + case *ast.FuncType: + if isTypeParam(id, ancestor) { + return semtok.TokTypeParam, modifiers } - return semtok.TokParameter, mods + return semtok.TokParameter, modifiers case *ast.InterfaceType: - return semtok.TokMethod, mods + return semtok.TokMethod, modifiers case *ast.TypeSpec: // GenDecl/Typespec/FuncType/FieldList/Field/Ident // (type A func(b uint64)) (err error) @@ -692,8 +658,8 @@ func (tv *tokenVisitor) definitionFor(x *ast.Ident, def types.Object) (semtok.To // (type A struct{b uint64} // but on type B struct{C}), C is a type, but is not being defined. // GenDecl/TypeSpec/FieldList/Field/Ident is a typeParam - if _, ok := tv.stack[i+1].(*ast.FieldList); ok { - return semtok.TokTypeParam, mods + if is[*ast.FieldList](tv.stack[i+1]) { + return semtok.TokTypeParam, modifiers } fldm := tv.stack[len(tv.stack)-2] if fld, ok := fldm.(*ast.Field); ok { @@ -701,36 +667,36 @@ func (tv *tokenVisitor) definitionFor(x *ast.Ident, def types.Object) (semtok.To if len(fld.Names) == 0 { return semtok.TokType, nil } - return semtok.TokVariable, mods + return semtok.TokVariable, modifiers } - return semtok.TokType, mods + return semtok.TokType, modifiers } } // can't happen - msg := fmt.Sprintf("failed to find the decl for %s", safetoken.Position(tv.pgf.Tok, x.Pos())) - tv.unexpected(msg) - return "", []string{""} + tv.errorf("failed to find the decl for %s", safetoken.Position(tv.pgf.Tok, id.Pos())) + return "", []string{""} // TODO(adonovan): shouldn't that be []string{}? } -func isTypeParam(x *ast.Ident, y *ast.FuncType) bool { - tp := y.TypeParams - if tp == nil { - return false - } - for _, p := range tp.List { - for _, n := range p.Names { - if x == n { - return true +func isTypeParam(id *ast.Ident, t *ast.FuncType) bool { + if tp := t.TypeParams; tp != nil { + for _, p := range tp.List { + for _, n := range p.Names { + if id == n { + return true + } } } } return false } -func (tv *tokenVisitor) multiline(start, end token.Pos, val string, tok semtok.TokenType) { +// multiline emits a multiline token (`string` or /*comment*/). +func (tv *tokenVisitor) multiline(start, end token.Pos, tok semtok.TokenType) { + // TODO(adonovan): test with non-ASCII. + f := tv.fset.File(start) // the hard part is finding the lengths of lines. include the \n - leng := func(line int) int { + length := func(line int) int { n := f.LineStart(line) if line >= f.LineCount() { return f.Size() - int(n) @@ -742,17 +708,19 @@ func (tv *tokenVisitor) multiline(start, end token.Pos, val string, tok semtok.T sline := spos.Line eline := epos.Line // first line is from spos.Column to end - tv.token(start, leng(sline)-spos.Column, tok, nil) // leng(sline)-1 - (spos.Column-1) + tv.token(start, length(sline)-spos.Column, tok, nil) // leng(sline)-1 - (spos.Column-1) for i := sline + 1; i < eline; i++ { // intermediate lines are from 1 to end - tv.token(f.LineStart(i), leng(i)-1, tok, nil) // avoid the newline + tv.token(f.LineStart(i), length(i)-1, tok, nil) // avoid the newline } // last line is from 1 to epos.Column tv.token(f.LineStart(eline), epos.Column-1, tok, nil) // columns are 1-based } -// findKeyword finds a keyword rather than guessing its location +// findKeyword returns the position of a keyword by searching within +// the specified range, for when its cannot be exactly known from the AST. func (tv *tokenVisitor) findKeyword(keyword string, start, end token.Pos) token.Pos { + // TODO(adonovan): use safetoken.Offset. offset := int(start) - tv.pgf.Tok.Base() last := int(end) - tv.pgf.Tok.Base() buf := tv.pgf.Src @@ -761,25 +729,25 @@ func (tv *tokenVisitor) findKeyword(keyword string, start, end token.Pos) token. return start + token.Pos(idx) } //(in unparsable programs: type _ <-<-chan int) - tv.unexpected(fmt.Sprintf("not found:%s %v", keyword, safetoken.StartPosition(tv.fset, start))) + tv.errorf("not found:%s %v", keyword, safetoken.StartPosition(tv.fset, start)) return token.NoPos } -func (tv *tokenVisitor) importSpec(d *ast.ImportSpec) { +func (tv *tokenVisitor) importSpec(spec *ast.ImportSpec) { // a local package name or the last component of the Path - if d.Name != nil { - nm := d.Name.String() - if nm != "_" && nm != "." { - tv.token(d.Name.Pos(), len(nm), semtok.TokNamespace, nil) + if spec.Name != nil { + name := spec.Name.String() + if name != "_" && name != "." { + tv.token(spec.Name.Pos(), len(name), semtok.TokNamespace, nil) } return // don't mark anything for . or _ } - importPath := metadata.UnquoteImportPath(d) + importPath := metadata.UnquoteImportPath(spec) if importPath == "" { return } // Import strings are implementation defined. Try to match with parse information. - depID := tv.pkg.Metadata().DepsByImpPath[importPath] + depID := tv.metadata.DepsByImpPath[importPath] if depID == "" { return } @@ -789,18 +757,19 @@ func (tv *tokenVisitor) importSpec(d *ast.ImportSpec) { return } // Check whether the original literal contains the package's declared name. - j := strings.LastIndex(d.Path.Value, string(depMD.Name)) - if j == -1 { + j := strings.LastIndex(spec.Path.Value, string(depMD.Name)) + if j < 0 { // Package name does not match import path, so there is nothing to report. return } // Report virtual declaration at the position of the substring. - start := d.Path.Pos() + token.Pos(j) + start := spec.Path.Pos() + token.Pos(j) tv.token(start, len(depMD.Name), semtok.TokNamespace, nil) } -// log unexpected state -func (tv *tokenVisitor) unexpected(msg string) { +// errorf logs an error. When debugging is enabled, it panics. +func (tv *tokenVisitor) errorf(format string, args ...any) { + msg := fmt.Sprintf(format, args...) if semDebug { panic(msg) } @@ -831,7 +800,7 @@ func (tv *tokenVisitor) godirective(c *ast.Comment) { directive, args, _ := strings.Cut(c.Text, " ") kind, _ := stringsCutPrefix(directive, "//go:") if _, ok := godirectives[kind]; !ok { - // Unknown go: directive. + // Unknown 'go:' directive. tv.token(c.Pos(), len(c.Text), semtok.TokComment, nil) return } @@ -855,3 +824,8 @@ func stringsCutPrefix(s, prefix string) (after string, found bool) { } return s[len(prefix):], true } + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} From 32d313923ad395eac8d4ae25cfd3d36bcf75dc24 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 7 Feb 2024 10:25:56 -0500 Subject: [PATCH 16/47] gopls/internal/golang: add semantic tokens for control labels Fixes golang/go#65494 Change-Id: Id044cd12a5c48bb3d5dc8f91657febdc431811b4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/562244 Reviewed-by: Peter Weinberger Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/semtok.go | 29 +++++++++++++++---- gopls/internal/protocol/semantic.go | 2 +- gopls/internal/protocol/semtok/semtok.go | 7 +++++ .../internal/test/integration/fake/editor.go | 2 ++ .../test/marker/testdata/token/range.txt | 10 +++++++ 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index 268459219f2..546f8b68ac5 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go @@ -231,7 +231,9 @@ func (tv *tokenVisitor) inspect(n ast.Node) (descend bool) { case *ast.BlockStmt: case *ast.BranchStmt: tv.token(n.TokPos, len(n.Tok.String()), semtok.TokKeyword, nil) - // There's no semantic encoding for labels + if n.Label != nil { + tv.token(n.Label.Pos(), len(n.Label.Name), semtok.TokLabel, nil) + } case *ast.CallExpr: if n.Ellipsis.IsValid() { tv.token(n.Ellipsis, len("..."), semtok.TokOperator, nil) @@ -303,6 +305,7 @@ func (tv *tokenVisitor) inspect(n ast.Node) (descend bool) { tv.token(n.Interface, len("interface"), semtok.TokKeyword, nil) case *ast.KeyValueExpr: case *ast.LabeledStmt: + tv.token(n.Label.Pos(), len(n.Label.Name), semtok.TokLabel, []string{"definition"}) case *ast.MapType: tv.token(n.Map, len("map"), semtok.TokKeyword, nil) case *ast.ParenExpr: @@ -411,7 +414,7 @@ func (tv *tokenVisitor) ident(id *ast.Ident) { case *types.Func: emit(semtok.TokFunction) case *types.Label: - // nothing to map it to + // Labels are reliably covered by the syntax traversal. case *types.Nil: // nil is a predeclared identifier emit(semtok.TokVariable, "readonly", "defaultLibrary") @@ -571,8 +574,14 @@ func (tv *tokenVisitor) unkIdent(id *ast.Ident) (semtok.TokenType, []string) { return semtok.TokMethod, def } return semtok.TokVariable, nil - case *ast.LabeledStmt, *ast.BranchStmt: - // nothing to report + case *ast.LabeledStmt: + if id == parent.Label { + return semtok.TokLabel, def + } + case *ast.BranchStmt: + if id == parent.Label { + return semtok.TokLabel, nil + } case *ast.CompositeLit: if parent.Type == id { return semtok.TokType, nil @@ -604,7 +613,17 @@ func isDeprecated(n *ast.CommentGroup) bool { // definitionFor handles a defining identifier. func (tv *tokenVisitor) definitionFor(id *ast.Ident, obj types.Object) (semtok.TokenType, []string) { - // PJW: obj == types.Label? probably a nothing + // The definition of a types.Label cannot be found by + // ascending the syntax tree, and doing so will reach the + // FuncDecl, causing us to misinterpret the label as a + // parameter (#65494). + // + // However, labels are reliably covered by the syntax + // traversal, so we don't need to use type information. + if is[*types.Label](obj) { + return "", nil + } + // PJW: look into replacing these syntactic tests with types more generally modifiers := []string{"definition"} for i := len(tv.stack) - 1; i >= 0; i-- { diff --git a/gopls/internal/protocol/semantic.go b/gopls/internal/protocol/semantic.go index 65253d0323c..03407899b57 100644 --- a/gopls/internal/protocol/semantic.go +++ b/gopls/internal/protocol/semantic.go @@ -8,7 +8,7 @@ package protocol import "fmt" -// SemanticTypes to use in case there is no client, as in the command line, or tests +// SemanticTypes to use in case there is no client, as in the command line, or tests. func SemanticTypes() []string { return semanticTypes[:] } diff --git a/gopls/internal/protocol/semtok/semtok.go b/gopls/internal/protocol/semtok/semtok.go index e69ec825f17..850e234a1b0 100644 --- a/gopls/internal/protocol/semtok/semtok.go +++ b/gopls/internal/protocol/semtok/semtok.go @@ -18,6 +18,9 @@ type Token struct { type TokenType string const ( + // These are the tokens defined by LSP 3.17, but a client is + // free to send its own set; any tokens that the server emits + // that are not in this set are simply not encoded in the bitfield. TokNamespace TokenType = "namespace" TokType TokenType = "type" TokInterface TokenType = "interface" @@ -32,6 +35,10 @@ const ( TokNumber TokenType = "number" TokOperator TokenType = "operator" TokMacro TokenType = "macro" // for templates + + // not part of LSP 3.17 (even though JS has labels) + // https://github.com/microsoft/vscode-languageserver-node/issues/1422 + TokLabel TokenType = "label" ) // Encode returns the LSP encoding of a sequence of tokens. diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index 7c15106603f..bd32ec620c3 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -337,6 +337,8 @@ func clientCapabilities(cfg EditorConfig) (protocol.ClientCapabilities, error) { "struct", "typeParameter", "parameter", "variable", "property", "enumMember", "event", "function", "method", "macro", "keyword", "modifier", "comment", "string", "number", "regexp", "operator", + // Additional types supported by this client: + "label", } capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{ "declaration", "definition", "readonly", "static", diff --git a/gopls/internal/test/marker/testdata/token/range.txt b/gopls/internal/test/marker/testdata/token/range.txt index 23f08403470..2f98c043d8e 100644 --- a/gopls/internal/test/marker/testdata/token/range.txt +++ b/gopls/internal/test/marker/testdata/token/range.txt @@ -17,3 +17,13 @@ func F() { //@token("F", "function", "definition") _ = x //@token("x", "variable", "") _ = F //@token("F", "function", "") } + +func _() { + // A goto's label cannot be found by ascending the syntax tree. + goto loop //@ token("goto", "keyword", ""), token("loop", "label", "") + +loop: //@token("loop", "label", "definition") + for { + continue loop //@ token("continue", "keyword", ""), token("loop", "label", "") + } +} From 4bc74c346c87c2d9edbe6d97e3788c6dabc3d575 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 15 Feb 2024 17:55:30 -0500 Subject: [PATCH 17/47] gopls/internal/golang: enable bug.Report in semantic tokens Also, simplify the handling of constants and address a couple of TODOs from the preceding refactoring. Change-Id: Ic2e2231a4eb2172b365969d2d2b251572287e511 Reviewed-on: https://go-review.googlesource.com/c/tools/+/564655 Reviewed-by: Peter Weinberger Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/semtok.go | 35 ++++++++++----------------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index 546f8b68ac5..7a58065acd6 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go @@ -25,6 +25,7 @@ import ( "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "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/internal/event" ) @@ -392,24 +393,11 @@ func (tv *tokenVisitor) ident(id *ast.Ident) { case *types.Builtin: emit(semtok.TokFunction, "defaultLibrary") case *types.Const: - switch t := obj.Type().(type) { - // TODO(adonovan): set defaultLibrary modified for symbols in types.Universe. - case *types.Basic: + if is[*types.Named](obj.Type()) && + (id.Name == "iota" || id.Name == "true" || id.Name == "false") { + emit(semtok.TokVariable, "readonly", "defaultLibrary") + } else { emit(semtok.TokVariable, "readonly") - case *types.Named: - if id.Name == "iota" { - // TODO(adonovan): this is almost certainly reachable. - // Add a test and do something sensible, e.g. - // emit(semtok.TokVariable, "readonly", "defaultLibrary") - tv.errorf("iota:%T", t) - } - if is[*types.Basic](t.Underlying()) { - emit(semtok.TokVariable, "readonly") - } else { - tv.errorf("%q/%T", id.Name, t) - } - default: - tv.errorf("%s %T %#v", id.Name, t, t) // can't happen } case *types.Func: emit(semtok.TokFunction) @@ -479,10 +467,9 @@ func (tv *tokenVisitor) isParam(pos token.Pos) bool { // but in that case it is all best-effort from the parse tree. func (tv *tokenVisitor) unkIdent(id *ast.Ident) (semtok.TokenType, []string) { def := []string{"definition"} - n := len(tv.stack) - 2 // parent of Ident + n := len(tv.stack) - 2 // parent of Ident; stack is [File ... Ident] if n < 0 { - // TODO(adonovan): I suspect this reachable for the "package p" declaration. - tv.errorf("no stack?") + tv.errorf("no stack") // can't happen return "", nil } switch parent := tv.stack[n].(type) { @@ -693,7 +680,7 @@ func (tv *tokenVisitor) definitionFor(id *ast.Ident, obj types.Object) (semtok.T } // can't happen tv.errorf("failed to find the decl for %s", safetoken.Position(tv.pgf.Tok, id.Pos())) - return "", []string{""} // TODO(adonovan): shouldn't that be []string{}? + return "", nil } func isTypeParam(id *ast.Ident, t *ast.FuncType) bool { @@ -786,12 +773,10 @@ func (tv *tokenVisitor) importSpec(spec *ast.ImportSpec) { tv.token(start, len(depMD.Name), semtok.TokNamespace, nil) } -// errorf logs an error. When debugging is enabled, it panics. +// errorf logs an error and reports a bug. func (tv *tokenVisitor) errorf(format string, args ...any) { msg := fmt.Sprintf(format, args...) - if semDebug { - panic(msg) - } + bug.Report(msg) event.Error(tv.ctx, tv.strStack(), errors.New(msg)) } From 451218f6d4261595b00ad646f7a0534778d0fa1f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 15 Feb 2024 16:34:27 -0500 Subject: [PATCH 18/47] x/tools: address review of CL 564515 (CombinedOutput) I fumbled with git and forked the CL, stranding some review comments from bcmills on the ill-fated fork. Updates golang/go#65729 Change-Id: I6d0bf431f841dacb94e9e13a90bf39f8e2ed2fbf Reviewed-on: https://go-review.googlesource.com/c/tools/+/564339 Reviewed-by: Bryan Mills Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/internal/cgo/cgo_pkgconfig.go | 3 +++ go/internal/gccgoimporter/importer_test.go | 3 +++ internal/diff/difftest/difftest_test.go | 10 ++++++++-- internal/testenv/testenv.go | 15 ++++++++++++--- present/parse_test.go | 3 +++ refactor/rename/rename.go | 3 +++ 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/go/internal/cgo/cgo_pkgconfig.go b/go/internal/cgo/cgo_pkgconfig.go index 4f0d3cf4338..2455be54f6e 100644 --- a/go/internal/cgo/cgo_pkgconfig.go +++ b/go/internal/cgo/cgo_pkgconfig.go @@ -21,6 +21,9 @@ func pkgConfig(mode string, pkgs []string) (flags []string, err error) { if len(out) > 0 { s = fmt.Sprintf("%s: %s", s, out) } + if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 { + s = fmt.Sprintf("%s\nstderr:\n%s", s, err.Stderr) + } return nil, errors.New(s) } if len(out) > 0 { diff --git a/go/internal/gccgoimporter/importer_test.go b/go/internal/gccgoimporter/importer_test.go index 3014c6206d7..d8c6e42f6ad 100644 --- a/go/internal/gccgoimporter/importer_test.go +++ b/go/internal/gccgoimporter/importer_test.go @@ -143,6 +143,9 @@ func TestObjImporter(t *testing.T) { verout, err := exec.Command(gpath, "--version").Output() if err != nil { t.Logf("%s", verout) + if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + t.Logf("stderr:\n%s", exit.Stderr) + } t.Fatal(err) } vers := regexp.MustCompile(`([0-9]+)\.([0-9]+)`).FindSubmatch(verout) diff --git a/internal/diff/difftest/difftest_test.go b/internal/diff/difftest/difftest_test.go index bd98d7304d3..dcd92d7dfeb 100644 --- a/internal/diff/difftest/difftest_test.go +++ b/internal/diff/difftest/difftest_test.go @@ -66,9 +66,15 @@ func getDiffOutput(a, b string) (string, error) { cmd.Env = append(cmd.Env, "LANG=en_US.UTF-8") out, err := cmd.Output() if err != nil { - if _, ok := err.(*exec.ExitError); !ok { - return "", fmt.Errorf("failed to run diff -u %v %v: %v\n%v", fileA.Name(), fileB.Name(), err, string(out)) + exit, ok := err.(*exec.ExitError) + if !ok { + return "", fmt.Errorf("can't exec %s: %v", cmd, err) } + if len(out) == 0 { + // Nonzero exit with no output: terminated by signal? + return "", fmt.Errorf("%s failed: %v; stderr:\n%s", cmd, err, exit.Stderr) + } + // nonzero exit + output => files differ } diff := string(out) if len(diff) <= 0 { diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index 0360d169b50..6cea5d7b6a2 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -85,6 +85,9 @@ func hasTool(tool string) error { // certainly not what the user intended. out, err := exec.Command(tool, "env", "GOROOT").Output() if err != nil { + if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + err = fmt.Errorf("%w\nstderr:\n%s)", err, exit.Stderr) + } checkGoBuild.err = err return } @@ -143,6 +146,9 @@ func cgoEnabled(bypassEnvironment bool) (bool, error) { } out, err := cmd.Output() if err != nil { + if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + err = fmt.Errorf("%w\nstderr:\n%s", err, exit.Stderr) + } return false, err } enabled := strings.TrimSpace(string(out)) @@ -199,9 +205,12 @@ func NeedsTool(t testing.TB, tool string) { t.Helper() if allowMissingTool(tool) { - // TODO(adonovan): might silently skipping because of - // (e.g.) mismatched go env GOROOT and runtime.GOROOT - // risk some users not getting the coverage they expect? + // TODO(adonovan): if we skip because of (e.g.) + // mismatched go env GOROOT and runtime.GOROOT, don't + // we risk some users not getting the coverage they expect? + // bcmills notes: this shouldn't be a concern as of CL 404134 (Go 1.19). + // We could probably safely get rid of that GOPATH consistency + // check entirely at this point. t.Skipf("skipping because %s tool not available: %v", tool, err) } else { t.Fatalf("%s tool not available: %v", tool, err) diff --git a/present/parse_test.go b/present/parse_test.go index 4351c368201..dad57ea77ca 100644 --- a/present/parse_test.go +++ b/present/parse_test.go @@ -6,6 +6,7 @@ package present import ( "bytes" + "fmt" "html/template" "os" "os/exec" @@ -84,6 +85,8 @@ func diff(prefix string, name1 string, b1 []byte, name2 string, b2 []byte) ([]by // diff exits with a non-zero status when the files don't match. // Ignore that failure as long as we get output. err = nil + } else if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + err = fmt.Errorf("%w\nstderr:\n%s)", err, exit.Stderr) } data = bytes.Replace(data, []byte(f1), []byte(name1), -1) diff --git a/refactor/rename/rename.go b/refactor/rename/rename.go index 5415f2e8e82..56603cd33a9 100644 --- a/refactor/rename/rename.go +++ b/refactor/rename/rename.go @@ -596,6 +596,9 @@ func diff(filename string, content []byte) error { return nil } if err != nil { + if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { + err = fmt.Errorf("%w\nstderr:\n%s", err, exit.Stderr) + } return fmt.Errorf("computing diff: %v", err) } return nil From c61f99f1f05571ada0066bc5e26116e332038a33 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Mon, 12 Feb 2024 11:37:50 -0500 Subject: [PATCH 19/47] cmd/getgo: create a module in preparation for deprecation And add deprecation notice. Updates golang/go#60951 Change-Id: I9d4ffffc03201b038db5cc11563ea1cc5330e668 Reviewed-on: https://go-review.googlesource.com/c/tools/+/563398 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov --- cmd/getgo/README.md | 2 ++ cmd/getgo/go.mod | 4 ++++ cmd/getgo/main.go | 22 +++++++++++++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 cmd/getgo/go.mod diff --git a/cmd/getgo/README.md b/cmd/getgo/README.md index e62a6c2b64e..0512053d440 100644 --- a/cmd/getgo/README.md +++ b/cmd/getgo/README.md @@ -1,3 +1,5 @@ +**Deprecated** - See https://go.dev/issues/60951 + # getgo A proof-of-concept command-line installer for Go. diff --git a/cmd/getgo/go.mod b/cmd/getgo/go.mod new file mode 100644 index 00000000000..7080c135a43 --- /dev/null +++ b/cmd/getgo/go.mod @@ -0,0 +1,4 @@ +// Deprecated: follow the installation instructions at go.dev/doc/install. +module golang.org/x/tools/cmd/getgo + +go 1.18 diff --git a/cmd/getgo/main.go b/cmd/getgo/main.go index e7bb2ee59d9..86d60b73537 100644 --- a/cmd/getgo/main.go +++ b/cmd/getgo/main.go @@ -5,7 +5,25 @@ //go:build !plan9 // +build !plan9 -// The getgo command installs Go to the user's system. +/* +The getgo command is deprecated. + +Deprecated: See https://go.dev/issues/60951. + +Follow the instructions at https://go.dev/doc/install to install Go instead. + +Tips: + +To find the latest available go version, run: + + go list -m -f '{{.Version}}' go@latest + +If you want to use the latest go by default, you can use "go env -w" to override "GOTOOLCHAIN": + + go env -w GOTOOLCHAIN=go$(go list -m -f '{{.Version}}' go@latest)+auto + +See https://go.dev/blog/toolchain for more information about toolchain management. +*/ package main import ( @@ -32,6 +50,8 @@ var errExitCleanly error = errors.New("exit cleanly sentinel value") func main() { flag.Parse() + fmt.Fprintln(os.Stderr, "getgo is deprecated. See https://pkg.go.dev/golang.org/x/tools/cmd/getgo.") + if *goVersion != "" && !strings.HasPrefix(*goVersion, "go") { *goVersion = "go" + *goVersion } From 4231a57faa43c796f9fd5c414fda4c361fb527b1 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Fri, 16 Feb 2024 12:58:14 -0500 Subject: [PATCH 20/47] cmd/getgo: delete package The last version v0.1.0-deprecated was published. Fixes golang/go#60951 Change-Id: Ib178384ba3a4010edd509825f8a9106853ccf703 Reviewed-on: https://go-review.googlesource.com/c/tools/+/564657 LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov Reviewed-by: Alan Donovan Auto-Submit: Hyang-Ah Hana Kim --- cmd/getgo/.dockerignore | 5 - cmd/getgo/.gitignore | 2 - cmd/getgo/Dockerfile | 20 ---- cmd/getgo/LICENSE | 27 ----- cmd/getgo/README.md | 73 ------------- cmd/getgo/download.go | 190 --------------------------------- cmd/getgo/download_test.go | 36 ------- cmd/getgo/go.mod | 4 - cmd/getgo/main.go | 138 ------------------------ cmd/getgo/main_test.go | 156 --------------------------- cmd/getgo/make.bash | 13 --- cmd/getgo/path.go | 156 --------------------------- cmd/getgo/path_test.go | 58 ---------- cmd/getgo/server/.gcloudignore | 25 ----- cmd/getgo/server/README.md | 7 -- cmd/getgo/server/app.yaml | 2 - cmd/getgo/server/main.go | 73 ------------- cmd/getgo/steps.go | 134 ----------------------- cmd/getgo/system.go | 39 ------- cmd/getgo/system_unix.go | 55 ---------- cmd/getgo/system_windows.go | 87 --------------- cmd/getgo/upload.bash | 19 ---- 22 files changed, 1319 deletions(-) delete mode 100644 cmd/getgo/.dockerignore delete mode 100644 cmd/getgo/.gitignore delete mode 100644 cmd/getgo/Dockerfile delete mode 100644 cmd/getgo/LICENSE delete mode 100644 cmd/getgo/README.md delete mode 100644 cmd/getgo/download.go delete mode 100644 cmd/getgo/download_test.go delete mode 100644 cmd/getgo/go.mod delete mode 100644 cmd/getgo/main.go delete mode 100644 cmd/getgo/main_test.go delete mode 100755 cmd/getgo/make.bash delete mode 100644 cmd/getgo/path.go delete mode 100644 cmd/getgo/path_test.go delete mode 100644 cmd/getgo/server/.gcloudignore delete mode 100644 cmd/getgo/server/README.md delete mode 100644 cmd/getgo/server/app.yaml delete mode 100644 cmd/getgo/server/main.go delete mode 100644 cmd/getgo/steps.go delete mode 100644 cmd/getgo/system.go delete mode 100644 cmd/getgo/system_unix.go delete mode 100644 cmd/getgo/system_windows.go delete mode 100755 cmd/getgo/upload.bash diff --git a/cmd/getgo/.dockerignore b/cmd/getgo/.dockerignore deleted file mode 100644 index 2b87ad9cd76..00000000000 --- a/cmd/getgo/.dockerignore +++ /dev/null @@ -1,5 +0,0 @@ -.git -.dockerignore -LICENSE -README.md -.gitignore diff --git a/cmd/getgo/.gitignore b/cmd/getgo/.gitignore deleted file mode 100644 index 47fe98419a0..00000000000 --- a/cmd/getgo/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -build -getgo diff --git a/cmd/getgo/Dockerfile b/cmd/getgo/Dockerfile deleted file mode 100644 index 78fd9566799..00000000000 --- a/cmd/getgo/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM golang:latest - -ENV SHELL /bin/bash -ENV HOME /root -WORKDIR $HOME - -COPY . /go/src/golang.org/x/tools/cmd/getgo - -RUN ( \ - cd /go/src/golang.org/x/tools/cmd/getgo \ - && go build \ - && mv getgo /usr/local/bin/getgo \ - ) - -# undo the adding of GOPATH to env for testing -ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin -ENV GOPATH "" - -# delete /go and /usr/local/go for testing -RUN rm -rf /go /usr/local/go diff --git a/cmd/getgo/LICENSE b/cmd/getgo/LICENSE deleted file mode 100644 index 32017f8fa1d..00000000000 --- a/cmd/getgo/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2017 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/cmd/getgo/README.md b/cmd/getgo/README.md deleted file mode 100644 index 0512053d440..00000000000 --- a/cmd/getgo/README.md +++ /dev/null @@ -1,73 +0,0 @@ -**Deprecated** - See https://go.dev/issues/60951 - -# getgo - -A proof-of-concept command-line installer for Go. - -This installer is designed to both install Go as well as do the initial configuration -of setting up the right environment variables and paths. - -It will install the Go distribution (tools & stdlib) to "/.go" inside your home directory by default. - -It will setup "$HOME/go" as your GOPATH. -This is where third party libraries and apps will be installed as well as where you will write your Go code. - -If Go is already installed via this installer it will upgrade it to the latest version of Go. - -Currently the installer supports Windows, \*nix and macOS on x86 & x64. -It supports Bash and Zsh on all of these platforms as well as powershell & cmd.exe on Windows. - -## Usage - -Windows Powershell/cmd.exe: - -`(New-Object System.Net.WebClient).DownloadFile('https://get.golang.org/installer.exe', 'installer.exe'); Start-Process -Wait -NonewWindow installer.exe; Remove-Item installer.exe` - -Shell (Linux/macOS/Windows): - -`curl -LO https://get.golang.org/$(uname)/go_installer && chmod +x go_installer && ./go_installer && rm go_installer` - -## To Do - -* Check if Go is already installed (via a different method) and update it in place or at least notify the user -* Lots of testing. It's only had limited testing so far. -* Add support for additional shells. - -## Development instructions - -### Testing - -There are integration tests in [`main_test.go`](main_test.go). Please add more -tests there. - -#### On unix/linux with the Dockerfile - -The Dockerfile automatically builds the binary, moves it to -`/usr/local/bin/getgo` and then unsets `$GOPATH` and removes all `$GOPATH` from -`$PATH`. - -```bash -$ docker build --rm --force-rm -t getgo . -... -$ docker run --rm -it getgo bash -root@78425260fad0:~# getgo -v -Welcome to the Go installer! -Downloading Go version go1.8.3 to /usr/local/go -This may take a bit of time... -Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin" to /root/.bashrc -Downloaded! -Setting up GOPATH -Adding "export GOPATH=/root/go" to /root/.bashrc -Adding "export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin" to /root/.bashrc -GOPATH has been setup! -root@78425260fad0:~# which go -/usr/local/go/bin/go -root@78425260fad0:~# echo $GOPATH -/root/go -root@78425260fad0:~# echo $PATH -/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin:/root/go/bin -``` - -## Release instructions - -To upload a new release of getgo, run `./make.bash && ./upload.bash`. diff --git a/cmd/getgo/download.go b/cmd/getgo/download.go deleted file mode 100644 index 18e1aec2eef..00000000000 --- a/cmd/getgo/download.go +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2017 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 !plan9 -// +build !plan9 - -package main - -import ( - "archive/tar" - "archive/zip" - "compress/gzip" - "crypto/sha256" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" -) - -const ( - downloadURLPrefix = "https://dl.google.com/go" -) - -// downloadGoVersion downloads and upacks the specific go version to dest/go. -func downloadGoVersion(version, ops, arch, dest string) error { - suffix := "tar.gz" - if ops == "windows" { - suffix = "zip" - } - uri := fmt.Sprintf("%s/%s.%s-%s.%s", downloadURLPrefix, version, ops, arch, suffix) - - verbosef("Downloading %s", uri) - - req, err := http.NewRequest("GET", uri, nil) - if err != nil { - return err - } - req.Header.Add("User-Agent", fmt.Sprintf("golang.org-getgo/%s", version)) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("Downloading Go from %s failed: %v", uri, err) - } - if resp.StatusCode > 299 { - return fmt.Errorf("Downloading Go from %s failed with HTTP status %s", uri, resp.Status) - } - defer resp.Body.Close() - - tmpf, err := os.CreateTemp("", "go") - if err != nil { - return err - } - defer os.Remove(tmpf.Name()) - - h := sha256.New() - - w := io.MultiWriter(tmpf, h) - if _, err := io.Copy(w, resp.Body); err != nil { - return err - } - - verbosef("Downloading SHA %s.sha256", uri) - - sresp, err := http.Get(uri + ".sha256") - if err != nil { - return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed: %v", uri, err) - } - defer sresp.Body.Close() - if sresp.StatusCode > 299 { - return fmt.Errorf("Downloading Go sha256 from %s.sha256 failed with HTTP status %s", uri, sresp.Status) - } - - shasum, err := io.ReadAll(sresp.Body) - if err != nil { - return err - } - - // Check the shasum. - sum := fmt.Sprintf("%x", h.Sum(nil)) - if sum != string(shasum) { - return fmt.Errorf("Shasum mismatch %s vs. %s", sum, string(shasum)) - } - - unpackFunc := unpackTar - if ops == "windows" { - unpackFunc = unpackZip - } - if err := unpackFunc(tmpf.Name(), dest); err != nil { - return fmt.Errorf("Unpacking Go to %s failed: %v", dest, err) - } - return nil -} - -func unpack(dest, name string, fi os.FileInfo, r io.Reader) error { - if strings.HasPrefix(name, "go/") { - name = name[len("go/"):] - } - - path := filepath.Join(dest, name) - if fi.IsDir() { - return os.MkdirAll(path, fi.Mode()) - } - - f, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode()) - if err != nil { - return err - } - defer f.Close() - - _, err = io.Copy(f, r) - return err -} - -func unpackTar(src, dest string) error { - r, err := os.Open(src) - if err != nil { - return err - } - defer r.Close() - - archive, err := gzip.NewReader(r) - if err != nil { - return err - } - defer archive.Close() - - tarReader := tar.NewReader(archive) - - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } else if err != nil { - return err - } - - if err := unpack(dest, header.Name, header.FileInfo(), tarReader); err != nil { - return err - } - } - - return nil -} - -func unpackZip(src, dest string) error { - zr, err := zip.OpenReader(src) - if err != nil { - return err - } - - for _, f := range zr.File { - fr, err := f.Open() - if err != nil { - return err - } - if err := unpack(dest, f.Name, f.FileInfo(), fr); err != nil { - return err - } - fr.Close() - } - - return nil -} - -func getLatestGoVersion() (string, error) { - resp, err := http.Get("https://golang.org/dl/?mode=json") - if err != nil { - return "", fmt.Errorf("Getting current Go version failed: %v", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - b, _ := io.ReadAll(io.LimitReader(resp.Body, 1024)) - return "", fmt.Errorf("Could not get current Go release: HTTP %d: %q", resp.StatusCode, b) - } - var releases []struct { - Version string - } - err = json.NewDecoder(resp.Body).Decode(&releases) - if err != nil { - return "", err - } - if len(releases) < 1 { - return "", fmt.Errorf("Could not get at least one Go release") - } - return releases[0].Version, nil -} diff --git a/cmd/getgo/download_test.go b/cmd/getgo/download_test.go deleted file mode 100644 index b4f2059d14e..00000000000 --- a/cmd/getgo/download_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2017 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 !plan9 -// +build !plan9 - -package main - -import ( - "os" - "path/filepath" - "testing" -) - -func TestDownloadGoVersion(t *testing.T) { - if testing.Short() { - t.Skipf("Skipping download in short mode") - } - - tmpd, err := os.MkdirTemp("", "go") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpd) - - if err := downloadGoVersion("go1.8.1", "linux", "amd64", filepath.Join(tmpd, "go")); err != nil { - t.Fatal(err) - } - - // Ensure the VERSION file exists. - vf := filepath.Join(tmpd, "go", "VERSION") - if _, err := os.Stat(vf); os.IsNotExist(err) { - t.Fatalf("file %s does not exist and should", vf) - } -} diff --git a/cmd/getgo/go.mod b/cmd/getgo/go.mod deleted file mode 100644 index 7080c135a43..00000000000 --- a/cmd/getgo/go.mod +++ /dev/null @@ -1,4 +0,0 @@ -// Deprecated: follow the installation instructions at go.dev/doc/install. -module golang.org/x/tools/cmd/getgo - -go 1.18 diff --git a/cmd/getgo/main.go b/cmd/getgo/main.go deleted file mode 100644 index 86d60b73537..00000000000 --- a/cmd/getgo/main.go +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2017 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 !plan9 -// +build !plan9 - -/* -The getgo command is deprecated. - -Deprecated: See https://go.dev/issues/60951. - -Follow the instructions at https://go.dev/doc/install to install Go instead. - -Tips: - -To find the latest available go version, run: - - go list -m -f '{{.Version}}' go@latest - -If you want to use the latest go by default, you can use "go env -w" to override "GOTOOLCHAIN": - - go env -w GOTOOLCHAIN=go$(go list -m -f '{{.Version}}' go@latest)+auto - -See https://go.dev/blog/toolchain for more information about toolchain management. -*/ -package main - -import ( - "bufio" - "context" - "errors" - "flag" - "fmt" - "os" - "os/exec" - "strings" -) - -var ( - interactive = flag.Bool("i", false, "Interactive mode, prompt for inputs.") - verbose = flag.Bool("v", false, "Verbose.") - setupOnly = flag.Bool("skip-dl", false, "Don't download - only set up environment variables") - goVersion = flag.String("version", "", `Version of Go to install (e.g. "1.8.3"). If empty, uses the latest version.`) - - version = "devel" -) - -var errExitCleanly error = errors.New("exit cleanly sentinel value") - -func main() { - flag.Parse() - fmt.Fprintln(os.Stderr, "getgo is deprecated. See https://pkg.go.dev/golang.org/x/tools/cmd/getgo.") - - if *goVersion != "" && !strings.HasPrefix(*goVersion, "go") { - *goVersion = "go" + *goVersion - } - - ctx := context.Background() - - verbosef("version " + version) - - runStep := func(s step) { - err := s(ctx) - if err == errExitCleanly { - os.Exit(0) - } - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(2) - } - } - - if !*setupOnly { - runStep(welcome) - runStep(checkOthers) - runStep(chooseVersion) - runStep(downloadGo) - } - - runStep(setupGOPATH) -} - -func verbosef(format string, v ...interface{}) { - if !*verbose { - return - } - - fmt.Printf(format+"\n", v...) -} - -func prompt(ctx context.Context, query, defaultAnswer string) (string, error) { - if !*interactive { - return defaultAnswer, nil - } - - fmt.Printf("%s [%s]: ", query, defaultAnswer) - - type result struct { - answer string - err error - } - ch := make(chan result, 1) - go func() { - s := bufio.NewScanner(os.Stdin) - if !s.Scan() { - ch <- result{"", s.Err()} - return - } - answer := s.Text() - if answer == "" { - answer = defaultAnswer - } - ch <- result{answer, nil} - }() - - select { - case r := <-ch: - return r.answer, r.err - case <-ctx.Done(): - return "", ctx.Err() - } -} - -func runCommand(ctx context.Context, prog string, args ...string) ([]byte, error) { - verbosef("Running command: %s %v", prog, args) - - cmd := exec.CommandContext(ctx, prog, args...) - out, err := cmd.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("running cmd '%s %s' failed: %s err: %v", prog, strings.Join(args, " "), string(out), err) - } - if out != nil && err == nil && len(out) != 0 { - verbosef("%s", out) - } - - return out, nil -} diff --git a/cmd/getgo/main_test.go b/cmd/getgo/main_test.go deleted file mode 100644 index 878137dd3f4..00000000000 --- a/cmd/getgo/main_test.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2017 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 !plan9 -// +build !plan9 - -package main - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "testing" -) - -func TestMain(m *testing.M) { - if os.Getenv("GO_GETGO_TEST_IS_GETGO") != "" { - main() - os.Exit(0) - } - - if os.Getenv("GOGET_INTEGRATION") == "" { - fmt.Fprintln(os.Stderr, "main_test: Skipping integration tests with GOGET_INTEGRATION unset") - return - } - - // Don't let these environment variables confuse the test. - os.Unsetenv("GOBIN") - os.Unsetenv("GOPATH") - os.Unsetenv("GIT_ALLOW_PROTOCOL") - os.Unsetenv("PATH") - - os.Exit(m.Run()) -} - -func createTmpHome(t *testing.T) string { - tmpd, err := os.MkdirTemp("", "testgetgo") - if err != nil { - t.Fatalf("creating test tempdir failed: %v", err) - } - - os.Setenv("HOME", tmpd) - return tmpd -} - -// doRun runs the test getgo command, recording stdout and stderr and -// returning exit status. -func doRun(t *testing.T, args ...string) error { - exe, err := os.Executable() - if err != nil { - t.Fatal(err) - } - t.Helper() - - t.Logf("running getgo %v", args) - var stdout, stderr bytes.Buffer - cmd := exec.Command(exe, args...) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = append(os.Environ(), "GO_GETGO_TEST_IS_GETGO=1") - status := cmd.Run() - if stdout.Len() > 0 { - t.Log("standard output:") - t.Log(stdout.String()) - } - if stderr.Len() > 0 { - t.Log("standard error:") - t.Log(stderr.String()) - } - return status -} - -func TestCommandVerbose(t *testing.T) { - tmpd := createTmpHome(t) - defer os.RemoveAll(tmpd) - - err := doRun(t, "-v") - if err != nil { - t.Fatal(err) - } - // make sure things are in path - shellConfig, err := shellConfigFile() - if err != nil { - t.Fatal(err) - } - b, err := os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - home, err := getHomeDir() - if err != nil { - t.Fatal(err) - } - - expected := fmt.Sprintf(` -export PATH=$PATH:%s/.go/bin - -export GOPATH=%s/go - -export PATH=$PATH:%s/go/bin -`, home, home, home) - - if string(b) != expected { - t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) - } -} - -func TestCommandPathExists(t *testing.T) { - tmpd := createTmpHome(t) - defer os.RemoveAll(tmpd) - - // run once - err := doRun(t, "-skip-dl") - if err != nil { - t.Fatal(err) - } - // make sure things are in path - shellConfig, err := shellConfigFile() - if err != nil { - t.Fatal(err) - } - b, err := os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - home, err := getHomeDir() - if err != nil { - t.Fatal(err) - } - - expected := fmt.Sprintf(` -export GOPATH=%s/go - -export PATH=$PATH:%s/go/bin -`, home, home) - - if string(b) != expected { - t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) - } - - // run twice - if err := doRun(t, "-skip-dl"); err != nil { - t.Fatal(err) - } - - b, err = os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - - if string(b) != expected { - t.Fatalf("%s expected %q, got %q", shellConfig, expected, string(b)) - } -} diff --git a/cmd/getgo/make.bash b/cmd/getgo/make.bash deleted file mode 100755 index cbc36857e86..00000000000 --- a/cmd/getgo/make.bash +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -# Copyright 2017 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. - -set -e -o -x - -LDFLAGS="-X main.version=$(git describe --always --dirty='*')" - -GOOS=windows GOARCH=386 go build -o build/installer.exe -ldflags="$LDFLAGS" -GOOS=linux GOARCH=386 go build -o build/installer_linux -ldflags="$LDFLAGS" -GOOS=darwin GOARCH=386 go build -o build/installer_darwin -ldflags="$LDFLAGS" diff --git a/cmd/getgo/path.go b/cmd/getgo/path.go deleted file mode 100644 index f1799a85f4e..00000000000 --- a/cmd/getgo/path.go +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2017 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 !plan9 -// +build !plan9 - -package main - -import ( - "bufio" - "context" - "fmt" - "os" - "os/user" - "path/filepath" - "runtime" - "strings" -) - -const ( - bashConfig = ".bash_profile" - zshConfig = ".zshrc" -) - -// appendToPATH adds the given path to the PATH environment variable and -// persists it for future sessions. -func appendToPATH(value string) error { - if isInPATH(value) { - return nil - } - return persistEnvVar("PATH", pathVar+envSeparator+value) -} - -func isInPATH(dir string) bool { - p := os.Getenv("PATH") - - paths := strings.Split(p, envSeparator) - for _, d := range paths { - if d == dir { - return true - } - } - - return false -} - -func getHomeDir() (string, error) { - home := os.Getenv(homeKey) - if home != "" { - return home, nil - } - - u, err := user.Current() - if err != nil { - return "", err - } - return u.HomeDir, nil -} - -func checkStringExistsFile(filename, value string) (bool, error) { - file, err := os.OpenFile(filename, os.O_RDONLY, 0600) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - defer file.Close() - - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if line == value { - return true, nil - } - } - - return false, scanner.Err() -} - -func appendToFile(filename, value string) error { - verbosef("Adding %q to %s", value, filename) - - ok, err := checkStringExistsFile(filename, value) - if err != nil { - return err - } - if ok { - // Nothing to do. - return nil - } - - f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString(lineEnding + value + lineEnding) - return err -} - -func isShell(name string) bool { - return strings.Contains(currentShell(), name) -} - -// persistEnvVarWindows sets an environment variable in the Windows -// registry. -func persistEnvVarWindows(name, value string) error { - _, err := runCommand(context.Background(), "powershell", "-command", - fmt.Sprintf(`[Environment]::SetEnvironmentVariable("%s", "%s", "User")`, name, value)) - return err -} - -func persistEnvVar(name, value string) error { - if runtime.GOOS == "windows" { - if err := persistEnvVarWindows(name, value); err != nil { - return err - } - - if isShell("cmd.exe") || isShell("powershell.exe") { - return os.Setenv(strings.ToUpper(name), value) - } - // User is in bash, zsh, etc. - // Also set the environment variable in their shell config. - } - - rc, err := shellConfigFile() - if err != nil { - return err - } - - line := fmt.Sprintf("export %s=%s", strings.ToUpper(name), value) - if err := appendToFile(rc, line); err != nil { - return err - } - - return os.Setenv(strings.ToUpper(name), value) -} - -func shellConfigFile() (string, error) { - home, err := getHomeDir() - if err != nil { - return "", err - } - - switch { - case isShell("bash"): - return filepath.Join(home, bashConfig), nil - case isShell("zsh"): - return filepath.Join(home, zshConfig), nil - default: - return "", fmt.Errorf("%q is not a supported shell", currentShell()) - } -} diff --git a/cmd/getgo/path_test.go b/cmd/getgo/path_test.go deleted file mode 100644 index 8195f2e68d5..00000000000 --- a/cmd/getgo/path_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2017 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 !plan9 -// +build !plan9 - -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestAppendPath(t *testing.T) { - tmpd, err := os.MkdirTemp("", "go") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tmpd) - - if err := os.Setenv("HOME", tmpd); err != nil { - t.Fatal(err) - } - - GOPATH := os.Getenv("GOPATH") - if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil { - t.Fatal(err) - } - - shellConfig, err := shellConfigFile() - if err != nil { - t.Fatal(err) - } - b, err := os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - - expected := "export PATH=" + pathVar + envSeparator + filepath.Join(GOPATH, "bin") - if strings.TrimSpace(string(b)) != expected { - t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b))) - } - - // Check that appendToPATH is idempotent. - if err := appendToPATH(filepath.Join(GOPATH, "bin")); err != nil { - t.Fatal(err) - } - b, err = os.ReadFile(shellConfig) - if err != nil { - t.Fatal(err) - } - if strings.TrimSpace(string(b)) != expected { - t.Fatalf("expected: %q, got %q", expected, strings.TrimSpace(string(b))) - } -} diff --git a/cmd/getgo/server/.gcloudignore b/cmd/getgo/server/.gcloudignore deleted file mode 100644 index 199e6d9f2f9..00000000000 --- a/cmd/getgo/server/.gcloudignore +++ /dev/null @@ -1,25 +0,0 @@ -# This file specifies files that are *not* uploaded to Google Cloud Platform -# using gcloud. It follows the same syntax as .gitignore, with the addition of -# "#!include" directives (which insert the entries of the given .gitignore-style -# file at that point). -# -# For more information, run: -# $ gcloud topic gcloudignore -# -.gcloudignore -# If you would like to upload your .git directory, .gitignore file or files -# from your .gitignore file, remove the corresponding line -# below: -.git -.gitignore - -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib -# Test binary, build with `go test -c` -*.test -# Output of the go coverage tool, specifically when used with LiteIDE -*.out \ No newline at end of file diff --git a/cmd/getgo/server/README.md b/cmd/getgo/server/README.md deleted file mode 100644 index 0cf629d6e6e..00000000000 --- a/cmd/getgo/server/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# getgo server - -## Deployment - -``` -gcloud app deploy --promote --project golang-org -``` diff --git a/cmd/getgo/server/app.yaml b/cmd/getgo/server/app.yaml deleted file mode 100644 index 5c47312ef1d..00000000000 --- a/cmd/getgo/server/app.yaml +++ /dev/null @@ -1,2 +0,0 @@ -runtime: go112 -service: get diff --git a/cmd/getgo/server/main.go b/cmd/getgo/server/main.go deleted file mode 100644 index bdb0f70cf49..00000000000 --- a/cmd/getgo/server/main.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2017 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. - -// Command server serves get.golang.org, redirecting users to the appropriate -// getgo installer based on the request path. -package main - -import ( - "fmt" - "net/http" - "os" - "strings" - "time" -) - -const ( - base = "https://dl.google.com/go/getgo/" - windowsInstaller = base + "installer.exe" - linuxInstaller = base + "installer_linux" - macInstaller = base + "installer_darwin" -) - -// substring-based redirects. -var stringMatch = map[string]string{ - // via uname, from bash - "MINGW": windowsInstaller, // Reported as MINGW64_NT-10.0 in git bash - "Linux": linuxInstaller, - "Darwin": macInstaller, -} - -func main() { - http.HandleFunc("/", handler) - - port := os.Getenv("PORT") - if port == "" { - port = "8080" - fmt.Printf("Defaulting to port %s", port) - } - - fmt.Printf("Listening on port %s", port) - if err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil); err != nil { - fmt.Fprintf(os.Stderr, "http.ListenAndServe: %v", err) - } -} - -func handler(w http.ResponseWriter, r *http.Request) { - if containsIgnoreCase(r.URL.Path, "installer.exe") { - // cache bust - http.Redirect(w, r, windowsInstaller+cacheBust(), http.StatusFound) - return - } - - for match, redirect := range stringMatch { - if containsIgnoreCase(r.URL.Path, match) { - http.Redirect(w, r, redirect, http.StatusFound) - return - } - } - - http.NotFound(w, r) -} - -func containsIgnoreCase(s, substr string) bool { - return strings.Contains( - strings.ToLower(s), - strings.ToLower(substr), - ) -} - -func cacheBust() string { - return fmt.Sprintf("?%d", time.Now().Nanosecond()) -} diff --git a/cmd/getgo/steps.go b/cmd/getgo/steps.go deleted file mode 100644 index fe69aa63aaf..00000000000 --- a/cmd/getgo/steps.go +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2017 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 !plan9 -// +build !plan9 - -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" - "runtime" - "strings" -) - -type step func(context.Context) error - -func welcome(ctx context.Context) error { - fmt.Println("Welcome to the Go installer!") - answer, err := prompt(ctx, "Would you like to install Go? Y/n", "Y") - if err != nil { - return err - } - if strings.ToLower(answer) != "y" { - fmt.Println("Exiting install.") - return errExitCleanly - } - - return nil -} - -func checkOthers(ctx context.Context) error { - // TODO: if go is currently installed install new version over that - path, err := whichGo(ctx) - if err != nil { - fmt.Printf("Cannot check if Go is already installed:\n%v\n", err) - } - if path == "" { - return nil - } - if path != installPath { - fmt.Printf("Go is already installed at %v; remove it from your PATH.\n", path) - } - return nil -} - -func chooseVersion(ctx context.Context) error { - if *goVersion != "" { - return nil - } - - var err error - *goVersion, err = getLatestGoVersion() - if err != nil { - return err - } - - answer, err := prompt(ctx, fmt.Sprintf("The latest Go version is %s, install that? Y/n", *goVersion), "Y") - if err != nil { - return err - } - - if strings.ToLower(answer) != "y" { - // TODO: handle passing a version - fmt.Println("Aborting install.") - return errExitCleanly - } - - return nil -} - -func downloadGo(ctx context.Context) error { - answer, err := prompt(ctx, fmt.Sprintf("Download Go version %s to %s? Y/n", *goVersion, installPath), "Y") - if err != nil { - return err - } - - if strings.ToLower(answer) != "y" { - fmt.Println("Aborting install.") - return errExitCleanly - } - - fmt.Printf("Downloading Go version %s to %s\n", *goVersion, installPath) - fmt.Println("This may take a bit of time...") - - if err := downloadGoVersion(*goVersion, runtime.GOOS, arch, installPath); err != nil { - return err - } - - if err := appendToPATH(filepath.Join(installPath, "bin")); err != nil { - return err - } - - fmt.Println("Downloaded!") - return nil -} - -func setupGOPATH(ctx context.Context) error { - answer, err := prompt(ctx, "Would you like us to setup your GOPATH? Y/n", "Y") - if err != nil { - return err - } - - if strings.ToLower(answer) != "y" { - fmt.Println("Exiting and not setting up GOPATH.") - return errExitCleanly - } - - fmt.Println("Setting up GOPATH") - home, err := getHomeDir() - if err != nil { - return err - } - - gopath := os.Getenv("GOPATH") - if gopath == "" { - // set $GOPATH - gopath = filepath.Join(home, "go") - if err := persistEnvVar("GOPATH", gopath); err != nil { - return err - } - fmt.Println("GOPATH has been set up!") - } else { - verbosef("GOPATH is already set to %s", gopath) - } - - if err := appendToPATH(filepath.Join(gopath, "bin")); err != nil { - return err - } - return persistEnvChangesForSession() -} diff --git a/cmd/getgo/system.go b/cmd/getgo/system.go deleted file mode 100644 index 729983986ba..00000000000 --- a/cmd/getgo/system.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2017 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 !plan9 -// +build !plan9 - -package main - -import ( - "bytes" - "context" - "os/exec" - "runtime" - "strings" -) - -// arch contains either amd64 or 386. -var arch = func() string { - cmd := exec.Command("uname", "-m") // "x86_64" - if runtime.GOOS == "windows" { - cmd = exec.Command("powershell", "-command", "(Get-WmiObject -Class Win32_ComputerSystem).SystemType") // "x64-based PC" - } - - out, err := cmd.Output() - if err != nil { - // a sensible default? - return "amd64" - } - if bytes.Contains(out, []byte("64")) { - return "amd64" - } - return "386" -}() - -func findGo(ctx context.Context, cmd string) (string, error) { - out, err := exec.CommandContext(ctx, cmd, "go").CombinedOutput() - return strings.TrimSpace(string(out)), err -} diff --git a/cmd/getgo/system_unix.go b/cmd/getgo/system_unix.go deleted file mode 100644 index 0b511dbeb4b..00000000000 --- a/cmd/getgo/system_unix.go +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2017 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 !plan9 && !windows - -package main - -import ( - "context" - "fmt" - "os" - "path/filepath" -) - -const ( - envSeparator = ":" - homeKey = "HOME" - lineEnding = "\n" - pathVar = "$PATH" -) - -var installPath = func() string { - home, err := getHomeDir() - if err != nil { - return "/usr/local/go" - } - - return filepath.Join(home, ".go") -}() - -func whichGo(ctx context.Context) (string, error) { - return findGo(ctx, "which") -} - -func isWindowsXP() bool { - return false -} - -func currentShell() string { - return os.Getenv("SHELL") -} - -func persistEnvChangesForSession() error { - shellConfig, err := shellConfigFile() - if err != nil { - return err - } - fmt.Println() - fmt.Printf("One more thing! Run `source %s` to persist the\n", shellConfig) - fmt.Println("new environment variables to your current session, or open a") - fmt.Println("new shell prompt.") - - return nil -} diff --git a/cmd/getgo/system_windows.go b/cmd/getgo/system_windows.go deleted file mode 100644 index 5b1e2471300..00000000000 --- a/cmd/getgo/system_windows.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2017 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 windows -// +build windows - -package main - -import ( - "context" - "log" - "os" - "syscall" - "unsafe" -) - -const ( - envSeparator = ";" - homeKey = "USERPROFILE" - lineEnding = "/r/n" - pathVar = "$env:Path" -) - -var installPath = `c:\go` - -func isWindowsXP() bool { - v, err := syscall.GetVersion() - if err != nil { - log.Fatalf("GetVersion failed: %v", err) - } - major := byte(v) - return major < 6 -} - -func whichGo(ctx context.Context) (string, error) { - return findGo(ctx, "where") -} - -// currentShell reports the current shell. -// It might be "powershell.exe", "cmd.exe" or any of the *nix shells. -// -// Returns empty string if the shell is unknown. -func currentShell() string { - shell := os.Getenv("SHELL") - if shell != "" { - return shell - } - - pid := os.Getppid() - pe, err := getProcessEntry(pid) - if err != nil { - verbosef("getting shell from process entry failed: %v", err) - return "" - } - - return syscall.UTF16ToString(pe.ExeFile[:]) -} - -func getProcessEntry(pid int) (*syscall.ProcessEntry32, error) { - // From https://go.googlesource.com/go/+/go1.8.3/src/syscall/syscall_windows.go#941 - snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0) - if err != nil { - return nil, err - } - defer syscall.CloseHandle(snapshot) - - var procEntry syscall.ProcessEntry32 - procEntry.Size = uint32(unsafe.Sizeof(procEntry)) - if err = syscall.Process32First(snapshot, &procEntry); err != nil { - return nil, err - } - - for { - if procEntry.ProcessID == uint32(pid) { - return &procEntry, nil - } - - if err := syscall.Process32Next(snapshot, &procEntry); err != nil { - return nil, err - } - } -} - -func persistEnvChangesForSession() error { - return nil -} diff --git a/cmd/getgo/upload.bash b/cmd/getgo/upload.bash deleted file mode 100755 index f52bb23c93c..00000000000 --- a/cmd/getgo/upload.bash +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Copyright 2017 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. - -if ! command -v gsutil 2>&1 > /dev/null; then - echo "Install gsutil:" - echo - echo " https://cloud.google.com/storage/docs/gsutil_install#sdk-install" -fi - -if [ ! -d build ]; then - echo "Run make.bash first" -fi - -set -e -o -x - -gsutil -m cp -a public-read build/* gs://golang/getgo From 68515eaff75847a1e61a120673f3a281fc87cdfd Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 16 Feb 2024 14:48:20 -0500 Subject: [PATCH 21/47] gopls/internal/test/integration/fake: set LSP client name Change-Id: I9467de61e7a08f25ae16ae59b9ae6a1dc524d1a6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/564658 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/test/integration/fake/editor.go | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index bd32ec620c3..e93776408b6 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -82,6 +82,8 @@ type EditorConfig struct { // // Since this can only be set during initialization, changing this field via // Editor.ChangeConfiguration has no effect. + // + // If empty, "fake.Editor" is used. ClientName string // Env holds environment variables to apply on top of the default editor @@ -282,12 +284,15 @@ func makeSettings(sandbox *Sandbox, config EditorConfig, scopeURI *protocol.URI) func (e *Editor) initialize(ctx context.Context) error { config := e.Config() + clientName := config.ClientName + if clientName == "" { + clientName = "fake.Editor" + } + params := &protocol.ParamInitialize{} - if e.config.ClientName != "" { - params.ClientInfo = &protocol.ClientInfo{ - Name: e.config.ClientName, - Version: "v1.0.0", - } + params.ClientInfo = &protocol.ClientInfo{ + Name: clientName, + Version: "v1.0.0", } params.InitializationOptions = makeSettings(e.sandbox, config, nil) params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) From 3ac77cb1c1a6c81d09a674b2ecf89c6d01ca0415 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 16 Feb 2024 21:26:22 +0000 Subject: [PATCH 22/47] gopls/internal/settings: default "includeReplaceInWorkspace" to false As described in golang/go#65762, treating locally replaced modules the same as workspace modules doesn't really work, since there will be missing go.sum entries. Fortunately, this functionality was guarded by the "includeReplaceInWorkspace" setting. Revert the default value for this setting. Fixes golang/go#65762 Change-Id: I521acb2863404cba7612887aa7730075dcfebd96 Reviewed-on: https://go-review.googlesource.com/c/tools/+/564558 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/cache/session_test.go | 17 ++++++++++++----- gopls/internal/settings/default.go | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/gopls/internal/cache/session_test.go b/gopls/internal/cache/session_test.go index a3bd8ce5800..913c3bd1f27 100644 --- a/gopls/internal/cache/session_test.go +++ b/gopls/internal/cache/session_test.go @@ -20,6 +20,7 @@ import ( func TestZeroConfigAlgorithm(t *testing.T) { testenv.NeedsExec(t) // executes the Go command + t.Setenv("GOPACKAGESDRIVER", "off") type viewSummary struct { // fields exported for cmp.Diff @@ -33,6 +34,12 @@ func TestZeroConfigAlgorithm(t *testing.T) { options func(dir string) map[string]any // options may refer to the temp dir } + includeReplaceInWorkspace := func(string) map[string]any { + return map[string]any{ + "includeReplaceInWorkspace": true, + } + } + type test struct { name string files map[string]string // use a map rather than txtar as file content is tiny @@ -235,7 +242,7 @@ func TestZeroConfigAlgorithm(t *testing.T) { "b/go.mod": "module golang.org/b\ngo 1.18\n", "b/b.go": "package b", }, - []folderSummary{{dir: "."}}, + []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, []string{"a/a.go", "b/b.go"}, []viewSummary{{GoModView, ".", nil}}, }, @@ -247,7 +254,7 @@ func TestZeroConfigAlgorithm(t *testing.T) { "b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../", "b/b.go": "package b", }, - []folderSummary{{dir: "."}}, + []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, []string{"a/a.go", "b/b.go"}, []viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}}, }, @@ -277,12 +284,12 @@ replace ( "d/go.mod": "module golang.org/d\ngo 1.18", "d/d.go": "package d", }, - []folderSummary{{dir: "."}}, + []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, []string{"b/b.go", "c/c.go", "d/d.go"}, []viewSummary{{GoModView, ".", nil}, {GoModView, "d", nil}}, }, { - "go.mod with many replace", + "go.mod with replace outside the workspace", map[string]string{ "go.mod": "module golang.org/a\ngo 1.18", "a.go": "package a", @@ -290,7 +297,7 @@ replace ( "b/b.go": "package b", }, []folderSummary{{dir: "b"}}, - []string{"a/a.go", "b/b.go"}, + []string{"a.go", "b/b.go"}, []viewSummary{{GoModView, "b", nil}}, }, { diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index 752effab3ef..f29b439428b 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -115,7 +115,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options { ReportAnalysisProgressAfter: 5 * time.Second, TelemetryPrompt: false, LinkifyShowMessage: false, - IncludeReplaceInWorkspace: true, + IncludeReplaceInWorkspace: false, ZeroConfig: true, }, Hooks: Hooks{ From 51dec259b54f5684baebc3629a6d10ab42cd8487 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 16 Feb 2024 17:08:57 -0500 Subject: [PATCH 23/47] gopls/internal/golang: highlight typeswitch break correctly Fixes golang/go#65752 Change-Id: I99a44c85c92615fbf3f4b1d34ab63a24454c8e85 Reviewed-on: https://go-review.googlesource.com/c/tools/+/564955 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/highlight.go | 12 +++++------ .../marker/testdata/highlight/switchbreak.txt | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 gopls/internal/test/marker/testdata/highlight/switchbreak.txt diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index f11426319cf..52483acdb9f 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go @@ -103,7 +103,7 @@ func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRa highlightIdentifier(node, file, info, result) case *ast.ForStmt, *ast.RangeStmt: highlightLoopControlFlow(path, info, result) - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: highlightSwitchFlow(path, info, result) case *ast.BranchStmt: // BREAK can exit a loop, switch or select, while CONTINUE exit a loop so @@ -309,7 +309,7 @@ func highlightUnlabeledBreakFlow(path []ast.Node, info *types.Info, result map[p case *ast.ForStmt, *ast.RangeStmt: highlightLoopControlFlow(path, info, result) return // only highlight the innermost statement - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: highlightSwitchFlow(path, info, result) return case *ast.SelectStmt: @@ -331,7 +331,7 @@ func highlightLabeledFlow(path []ast.Node, info *types.Info, stmt *ast.BranchStm switch label.Stmt.(type) { case *ast.ForStmt, *ast.RangeStmt: highlightLoopControlFlow([]ast.Node{label.Stmt, label}, info, result) - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: highlightSwitchFlow([]ast.Node{label.Stmt, label}, info, result) } return @@ -381,7 +381,7 @@ Outer: switch n.(type) { case *ast.ForStmt, *ast.RangeStmt: return loop == n - case *ast.SwitchStmt, *ast.SelectStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: return false } b, ok := n.(*ast.BranchStmt) @@ -434,7 +434,7 @@ Outer: // Reverse walk the path till we get to the switch statement. for i := range path { switch n := path[i].(type) { - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: switchNodeLabel = labelFor(path[i:]) if stmtLabel == nil || switchNodeLabel == stmtLabel { switchNode = n @@ -457,7 +457,7 @@ Outer: // Traverse AST to find break statements within the same switch. ast.Inspect(switchNode, func(n ast.Node) bool { switch n.(type) { - case *ast.SwitchStmt: + case *ast.SwitchStmt, *ast.TypeSwitchStmt: return switchNode == n case *ast.ForStmt, *ast.RangeStmt, *ast.SelectStmt: return false diff --git a/gopls/internal/test/marker/testdata/highlight/switchbreak.txt b/gopls/internal/test/marker/testdata/highlight/switchbreak.txt new file mode 100644 index 00000000000..b486ad1d80d --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/switchbreak.txt @@ -0,0 +1,21 @@ +This is a regression test for issue 65752: a break in a switch should +highlight the switch, not the enclosing loop. + +-- a.go -- +package a + +func _(x any) { + for { + // type switch + switch x.(type) { //@loc(tswitch, "switch") + default: + break //@highlight("break", tswitch, "break") + } + + // value switch + switch { //@loc(vswitch, "switch") + default: + break //@highlight("break", vswitch, "break") + } + } +} From 607b6641fe2fd4f4d22f30706e31719456a7e269 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 20 Feb 2024 12:31:38 -0500 Subject: [PATCH 24/47] gopls/internal/cache: fix two bugs related to workspace packages Fix two bugs related to workspace packages that contributed to golang/go#65801. The first is that we didn't consider packages with open files to be workspace packages. This was pre-existing behavior, but in the past we simply relied on diagnoseChangedFiles to provide diagnostics for open packages, even though we didn't consider them to be part of the workspace. That led to races and inconsistent behavior: a change in one file would wipe out diagnostics in another, and so on. It's much simpler to say that if the package is open, it is part of the workspace. This leads to consistent behavior, no matter where diagnostics originate. The second bug is related to loading std and cmd. The new workspace package heuristics relied on go/packages.Package.Module to determine if a package is included in the workspace. For std and cmd, this field is nil (golang/go#65816), apparently due to limitations of `go list`. As a result, we were finding no workspace packages for std or cmd. Fix this by falling back to searching for the relevant go.mod file in the filesystem. Unfortunately this required reinstating the lockedSnapshot file source. These bugs revealed a rather large gap in our test coverage for std. Add a test that verifies that we compute std workspace packages. Fixes golang/go#65801 Change-Id: Ic454d4a43e34af10e1224755a09d6c94c728c97d Reviewed-on: https://go-review.googlesource.com/c/tools/+/565475 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/load.go | 43 +++++- gopls/internal/cache/snapshot.go | 20 ++- .../test/integration/workspace/std_test.go | 73 ++++++++++ .../integration/workspace/workspace_test.go | 137 +++++++++++++++++- 4 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 gopls/internal/test/integration/workspace/std_test.go diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go index 4bbeb2d160a..74c90cd66f7 100644 --- a/gopls/internal/cache/load.go +++ b/gopls/internal/cache/load.go @@ -27,6 +27,7 @@ import ( "golang.org/x/tools/internal/event/tag" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/packagesinternal" + "golang.org/x/tools/internal/xcontext" ) var loadID uint64 // atomic identifier for loads @@ -283,7 +284,7 @@ func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates))) meta := s.meta.Update(updates) - workspacePackages := computeWorkspacePackagesLocked(s, meta) + workspacePackages := computeWorkspacePackagesLocked(ctx, s, meta) s.meta = meta s.workspacePackages = workspacePackages s.resetActivePackagesLocked() @@ -563,7 +564,7 @@ func computeLoadDiagnostics(ctx context.Context, snapshot *Snapshot, mp *metadat // heuristics. // // s.mu must be held while calling this function. -func isWorkspacePackageLocked(s *Snapshot, meta *metadata.Graph, pkg *metadata.Package) bool { +func isWorkspacePackageLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph, pkg *metadata.Package) bool { if metadata.IsCommandLineArguments(pkg.ID) { // Ad-hoc command-line-arguments packages aren't workspace packages. // With zero-config gopls (golang/go#57979) they should be very rare, as @@ -587,6 +588,16 @@ func isWorkspacePackageLocked(s *Snapshot, meta *metadata.Graph, pkg *metadata.P return containsOpenFileLocked(s, pkg) } + // golang/go#65801: any (non command-line-arguments) open package is a + // workspace package. + // + // Otherwise, we'd display diagnostics for changes in an open package (due to + // the logic of diagnoseChangedFiles), but then erase those diagnostics when + // we do the final diagnostics pass. Diagnostics should be stable. + if containsOpenFileLocked(s, pkg) { + return true + } + // Apply filtering logic. // // Workspace packages must contain at least one non-filtered file. @@ -624,10 +635,24 @@ func isWorkspacePackageLocked(s *Snapshot, meta *metadata.Graph, pkg *metadata.P // In module mode, a workspace package must be contained in a workspace // module. if s.view.moduleMode() { - if pkg.Module == nil { - return false + var modURI protocol.DocumentURI + if pkg.Module != nil { + modURI = protocol.URIFromPath(pkg.Module.GoMod) + } else { + // golang/go#65816: for std and cmd, Module is nil. + // Fall back to an inferior heuristic. + if len(pkg.CompiledGoFiles) == 0 { + return false // need at least one file to guess the go.mod file + } + dir := pkg.CompiledGoFiles[0].Dir() + var err error + modURI, err = findRootPattern(ctx, dir, "go.mod", lockedSnapshot{s}) + if err != nil || modURI == "" { + // err != nil implies context cancellation, in which case the result of + // this query does not matter. + return false + } } - modURI := protocol.URIFromPath(pkg.Module.GoMod) _, ok := s.view.workspaceModFiles[modURI] return ok } @@ -662,10 +687,14 @@ func containsOpenFileLocked(s *Snapshot, mp *metadata.Package) bool { // contain intermediate test variants. // // s.mu must be held while calling this function. -func computeWorkspacePackagesLocked(s *Snapshot, meta *metadata.Graph) immutable.Map[PackageID, PackagePath] { +func computeWorkspacePackagesLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph) immutable.Map[PackageID, PackagePath] { + // The provided context is used for reading snapshot files, which can only + // fail due to context cancellation. Don't let this happen as it could lead + // to inconsistent results. + ctx = xcontext.Detach(ctx) workspacePackages := make(map[PackageID]PackagePath) for _, mp := range meta.Packages { - if !isWorkspacePackageLocked(s, meta, mp) { + if !isWorkspacePackageLocked(ctx, s, meta, mp) { continue } diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 6ac8c031c5b..d81fd208c06 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go @@ -1274,14 +1274,26 @@ func (s *Snapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file s.mu.Lock() defer s.mu.Unlock() - fh, ok := s.files.get(uri) + return lockedSnapshot{s}.ReadFile(ctx, uri) +} + +// lockedSnapshot implements the file.Source interface, while holding s.mu. +// +// TODO(rfindley): This unfortunate type had been eliminated, but it had to be +// restored to fix golang/go#65801. We should endeavor to remove it again. +type lockedSnapshot struct { + s *Snapshot +} + +func (s lockedSnapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) { + fh, ok := s.s.files.get(uri) if !ok { var err error - fh, err = s.view.fs.ReadFile(ctx, uri) + fh, err = s.s.view.fs.ReadFile(ctx, uri) if err != nil { return nil, err } - s.files.set(uri, fh) + s.s.files.set(uri, fh) } return fh, nil } @@ -2017,7 +2029,7 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f // Update workspace and active packages, if necessary. if result.meta != s.meta || anyFileOpenedOrClosed { needsDiagnosis = true - result.workspacePackages = computeWorkspacePackagesLocked(result, result.meta) + result.workspacePackages = computeWorkspacePackagesLocked(ctx, result, result.meta) result.resetActivePackagesLocked() } else { result.workspacePackages = s.workspacePackages diff --git a/gopls/internal/test/integration/workspace/std_test.go b/gopls/internal/test/integration/workspace/std_test.go new file mode 100644 index 00000000000..51c0603f014 --- /dev/null +++ b/gopls/internal/test/integration/workspace/std_test.go @@ -0,0 +1,73 @@ +// 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 workspace + +import ( + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + . "golang.org/x/tools/gopls/internal/test/integration" +) + +func TestStdWorkspace(t *testing.T) { + // This test checks that we actually load workspace packages when opening + // GOROOT. + // + // In golang/go#65801, we failed to do this because go/packages returns nil + // Module for std and cmd. + // + // Because this test loads std as a workspace, it may be slow on smaller + // builders. + if testing.Short() { + t.Skip("skipping with -short: loads GOROOT") + } + + // The test also fails on Windows because an absolute path does not match + // (likely a misspelling due to slashes). + // TODO(rfindley): investigate and fix this on windows. + if runtime.GOOS == "windows" { + t.Skip("skipping on windows: fails to to misspelled paths") + } + + // Query GOROOT. This is slightly more precise than e.g. runtime.GOROOT, as + // it queries the go command in the environment. + goroot, err := exec.Command("go", "env", "GOROOT").Output() + if err != nil { + t.Fatal(err) + } + stdDir := filepath.Join(strings.TrimSpace(string(goroot)), "src") + WithOptions( + Modes(Default), // This test may be slow. No reason to run it multiple times. + WorkspaceFolders(stdDir), + ).Run(t, "", func(t *testing.T, env *Env) { + // Find parser.ParseFile. Query with `'` to get an exact match. + syms := env.Symbol("'go/parser.ParseFile") + if len(syms) != 1 { + t.Fatalf("got %d symbols, want exactly 1. Symbols:\n%v", len(syms), syms) + } + parserPath := syms[0].Location.URI.Path() + env.OpenFile(parserPath) + + // Find the reference to ast.File from the signature of ParseFile. This + // helps guard against matching a comment. + astFile := env.RegexpSearch(parserPath, `func ParseFile\(.*ast\.(File)`) + refs := env.References(astFile) + + // If we've successfully loaded workspace packages for std, we should find + // a reference in go/types. + foundGoTypesReference := false + for _, ref := range refs { + if strings.Contains(string(ref.URI), "go/types") { + foundGoTypesReference = true + } + } + if !foundGoTypesReference { + t.Errorf("references(ast.File) did not return a go/types reference. Refs:\n%v", refs) + } + }) +} diff --git a/gopls/internal/test/integration/workspace/workspace_test.go b/gopls/internal/test/integration/workspace/workspace_test.go index 28b3978a8cd..e2819404dfa 100644 --- a/gopls/internal/test/integration/workspace/workspace_test.go +++ b/gopls/internal/test/integration/workspace/workspace_test.go @@ -7,9 +7,11 @@ package workspace import ( "context" "fmt" + "sort" "strings" "testing" + "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/hooks" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/test/integration/fake" @@ -1123,16 +1125,139 @@ import ( env.AfterChange( Diagnostics(env.AtRegexp("a/main.go", "V"), WithMessage("not used")), ) + // Here, diagnostics are added because of zero-config gopls. + // In the past, they were added simply due to diagnosing changed files. + // (see TestClearNonWorkspaceDiagnostics_NoView below for a + // reimplementation of that test). + if got, want := len(env.Views()), 2; got != want { + t.Errorf("after opening a/main.go, got %d views, want %d", got, want) + } env.CloseBuffer("a/main.go") + env.AfterChange( + NoDiagnostics(ForFile("a/main.go")), + ) + if got, want := len(env.Views()), 1; got != want { + t.Errorf("after closing a/main.go, got %d views, want %d", got, want) + } + }) +} + +// This test is like TestClearNonWorkspaceDiagnostics, but bypasses the +// zero-config algorithm by opening a nested workspace folder. +// +// We should still compute diagnostics correctly for open packages. +func TestClearNonWorkspaceDiagnostics_NoView(t *testing.T) { + const ws = ` +-- a/go.mod -- +module example.com/a + +go 1.18 + +require example.com/b v1.2.3 + +replace example.com/b => ../b + +-- a/a.go -- +package a + +import "example.com/b" + +func _() { + V := b.B // unused +} + +-- b/go.mod -- +module b + +go 1.18 + +-- b/b.go -- +package b + +const B = 2 + +func _() { + var V int // unused +} + +-- b/b2.go -- +package b + +const B2 = B + +-- c/c.go -- +package main + +func main() { + var V int // unused +} +` + WithOptions( + WorkspaceFolders("a"), + ).Run(t, ws, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + NoDiagnostics(ForFile("c/c.go")), + ) + env.OpenFile("b/b.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + Diagnostics(env.AtRegexp("b/b.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("c/c.go")), + ) - // Make an arbitrary edit because gopls explicitly diagnoses a/main.go - // whenever it is "changed". + // Opening b/b.go should not result in a new view, because b is not + // contained in a workspace folder. // - // TODO(rfindley): it should not be necessary to make another edit here. - // Gopls should be smart enough to avoid diagnosing a. - env.RegexpReplace("b/main.go", "package b", "package b // a package") + // Yet we should get diagnostics for b, because it is open. + if got, want := len(env.Views()), 1; got != want { + t.Errorf("after opening b/b.go, got %d views, want %d", got, want) + } + env.CloseBuffer("b/b.go") env.AfterChange( - NoDiagnostics(ForFile("a/main.go")), + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + NoDiagnostics(ForFile("c/c.go")), + ) + + // We should get references in the b package. + bUse := env.RegexpSearch("a/a.go", `b\.(B)`) + refs := env.References(bUse) + wantRefs := []string{"a/a.go", "b/b.go", "b/b2.go"} + var gotRefs []string + for _, ref := range refs { + gotRefs = append(gotRefs, env.Sandbox.Workdir.URIToPath(ref.URI)) + } + sort.Strings(gotRefs) + if diff := cmp.Diff(wantRefs, gotRefs); diff != "" { + t.Errorf("references(b.B) mismatch (-want +got)\n%s", diff) + } + + // Opening c/c.go should also not result in a new view, yet we should get + // orphaned file diagnostics. + env.OpenFile("c/c.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + Diagnostics(env.AtRegexp("c/c.go", "V"), WithMessage("not used")), + ) + if got, want := len(env.Views()), 1; got != want { + t.Errorf("after opening b/b.go, got %d views, want %d", got, want) + } + + env.CloseBuffer("c/c.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + NoDiagnostics(ForFile("c/c.go")), + ) + env.CloseBuffer("a/a.go") + env.AfterChange( + Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), + NoDiagnostics(ForFile("b/b.go")), + NoDiagnostics(ForFile("c/c.go")), ) }) } From 3f67f80b66d9a03dbab341d333bf251be1c3dd16 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 20 Feb 2024 13:28:23 -0500 Subject: [PATCH 25/47] go/packages/gopackages: display module Updates golang/go#65816 Change-Id: I76db031fc8c1b23caeabe0360db34e1126184e2f Reviewed-on: https://go-review.googlesource.com/c/tools/+/565477 LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Matloob --- go/packages/gopackages/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go index bf0b5043fc6..706f13a99a0 100644 --- a/go/packages/gopackages/main.go +++ b/go/packages/gopackages/main.go @@ -104,6 +104,7 @@ func (app *application) Run(ctx context.Context, args ...string) error { default: return tool.CommandLineErrorf("invalid mode: %s", app.Mode) } + cfg.Mode |= packages.NeedModule lpkgs, err := packages.Load(cfg, args...) if err != nil { @@ -162,6 +163,9 @@ func (app *application) print(lpkg *packages.Package) { kind += "package" } fmt.Printf("Go %s %q:\n", kind, lpkg.ID) // unique ID + if mod := lpkg.Module; mod != nil { + fmt.Printf("\tmodule %s@%s\n", mod.Path, mod.Version) + } fmt.Printf("\tpackage %s\n", lpkg.Name) // characterize type info From a821e617f30094a078bfdc1d63623b93365b3130 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Tue, 20 Feb 2024 16:16:57 -0500 Subject: [PATCH 26/47] gopls/internal/cache: don't create Views for vendored modules We should not create Views for vendored modules, just as we don't create Views for modules in the module cache. With this change, gopls behaves similarly to gopls@v0.14.2 when navigating around the Kubernetes repo. Also add some test coverage that vendored packages are not workspace packages. Fixes golang/go#65830 Change-Id: If9883dc9616774952bd49c395e1c0d37ad3c2a6a Reviewed-on: https://go-review.googlesource.com/c/tools/+/565458 Reviewed-by: Alan Donovan Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/session.go | 12 +++- .../test/integration/misc/definition_test.go | 4 +- .../test/integration/misc/references_test.go | 6 +- .../test/integration/workspace/vendor_test.go | 67 +++++++++++++++++++ .../integration/workspace/zero_config_test.go | 56 ++++++++++++++++ 5 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 gopls/internal/test/integration/workspace/vendor_test.go diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go index 7d2bf43773f..3ad78336c5c 100644 --- a/gopls/internal/cache/session.go +++ b/gopls/internal/cache/session.go @@ -470,7 +470,17 @@ func selectViewDefs(ctx context.Context, fs file.Source, folders []*Folder, open folderForFile := func(uri protocol.DocumentURI) *Folder { var longest *Folder for _, folder := range folders { - if (longest == nil || len(folder.Dir) > len(longest.Dir)) && folder.Dir.Encloses(uri) { + // Check that this is a better match than longest, but not through a + // vendor directory. Count occurrences of "/vendor/" as a quick check + // that the vendor directory is between the folder and the file. Note the + // addition of a trailing "/" to handle the odd case where the folder is named + // vendor (which I hope is exceedingly rare in any case). + // + // Vendored packages are, by definition, part of an existing view. + if (longest == nil || len(folder.Dir) > len(longest.Dir)) && + folder.Dir.Encloses(uri) && + strings.Count(string(uri), "/vendor/") == strings.Count(string(folder.Dir)+"/", "/vendor/") { + longest = folder } } diff --git a/gopls/internal/test/integration/misc/definition_test.go b/gopls/internal/test/integration/misc/definition_test.go index b7394b8dd67..6b364e2e9d5 100644 --- a/gopls/internal/test/integration/misc/definition_test.go +++ b/gopls/internal/test/integration/misc/definition_test.go @@ -495,9 +495,7 @@ const _ = b.K } // Run 'go mod vendor' outside the editor. - if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, nil, true); err != nil { - t.Fatalf("go mod vendor: %v", err) - } + env.RunGoCommand("mod", "vendor") // Synchronize changes to watched files. env.Await(env.DoneWithChangeWatchedFiles()) diff --git a/gopls/internal/test/integration/misc/references_test.go b/gopls/internal/test/integration/misc/references_test.go index fcd72d85c68..73e4fffe3b8 100644 --- a/gopls/internal/test/integration/misc/references_test.go +++ b/gopls/internal/test/integration/misc/references_test.go @@ -360,8 +360,6 @@ func _() { // implementations in vendored modules were not found. The actual fix // was the same as for #55995; see TestVendoringInvalidatesMetadata. func TestImplementationsInVendor(t *testing.T) { - t.Skip("golang/go#56169: file watching does not capture vendor dirs") - const proxy = ` -- other.com/b@v1.0.0/go.mod -- module other.com/b @@ -415,9 +413,7 @@ var _ b.B checkVendor(env.Implementations(refLoc), false) // Run 'go mod vendor' outside the editor. - if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, nil, true); err != nil { - t.Fatalf("go mod vendor: %v", err) - } + env.RunGoCommand("mod", "vendor") // Synchronize changes to watched files. env.Await(env.DoneWithChangeWatchedFiles()) diff --git a/gopls/internal/test/integration/workspace/vendor_test.go b/gopls/internal/test/integration/workspace/vendor_test.go new file mode 100644 index 00000000000..f14cf539de0 --- /dev/null +++ b/gopls/internal/test/integration/workspace/vendor_test.go @@ -0,0 +1,67 @@ +// 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 workspace + +import ( + "testing" + + . "golang.org/x/tools/gopls/internal/test/integration" +) + +func TestWorkspacePackagesExcludesVendor(t *testing.T) { + // This test verifies that packages in the vendor directory are not workspace + // packages. This would be an easy mistake for gopls to make, since mod + // vendoring excludes go.mod files, and therefore the nearest go.mod file for + // vendored packages is often the workspace mod file. + const proxy = ` +-- other.com/b@v1.0.0/go.mod -- +module other.com/b + +go 1.18 + +-- other.com/b@v1.0.0/b.go -- +package b + +type B int + +func _() { + var V int // unused +} +` + const src = ` +-- go.mod -- +module example.com/a +go 1.14 +require other.com/b v1.0.0 + +-- go.sum -- +other.com/b v1.0.0 h1:ct1+0RPozzMvA2rSYnVvIfr/GDHcd7oVnw147okdi3g= +other.com/b v1.0.0/go.mod h1:bfTSZo/4ZtAQJWBYScopwW6n9Ctfsl2mi8nXsqjDXR8= + +-- a.go -- +package a + +import "other.com/b" + +var _ b.B + +` + WithOptions( + ProxyFiles(proxy), + Modes(Default), + ).Run(t, src, func(t *testing.T, env *Env) { + env.RunGoCommand("mod", "vendor") + // Uncomment for updated go.sum contents. + // env.DumpGoSum(".") + env.OpenFile("a.go") + env.AfterChange( + NoDiagnostics(), // as b is not a workspace package + ) + env.GoToDefinition(env.RegexpSearch("a.go", `b\.(B)`)) + env.AfterChange( + Diagnostics(env.AtRegexp("vendor/other.com/b/b.go", "V"), WithMessage("not used")), + ) + }) +} diff --git a/gopls/internal/test/integration/workspace/zero_config_test.go b/gopls/internal/test/integration/workspace/zero_config_test.go index 93c24886c9e..57498831a7d 100644 --- a/gopls/internal/test/integration/workspace/zero_config_test.go +++ b/gopls/internal/test/integration/workspace/zero_config_test.go @@ -5,6 +5,7 @@ package workspace import ( + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -268,3 +269,58 @@ package b } }) } + +func TestVendorExcluded(t *testing.T) { + // Test that we don't create Views for vendored modules. + // + // We construct the vendor directory manually here, as `go mod vendor` will + // omit the go.mod file. This synthesizes the setup of Kubernetes, where the + // entire module is vendored through a symlinked directory. + const src = ` +-- go.mod -- +module example.com/a + +go 1.18 + +require other.com/b v1.0.0 + +-- a.go -- +package a +import "other.com/b" +var _ b.B + +-- vendor/modules.txt -- +# other.com/b v1.0.0 +## explicit; go 1.14 +other.com/b + +-- vendor/other.com/b/go.mod -- +module other.com/b +go 1.14 + +-- vendor/other.com/b/b.go -- +package b +type B int + +func _() { + var V int // unused +} +` + WithOptions( + Modes(Default), + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("a.go") + env.AfterChange(NoDiagnostics()) + loc := env.GoToDefinition(env.RegexpSearch("a.go", `b\.(B)`)) + if !strings.Contains(string(loc.URI), "/vendor/") { + t.Fatalf("Definition(b.B) = %v, want vendored location", loc.URI) + } + env.AfterChange( + Diagnostics(env.AtRegexp("vendor/other.com/b/b.go", "V"), WithMessage("not used")), + ) + + if views := env.Views(); len(views) != 1 { + t.Errorf("After opening /vendor/, got %d views, want 1. Views:\n%v", len(views), views) + } + }) +} From c111c4dfab5efbd271b147a46658b12d789420ce Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 17 Feb 2024 13:23:35 -0500 Subject: [PATCH 27/47] internal/typesinternal: add ReceiverNamed helper ...and factor numerous places to use it. (This pattern kept recurring during my types.Alias audit, golang/go#65294.) Change-Id: I93228b735f7a8ff70df5c998017437d43742d9f3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/565075 Auto-Submit: Alan Donovan Reviewed-by: Jonathan Amsterdam Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- cmd/deadcode/deadcode.go | 9 ++---- cmd/guru/describe.go | 7 ++--- go/analysis/passes/httpmux/httpmux.go | 8 ++--- .../passes/httpresponse/httpresponse.go | 4 ++- go/analysis/passes/loopclosure/loopclosure.go | 8 ++--- go/analysis/passes/slog/slog.go | 26 ++++++---------- go/analysis/passes/unmarshal/unmarshal.go | 9 ++---- go/analysis/passes/unusedwrite/unusedwrite.go | 8 +++-- go/ssa/util.go | 19 +++++------- go/types/objectpath/objectpath.go | 10 ++---- gopls/internal/golang/code_lens.go | 9 ++---- gopls/internal/golang/hover.go | 7 ++--- gopls/internal/golang/rename.go | 9 ++---- internal/apidiff/compatibility.go | 31 ++++++------------- internal/gcimporter/gcimporter.go | 7 ----- internal/gcimporter/iimport.go | 16 ++-------- internal/typeparams/common.go | 11 +++---- internal/typesinternal/recv.go | 24 ++++++++++++++ 18 files changed, 91 insertions(+), 131 deletions(-) create mode 100644 internal/typesinternal/recv.go diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index 8ee439b06d0..a8b4f073831 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -33,7 +33,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -385,11 +385,8 @@ func prettyName(fn *ssa.Function, qualified bool) string { // method receiver? if recv := fn.Signature.Recv(); recv != nil { - t := recv.Type() - if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok { - t = ptr.Elem() - } - buf.WriteString(aliases.Unalias(t).(*types.Named).Obj().Name()) + _, named := typesinternal.ReceiverNamed(recv) + buf.WriteString(named.Obj().Name()) buf.WriteByte('.') } diff --git a/cmd/guru/describe.go b/cmd/guru/describe.go index 0e4964428d5..273d5e2d73c 100644 --- a/cmd/guru/describe.go +++ b/cmd/guru/describe.go @@ -19,6 +19,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" ) // describe describes the syntax node denoted by the query position, @@ -911,11 +912,7 @@ func accessibleFields(recv types.Type, from *types.Package) []describeField { // Handle recursion through anonymous fields. if f.Anonymous() { - tf := f.Type() - if ptr, ok := tf.(*types.Pointer); ok { - tf = ptr.Elem() - } - if named, ok := tf.(*types.Named); ok { // (be defensive) + if _, named := typesinternal.ReceiverNamed(f); named != nil { // If we've already visited this named type // on this path, break the cycle. for _, x := range stack { diff --git a/go/analysis/passes/httpmux/httpmux.go b/go/analysis/passes/httpmux/httpmux.go index fa99296b5ec..d13e8aab37b 100644 --- a/go/analysis/passes/httpmux/httpmux.go +++ b/go/analysis/passes/httpmux/httpmux.go @@ -17,6 +17,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" ) const Doc = `report using Go 1.22 enhanced ServeMux patterns in older Go versions @@ -83,11 +84,8 @@ func isServeMuxRegisterCall(pass *analysis.Pass, call *ast.CallExpr) bool { if !isMethodNamed(fn, "net/http", "Handle", "HandleFunc") { return false } - t, ok := fn.Type().(*types.Signature).Recv().Type().(*types.Pointer) - if !ok { - return false - } - return analysisutil.IsNamedType(t.Elem(), "net/http", "ServeMux") + isPtr, named := typesinternal.ReceiverNamed(fn.Type().(*types.Signature).Recv()) + return isPtr && analysisutil.IsNamedType(named, "net/http", "ServeMux") } func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool { diff --git a/go/analysis/passes/httpresponse/httpresponse.go b/go/analysis/passes/httpresponse/httpresponse.go index c6b6c81b420..8cb046e16da 100644 --- a/go/analysis/passes/httpresponse/httpresponse.go +++ b/go/analysis/passes/httpresponse/httpresponse.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/typesinternal" ) const Doc = `check for mistakes using HTTP responses @@ -116,7 +117,8 @@ func isHTTPFuncOrMethodOnClient(info *types.Info, expr *ast.CallExpr) bool { if res.Len() != 2 { return false // the function called does not return two values. } - if ptr, ok := res.At(0).Type().(*types.Pointer); !ok || !analysisutil.IsNamedType(ptr.Elem(), "net/http", "Response") { + isPtr, named := typesinternal.ReceiverNamed(res.At(0)) + if !isPtr || !analysisutil.IsNamedType(named, "net/http", "Response") { return false // the first return type is not *http.Response. } diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 4724c9f3b1a..7b78939c86e 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" ) @@ -367,9 +368,6 @@ func isMethodCall(info *types.Info, expr ast.Expr, pkgPath, typeName, method str // Check that the receiver is a . or // *.. - rtype := recv.Type() - if ptr, ok := recv.Type().(*types.Pointer); ok { - rtype = ptr.Elem() - } - return analysisutil.IsNamedType(rtype, pkgPath, typeName) + _, named := typesinternal.ReceiverNamed(recv) + return analysisutil.IsNamedType(named, pkgPath, typeName) } diff --git a/go/analysis/passes/slog/slog.go b/go/analysis/passes/slog/slog.go index a1323c3e666..b3c683b61cb 100644 --- a/go/analysis/passes/slog/slog.go +++ b/go/analysis/passes/slog/slog.go @@ -20,6 +20,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -150,14 +151,10 @@ func isAttr(t types.Type) bool { func shortName(fn *types.Func) string { var r string if recv := fn.Type().(*types.Signature).Recv(); recv != nil { - t := recv.Type() - if pt, ok := t.(*types.Pointer); ok { - t = pt.Elem() - } - if nt, ok := t.(*types.Named); ok { - r = nt.Obj().Name() + if _, named := typesinternal.ReceiverNamed(recv); named != nil { + r = named.Obj().Name() } else { - r = recv.Type().String() + r = recv.Type().String() // anon struct/interface } r += "." } @@ -173,17 +170,12 @@ func kvFuncSkipArgs(fn *types.Func) (int, bool) { return 0, false } var recvName string // by default a slog package function - recv := fn.Type().(*types.Signature).Recv() - if recv != nil { - t := recv.Type() - if pt, ok := t.(*types.Pointer); ok { - t = pt.Elem() - } - if nt, ok := t.(*types.Named); !ok { - return 0, false - } else { - recvName = nt.Obj().Name() + if recv := fn.Type().(*types.Signature).Recv(); recv != nil { + _, named := typesinternal.ReceiverNamed(recv) + if named == nil { + return 0, false // anon struct/interface } + recvName = named.Obj().Name() } skip, ok := kvFuncs[recvName][fn.Name()] return skip, ok diff --git a/go/analysis/passes/unmarshal/unmarshal.go b/go/analysis/passes/unmarshal/unmarshal.go index f4e73528b43..a7889fa4590 100644 --- a/go/analysis/passes/unmarshal/unmarshal.go +++ b/go/analysis/passes/unmarshal/unmarshal.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -69,12 +70,8 @@ func run(pass *analysis.Pass) (interface{}, error) { // (*"encoding/json".Decoder).Decode // (* "encoding/gob".Decoder).Decode // (* "encoding/xml".Decoder).Decode - t := recv.Type() - if ptr, ok := t.(*types.Pointer); ok { - t = ptr.Elem() - } - tname := t.(*types.Named).Obj() - if tname.Name() == "Decoder" { + _, named := typesinternal.ReceiverNamed(recv) + if tname := named.Obj(); tname.Name() == "Decoder" { switch tname.Pkg().Path() { case "encoding/json", "encoding/xml", "encoding/gob": argidx = 0 // func(interface{}) diff --git a/go/analysis/passes/unusedwrite/unusedwrite.go b/go/analysis/passes/unusedwrite/unusedwrite.go index f5d0f116cad..cc484620dcc 100644 --- a/go/analysis/passes/unusedwrite/unusedwrite.go +++ b/go/analysis/passes/unusedwrite/unusedwrite.go @@ -13,6 +13,7 @@ import ( "golang.org/x/tools/go/analysis/passes/buildssa" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/aliases" ) //go:embed doc.go @@ -159,10 +160,13 @@ func hasStructOrArrayType(v ssa.Value) bool { // // For example, for struct T {x int, y int), getFieldName(*T, 1) returns "y". func getFieldName(tp types.Type, index int) string { - if pt, ok := tp.(*types.Pointer); ok { + // TODO(adonovan): use + // stp, ok := typeparams.Deref(tp).Underlying().(*types.Struct); ok { + // when Deref is defined. + if pt, ok := aliases.Unalias(tp).(*types.Pointer); ok { tp = pt.Elem() } - if named, ok := tp.(*types.Named); ok { + if named, ok := aliases.Unalias(tp).(*types.Named); ok { tp = named.Underlying() } if stp, ok := tp.(*types.Struct); ok { diff --git a/go/ssa/util.go b/go/ssa/util.go index 6e9f1282b1b..729a4e5532e 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" ) //// Sanity checking utilities @@ -180,17 +181,13 @@ func makeLen(T types.Type) *Builtin { } } -// receiverTypeArgs returns the type arguments to a function's receiver. -// Returns an empty list if obj does not have a receiver or its receiver does not have type arguments. -func receiverTypeArgs(obj *types.Func) []types.Type { - rtype := recvType(obj) - if rtype == nil { - return nil - } - rtype, _ = deptr(rtype) - named, ok := rtype.(*types.Named) - if !ok { - return nil +// receiverTypeArgs returns the type arguments to a method's receiver. +// Returns an empty list if the receiver does not have type arguments. +func receiverTypeArgs(method *types.Func) []types.Type { + recv := method.Type().(*types.Signature).Recv() + _, named := typesinternal.ReceiverNamed(recv) + if named == nil { + return nil // recv is anonymous struct/interface } ts := named.TypeArgs() if ts.Len() == 0 { diff --git a/go/types/objectpath/objectpath.go b/go/types/objectpath/objectpath.go index 11d5c8c3adf..62d9585a691 100644 --- a/go/types/objectpath/objectpath.go +++ b/go/types/objectpath/objectpath.go @@ -30,6 +30,7 @@ import ( "strings" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" ) // A Path is an opaque name that identifies a types.Object @@ -395,13 +396,8 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) { return "", false } - recvT := meth.Type().(*types.Signature).Recv().Type() - if ptr, ok := recvT.(*types.Pointer); ok { - recvT = ptr.Elem() - } - - named, ok := recvT.(*types.Named) - if !ok { + _, named := typesinternal.ReceiverNamed(meth.Type().(*types.Signature).Recv()) + if named == nil { return "", false } diff --git a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go index 53684a58c79..ee33bf26c36 100644 --- a/gopls/internal/golang/code_lens.go +++ b/gopls/internal/golang/code_lens.go @@ -17,6 +17,7 @@ import ( "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" + "golang.org/x/tools/internal/typesinternal" ) type LensFunc func(context.Context, *cache.Snapshot, file.Handle) ([]protocol.CodeLens, error) @@ -152,12 +153,8 @@ func matchTestFunc(fn *ast.FuncDecl, pkg *cache.Package, nameRe *regexp.Regexp, } // Check the type of the only parameter - paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer) - if !ok { - return false - } - named, ok := paramTyp.Elem().(*types.Named) - if !ok { + isptr, named := typesinternal.ReceiverNamed(sig.Params().At(0)) + if !isptr || named == nil { return false } namedObj := named.Obj() diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 05cc5d98fd3..15bd97a18bb 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -38,6 +38,7 @@ import ( "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/tokeninternal" + "golang.org/x/tools/internal/typesinternal" ) // hoverJSON contains the structured result of a hover query. It is @@ -1235,11 +1236,7 @@ func promotedFields(t types.Type, from *types.Package) []promotedField { // Handle recursion through anonymous fields. if f.Anonymous() { - tf := f.Type() - if ptr, ok := tf.(*types.Pointer); ok { - tf = ptr.Elem() - } - if named, ok := tf.(*types.Named); ok { // (be defensive) + if _, named := typesinternal.ReceiverNamed(f); named != nil { // If we've already visited this named type // on this path, break the cycle. for _, x := range stack { diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index c60e5a25240..81114fc6a09 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -69,6 +69,7 @@ import ( "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/refactor/satisfy" ) @@ -1189,12 +1190,8 @@ func (r *renamer) updateCommentDocLinks() (map[protocol.DocumentURI][]diff.Edit, if recv == nil { continue } - recvT := recv.Type() - if ptr, ok := recvT.(*types.Pointer); ok { - recvT = ptr.Elem() - } - named, isNamed := recvT.(*types.Named) - if !isNamed { + _, named := typesinternal.ReceiverNamed(recv) + if named == nil { continue } // Doc links can't reference interface methods. diff --git a/internal/apidiff/compatibility.go b/internal/apidiff/compatibility.go index 0d2d2b34575..64cad5337be 100644 --- a/internal/apidiff/compatibility.go +++ b/internal/apidiff/compatibility.go @@ -10,6 +10,7 @@ import ( "reflect" "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) func (d *differ) checkCompatible(otn *types.TypeName, old, new types.Type) { @@ -305,7 +306,8 @@ func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addc // T and one for the embedded type U. We want both messages to appear, // but the messageSet dedup logic will allow only one message for a given // object. So use the part string to distinguish them. - if receiverNamedType(oldMethod).Obj() != otn { + recv := oldMethod.Type().(*types.Signature).Recv() + if _, named := typesinternal.ReceiverNamed(recv); named.Obj() != otn { part = fmt.Sprintf(", method set of %s", msname) } d.incompatible(oldMethod, part, "removed") @@ -336,11 +338,11 @@ func (d *differ) checkMethodSet(otn *types.TypeName, oldt, newt types.Type, addc } // exportedMethods collects all the exported methods of type's method set. -func exportedMethods(t types.Type) map[string]types.Object { - m := map[string]types.Object{} +func exportedMethods(t types.Type) map[string]*types.Func { + m := make(map[string]*types.Func) ms := types.NewMethodSet(t) for i := 0; i < ms.Len(); i++ { - obj := ms.At(i).Obj() + obj := ms.At(i).Obj().(*types.Func) if obj.Exported() { m[obj.Name()] = obj } @@ -348,22 +350,7 @@ func exportedMethods(t types.Type) map[string]types.Object { return m } -func receiverType(method types.Object) types.Type { - return method.Type().(*types.Signature).Recv().Type() -} - -func receiverNamedType(method types.Object) *types.Named { - switch t := aliases.Unalias(receiverType(method)).(type) { - case *types.Pointer: - return aliases.Unalias(t.Elem()).(*types.Named) - case *types.Named: - return t - default: - panic("unreachable") - } -} - -func hasPointerReceiver(method types.Object) bool { - _, ok := aliases.Unalias(receiverType(method)).(*types.Pointer) - return ok +func hasPointerReceiver(method *types.Func) bool { + isptr, _ := typesinternal.ReceiverNamed(method.Type().(*types.Signature).Recv()) + return isptr } diff --git a/internal/gcimporter/gcimporter.go b/internal/gcimporter/gcimporter.go index 2d078ccb19c..39df91124a4 100644 --- a/internal/gcimporter/gcimporter.go +++ b/internal/gcimporter/gcimporter.go @@ -259,13 +259,6 @@ func Import(packages map[string]*types.Package, path, srcDir string, lookup func return } -func deref(typ types.Type) types.Type { - if p, _ := typ.(*types.Pointer); p != nil { - return p.Elem() - } - return typ -} - type byPath []*types.Package func (a byPath) Len() int { return len(a) } diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index 9fffa9ad05c..778caf6078e 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -22,6 +22,7 @@ import ( "strings" "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/internal/typesinternal" ) type intReader struct { @@ -587,9 +588,8 @@ func (r *importReader) obj(name string) { // If the receiver has any targs, set those as the // rparams of the method (since those are the // typeparams being used in the method sig/body). - base := baseType(recv.Type()) - assert(base != nil) - targs := base.TypeArgs() + _, recvNamed := typesinternal.ReceiverNamed(recv) + targs := recvNamed.TypeArgs() var rparams []*types.TypeParam if targs.Len() > 0 { rparams = make([]*types.TypeParam, targs.Len()) @@ -1077,13 +1077,3 @@ func (r *importReader) byte() byte { } return x } - -func baseType(typ types.Type) *types.Named { - // pointer receivers are never types.Named types - if p, _ := typ.(*types.Pointer); p != nil { - typ = p.Elem() - } - // receiver base types are always (possibly generic) types.Named types - n, _ := typ.(*types.Named) - return n -} diff --git a/internal/typeparams/common.go b/internal/typeparams/common.go index cdab9885314..e5e3ed313cd 100644 --- a/internal/typeparams/common.go +++ b/internal/typeparams/common.go @@ -27,6 +27,8 @@ import ( "go/ast" "go/token" "go/types" + + "golang.org/x/tools/internal/typesinternal" ) // UnpackIndexExpr extracts data from AST nodes that represent index @@ -90,13 +92,8 @@ func OriginMethod(fn *types.Func) *types.Func { if recv == nil { return fn } - base := recv.Type() - p, isPtr := base.(*types.Pointer) - if isPtr { - base = p.Elem() - } - named, isNamed := base.(*types.Named) - if !isNamed { + _, named := typesinternal.ReceiverNamed(recv) + if named == nil { // Receiver is a *types.Interface. return fn } diff --git a/internal/typesinternal/recv.go b/internal/typesinternal/recv.go new file mode 100644 index 00000000000..1a7b7264d8f --- /dev/null +++ b/internal/typesinternal/recv.go @@ -0,0 +1,24 @@ +// 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 ( + "go/types" + + "golang.org/x/tools/internal/aliases" +) + +// ReceiverNamed returns the named type (if any) associated with the +// type of recv, which may be of the form N or *N, or aliases thereof. +// It also reports whether a Pointer was present. +func ReceiverNamed(recv *types.Var) (isPtr bool, named *types.Named) { + t := recv.Type() + if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok { + isPtr = true + t = ptr.Elem() + } + named, _ = aliases.Unalias(t).(*types.Named) + return +} From a4d9215280924a0b802ffdd747e1f0ead788f7ef Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 22 Feb 2024 16:09:42 +0000 Subject: [PATCH 28/47] gopls/internal/test/marker: add a test for initialization cycle errors CL 565838 changed the way initialization cycle errors were reporting, and would have broken gopls' parsing of continuation errors, yet no x/tools tests failed. Add a test for this behavior. Due to golang/go#65877, duplicate diagnostics had to be suppressed for this test. Updates golang/go#65877 Change-Id: I48244ac469ab78d2e40bf92ec061671cef72c6d9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/566075 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- .../marker/testdata/diagnostics/initcycle.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 gopls/internal/test/marker/testdata/diagnostics/initcycle.txt diff --git a/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt b/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt new file mode 100644 index 00000000000..f306bccf52c --- /dev/null +++ b/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt @@ -0,0 +1,17 @@ +This test verifies that gopls spreads initialization cycle errors across +multiple declarations. + +We set -ignore_extra_diags due to golang/go#65877: gopls produces redundant +diagnostics for initialization cycles. + +-- flags -- +-ignore_extra_diags + +-- p.go -- +package p + +var X = Y //@diag("X", re"initialization cycle") + +var Y = Z //@diag("Y", re"initialization cycle") + +var Z = X //@diag("Z", re"initialization cycle") From 054c06df410b0347e908c647ef441733aea4a8df Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 20 Feb 2024 12:53:47 -0500 Subject: [PATCH 29/47] gopls: rationalize "deref" helpers This change (part of the types.Alias audit) defines two helper functions for dealing with pointer types: 1. internal/typeparams.MustDeref (formerly go/ssa.mustDeref) is the type equivalent of a LOAD instruction. 2. internal/typesinternal.Unpointer strips off a Pointer constructor (possibly wrapped in an Alias). The golang.Deref function, which recursively strips off Pointer and Named constructors, is a meaningless operation. It has been replaced in all cases by Unpointer + Unalias. There are far too many functions called 'deref' with subtle variations in their behavior. I plan to standardize x/tools on few common idioms. (There is more to do.) Updates golang/go#65294 Change-Id: I502bab95e8d954715784b7e35ec801f4be4bc959 Reviewed-on: https://go-review.googlesource.com/c/tools/+/565476 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Reviewed-by: Tim King --- go/ssa/builder.go | 6 ++--- go/ssa/create.go | 1 + go/ssa/emit.go | 6 +++-- go/ssa/func.go | 4 +++- go/ssa/lift.go | 6 +++-- go/ssa/lvalue.go | 4 +++- go/ssa/print.go | 7 +++--- go/ssa/util.go | 9 ------- .../internal/golang/completion/completion.go | 10 ++++---- gopls/internal/golang/completion/literal.go | 16 +++++++++---- .../golang/completion/postfix_snippets.go | 4 +++- gopls/internal/golang/completion/util.go | 8 ++++++- gopls/internal/golang/hover.go | 2 +- gopls/internal/golang/identifier.go | 17 +++++++++---- gopls/internal/golang/rename_check.go | 6 +++-- gopls/internal/golang/util.go | 24 ------------------- internal/typeparams/coretype.go | 13 ++++++++++ internal/typesinternal/recv.go | 19 +++++++++++++++ 18 files changed, 100 insertions(+), 62 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 8622dfc53a8..ecbc3e4f566 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -328,7 +328,7 @@ func (b *builder) builtin(fn *Function, obj *types.Builtin, args []ast.Expr, typ } case "new": - return emitNew(fn, mustDeref(typ), pos, "new") + return emitNew(fn, typeparams.MustDeref(typ), pos, "new") case "len", "cap": // Special case: len or cap of an array or *array is @@ -419,7 +419,7 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { wantAddr := true v := b.receiver(fn, e.X, wantAddr, escaping, sel) index := sel.index[len(sel.index)-1] - fld := fieldOf(mustDeref(v.Type()), index) // v is an addr. + fld := fieldOf(typeparams.MustDeref(v.Type()), index) // v is an addr. // Due to the two phases of resolving AssignStmt, a panic from x.f = p() // when x is nil is required to come after the side-effects of @@ -468,7 +468,7 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { v.setType(et) return fn.emit(v) } - return &lazyAddress{addr: emit, t: mustDeref(et), pos: e.Lbrack, expr: e} + return &lazyAddress{addr: emit, t: typeparams.MustDeref(et), pos: e.Lbrack, expr: e} case *ast.StarExpr: return &address{addr: b.expr(fn, e.X), pos: e.Star, expr: e} diff --git a/go/ssa/create.go b/go/ssa/create.go index c4da35d0b08..4545c178d78 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -259,6 +259,7 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * obj := scope.Lookup(name) memberFromObject(p, obj, nil, "") if obj, ok := obj.(*types.TypeName); ok { + // No Unalias: aliases should not duplicate methods. if named, ok := obj.Type().(*types.Named); ok { for i, n := 0, named.NumMethods(); i < n; i++ { memberFromObject(p, named.Method(i), nil, "") diff --git a/go/ssa/emit.go b/go/ssa/emit.go index d77b4407a80..2a26c9492dc 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -11,6 +11,8 @@ import ( "go/ast" "go/token" "go/types" + + "golang.org/x/tools/internal/typeparams" ) // emitAlloc emits to f a new Alloc instruction allocating a variable @@ -64,7 +66,7 @@ func emitLocalVar(f *Function, v *types.Var) *Alloc { // new temporary, and returns the value so defined. func emitLoad(f *Function, addr Value) *UnOp { v := &UnOp{Op: token.MUL, X: addr} - v.setType(mustDeref(addr.Type())) + v.setType(typeparams.MustDeref(addr.Type())) f.emit(v) return v } @@ -414,7 +416,7 @@ func emitTypeCoercion(f *Function, v Value, typ types.Type) Value { // emitStore emits to f an instruction to store value val at location // addr, applying implicit conversions as required by assignability rules. func emitStore(f *Function, addr, val Value, pos token.Pos) *Store { - typ := mustDeref(addr.Type()) + typ := typeparams.MustDeref(addr.Type()) s := &Store{ Addr: addr, Val: emitConv(f, val, typ), diff --git a/go/ssa/func.go b/go/ssa/func.go index 0ac22046ebf..4d3e39129c5 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -14,6 +14,8 @@ import ( "io" "os" "strings" + + "golang.org/x/tools/internal/typeparams" ) // Like ObjectOf, but panics instead of returning nil. @@ -531,7 +533,7 @@ func WriteFunction(buf *bytes.Buffer, f *Function) { if len(f.Locals) > 0 { buf.WriteString("# Locals:\n") for i, l := range f.Locals { - fmt.Fprintf(buf, "# % 3d:\t%s %s\n", i, l.Name(), relType(mustDeref(l.Type()), from)) + fmt.Fprintf(buf, "# % 3d:\t%s %s\n", i, l.Name(), relType(typeparams.MustDeref(l.Type()), from)) } } writeSignature(buf, from, f.Name(), f.Signature) diff --git a/go/ssa/lift.go b/go/ssa/lift.go index da49fe9f177..8bb1949449f 100644 --- a/go/ssa/lift.go +++ b/go/ssa/lift.go @@ -43,6 +43,8 @@ import ( "go/token" "math/big" "os" + + "golang.org/x/tools/internal/typeparams" ) // If true, show diagnostic information at each step of lifting. @@ -465,7 +467,7 @@ func liftAlloc(df domFrontier, alloc *Alloc, newPhis newPhiMap, fresh *int) bool *fresh++ phi.pos = alloc.Pos() - phi.setType(mustDeref(alloc.Type())) + phi.setType(typeparams.MustDeref(alloc.Type())) phi.block = v if debugLifting { fmt.Fprintf(os.Stderr, "\tplace %s = %s at block %s\n", phi.Name(), phi, v) @@ -510,7 +512,7 @@ func replaceAll(x, y Value) { func renamed(renaming []Value, alloc *Alloc) Value { v := renaming[alloc.index] if v == nil { - v = zeroConst(mustDeref(alloc.Type())) + v = zeroConst(typeparams.MustDeref(alloc.Type())) renaming[alloc.index] = v } return v diff --git a/go/ssa/lvalue.go b/go/ssa/lvalue.go index 186cfcae704..eede307eabd 100644 --- a/go/ssa/lvalue.go +++ b/go/ssa/lvalue.go @@ -11,6 +11,8 @@ import ( "go/ast" "go/token" "go/types" + + "golang.org/x/tools/internal/typeparams" ) // An lvalue represents an assignable location that may appear on the @@ -52,7 +54,7 @@ func (a *address) address(fn *Function) Value { } func (a *address) typ() types.Type { - return mustDeref(a.addr.Type()) + return typeparams.MustDeref(a.addr.Type()) } // An element is an lvalue represented by m[k], the location of an diff --git a/go/ssa/print.go b/go/ssa/print.go index 727a7350265..38d8404fdc4 100644 --- a/go/ssa/print.go +++ b/go/ssa/print.go @@ -17,6 +17,7 @@ import ( "strings" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typeparams" ) // relName returns the name of v relative to i. @@ -94,7 +95,7 @@ func (v *Alloc) String() string { op = "new" } from := v.Parent().relPkg() - return fmt.Sprintf("%s %s (%s)", op, relType(mustDeref(v.Type()), from), v.Comment) + return fmt.Sprintf("%s %s (%s)", op, relType(typeparams.MustDeref(v.Type()), from), v.Comment) } func (v *Phi) String() string { @@ -260,7 +261,7 @@ func (v *MakeChan) String() string { func (v *FieldAddr) String() string { // Be robust against a bad index. name := "?" - if fld := fieldOf(mustDeref(v.X.Type()), v.Field); fld != nil { + if fld := fieldOf(typeparams.MustDeref(v.X.Type()), v.Field); fld != nil { name = fld.Name() } return fmt.Sprintf("&%s.%s [#%d]", relName(v.X, v), name, v.Field) @@ -449,7 +450,7 @@ func WritePackage(buf *bytes.Buffer, p *Package) { case *Global: fmt.Fprintf(buf, " var %-*s %s\n", - maxname, name, relType(mustDeref(mem.Type()), from)) + maxname, name, relType(typeparams.MustDeref(mem.Type()), from)) } } diff --git a/go/ssa/util.go b/go/ssa/util.go index 729a4e5532e..915b4e274c1 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -115,15 +115,6 @@ func deref(typ types.Type) (types.Type, bool) { return typ, false } -// mustDeref returns the element type of a type with a pointer core type. -// Panics on failure. -func mustDeref(typ types.Type) types.Type { - if et, ok := deref(typ); ok { - return et - } - panic("cannot dereference type " + typ.String()) -} - // recvType returns the receiver type of method obj. func recvType(obj *types.Func) types.Type { return obj.Type().(*types.Signature).Recv().Type() diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index b796de4932d..8ff7f0b9cf2 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go @@ -37,10 +37,12 @@ import ( goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" "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" "golang.org/x/tools/internal/fuzzy" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" ) // A CompletionItem represents a possible completion suggested by the algorithm. @@ -1585,7 +1587,7 @@ func (c *completer) lexical(ctx context.Context) error { } if c.inference.objType != nil { - if named, _ := golang.Deref(c.inference.objType).(*types.Named); named != nil { + if named, ok := aliases.Unalias(typesinternal.Unpointer(c.inference.objType)).(*types.Named); ok { // If we expected a named type, check the type's package for // completion items. This is useful when the current file hasn't // imported the type's package yet. @@ -1651,14 +1653,14 @@ func (c *completer) injectType(ctx context.Context, t types.Type) { return } - t = golang.Deref(t) + t = typesinternal.Unpointer(t) // If we have an expected type and it is _not_ a named type, handle // it specially. Non-named types like "[]int" will never be // considered via a lexical search, so we need to directly inject // them. Also allow generic types since lexical search does not // infer instantiated versions of them. - if named, _ := t.(*types.Named); named == nil || named.TypeParams().Len() > 0 { + if named, ok := aliases.Unalias(t).(*types.Named); !ok || named.TypeParams().Len() > 0 { // If our expected type is "[]int", this will add a literal // candidate of "[]int{}". c.literal(ctx, t, nil) @@ -1896,7 +1898,7 @@ func enclosingCompositeLiteral(path []ast.Node, pos token.Pos, info *types.Info) clInfo := compLitInfo{ cl: n, - clType: golang.Deref(tv.Type).Underlying(), + clType: typesinternal.Unpointer(tv.Type).Underlying(), } var ( diff --git a/gopls/internal/golang/completion/literal.go b/gopls/internal/golang/completion/literal.go index 45f772d789f..dc4fc0dcd60 100644 --- a/gopls/internal/golang/completion/literal.go +++ b/gopls/internal/golang/completion/literal.go @@ -14,7 +14,9 @@ import ( "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/typesinternal" ) // literal generates composite literal, function literal, and make() @@ -49,10 +51,11 @@ func (c *completer) literal(ctx context.Context, literalType types.Type, imp *im // // don't offer "mySlice{}" since we have already added a candidate // of "[]int{}". - if _, named := literalType.(*types.Named); named && expType != nil { - if _, named := golang.Deref(expType).(*types.Named); !named { - return - } + if is[*types.Named](aliases.Unalias(literalType)) && + expType != nil && + !is[*types.Named](aliases.Unalias(typesinternal.Unpointer(expType))) { + + return } // Check if an object of type literalType would match our expected type. @@ -589,3 +592,8 @@ func (c *completer) typeParamInScope(tp *types.TypeParam) bool { _, foundObj := scope.LookupParent(obj.Name(), c.pos) return obj == foundObj } + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} diff --git a/gopls/internal/golang/completion/postfix_snippets.go b/gopls/internal/golang/completion/postfix_snippets.go index 252d2e77a90..fad8f784713 100644 --- a/gopls/internal/golang/completion/postfix_snippets.go +++ b/gopls/internal/golang/completion/postfix_snippets.go @@ -21,8 +21,10 @@ import ( "golang.org/x/tools/gopls/internal/golang/completion/snippet" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/typesinternal" ) // Postfix snippets are artificial methods that allow the user to @@ -465,7 +467,7 @@ func (a *postfixTmplArgs) VarName(t types.Type, nonNamedDefault string) string { // go/types predicates are undefined on types.Typ[types.Invalid]. if !types.Identical(t, types.Typ[types.Invalid]) && types.Implements(t, errorIntf) { name = "err" - } else if _, isNamed := golang.Deref(t).(*types.Named); !isNamed { + } else if !is[*types.Named](aliases.Unalias(typesinternal.Unpointer(t))) { name = nonNamedDefault } diff --git a/gopls/internal/golang/completion/util.go b/gopls/internal/golang/completion/util.go index 65d1b4d96dc..8ac7f161e72 100644 --- a/gopls/internal/golang/completion/util.go +++ b/gopls/internal/golang/completion/util.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/typesinternal" ) // exprAtPos returns the index of the expression containing pos. @@ -38,7 +39,12 @@ func eachField(T types.Type, fn func(*types.Var)) { var visit func(T types.Type) visit = func(T types.Type) { - if T, ok := golang.Deref(T).Underlying().(*types.Struct); ok { + // T may be a Struct, optionally Named, with an optional + // Pointer (with optional Aliases at every step!): + // Consider: type T *struct{ f int }; _ = T(nil).f + // + // TODO(adonovan): use typeparams.Deref in next CL. + if T, ok := typesinternal.Unpointer(T.Underlying()).Underlying().(*types.Struct); ok { if seen.At(T) != nil { return } diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 15bd97a18bb..e7b903df26d 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -1226,7 +1226,7 @@ func promotedFields(t types.Type, from *types.Package) []promotedField { var fields []promotedField var visit func(t types.Type, stack []*types.Named) visit = func(t types.Type, stack []*types.Named) { - tStruct, ok := Deref(t).Underlying().(*types.Struct) + tStruct, ok := typesinternal.Unpointer(t).Underlying().(*types.Struct) if !ok { return } diff --git a/gopls/internal/golang/identifier.go b/gopls/internal/golang/identifier.go index 28f89757057..994010fd74b 100644 --- a/gopls/internal/golang/identifier.go +++ b/gopls/internal/golang/identifier.go @@ -8,6 +8,9 @@ import ( "errors" "go/ast" "go/types" + + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" ) // ErrNoIdentFound is error returned when no identifier is found at a particular position @@ -23,24 +26,30 @@ func inferredSignature(info *types.Info, id *ast.Ident) *types.Signature { return sig } +// searchForEnclosing returns, given the AST path to a SelectorExpr, +// the exported named type of the innermost implicit field selection. +// +// For example, given "new(A).d" where this is (due to embedding) a +// shorthand for "new(A).b.c.d", it returns the named type of c, +// if it is exported, otherwise the type of b, or A. func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { for _, n := range path { switch n := n.(type) { case *ast.SelectorExpr: if sel, ok := info.Selections[n]; ok { - recv := Deref(sel.Recv()) + recv := typesinternal.Unpointer(sel.Recv()) // Keep track of the last exported type seen. var exported *types.TypeName - if named, ok := recv.(*types.Named); ok && named.Obj().Exported() { + if named, ok := aliases.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { exported = named.Obj() } // We don't want the last element, as that's the field or // method itself. for _, index := range sel.Index()[:len(sel.Index())-1] { if r, ok := recv.Underlying().(*types.Struct); ok { - recv = Deref(r.Field(index).Type()) - if named, ok := recv.(*types.Named); ok && named.Obj().Exported() { + recv = typesinternal.Unpointer(r.Field(index).Type()) + if named, ok := aliases.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { exported = named.Obj() } } diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 11b154c4e18..ffaff303afa 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -45,6 +45,8 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/aliases" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/refactor/satisfy" ) @@ -371,7 +373,7 @@ func forEachLexicalRef(pkg *cache.Package, obj types.Object, fn func(id *ast.Ide return visit(nil) // pop stack, don't descend } // TODO(adonovan): fix: for generics, should be T.core not T.underlying. - if _, ok := Deref(tv.Type).Underlying().(*types.Struct); ok { + if _, ok := typesinternal.Unpointer(tv.Type).Underlying().(*types.Struct); ok { if n.Type != nil { ast.Inspect(n.Type, visit) } @@ -501,7 +503,7 @@ func (r *renamer) checkStructField(from *types.Var) { if from.Anonymous() { if named, ok := from.Type().(*types.Named); ok { r.check(named.Obj()) - } else if named, ok := Deref(from.Type()).(*types.Named); ok { + } else if named, ok := aliases.Unalias(typesinternal.Unpointer(from.Type())).(*types.Named); ok { r.check(named.Obj()) } } diff --git a/gopls/internal/golang/util.go b/gopls/internal/golang/util.go index 5283a5b6207..c2f5d50d608 100644 --- a/gopls/internal/golang/util.go +++ b/gopls/internal/golang/util.go @@ -113,30 +113,6 @@ func FormatNodeFile(file *token.File, n ast.Node) string { return FormatNode(fset, n) } -// Deref returns a pointer's element type, traversing as many levels as needed. -// Otherwise it returns typ. -// -// It can return a pointer type for cyclic types (see golang/go#45510). -func Deref(typ types.Type) types.Type { - var seen map[types.Type]struct{} - for { - p, ok := typ.Underlying().(*types.Pointer) - if !ok { - return typ - } - if _, ok := seen[p.Elem()]; ok { - return typ - } - - typ = p.Elem() - - if seen == nil { - seen = make(map[types.Type]struct{}) - } - seen[typ] = struct{}{} - } -} - // findFileInDeps finds package metadata containing URI in the transitive // dependencies of m. When using the Go command, the answer is unique. func findFileInDeps(s metadata.Source, mp *metadata.Package, uri protocol.DocumentURI) *metadata.Package { diff --git a/internal/typeparams/coretype.go b/internal/typeparams/coretype.go index 7ea8840eab7..048e2161493 100644 --- a/internal/typeparams/coretype.go +++ b/internal/typeparams/coretype.go @@ -5,6 +5,7 @@ package typeparams import ( + "fmt" "go/types" ) @@ -120,3 +121,15 @@ func _NormalTerms(typ types.Type) ([]*types.Term, error) { return []*types.Term{types.NewTerm(false, typ)}, nil } } + +// MustDeref returns the type of the variable pointed to by t. +// It panics if t's core type is not a pointer. +// +// TODO(adonovan): ideally this would live in typesinternal, but that +// creates an import cycle. Move there when we melt this package down. +func MustDeref(t types.Type) types.Type { + if ptr, ok := CoreType(t).(*types.Pointer); ok { + return ptr.Elem() + } + panic(fmt.Sprintf("%v is not a pointer", t)) +} diff --git a/internal/typesinternal/recv.go b/internal/typesinternal/recv.go index 1a7b7264d8f..fea7c8b75e8 100644 --- a/internal/typesinternal/recv.go +++ b/internal/typesinternal/recv.go @@ -22,3 +22,22 @@ func ReceiverNamed(recv *types.Var) (isPtr bool, named *types.Named) { named, _ = aliases.Unalias(t).(*types.Named) return } + +// Unpointer returns T given *T or an alias thereof. +// For all other types it is the identity function. +// It does not look at underlying types. +// The result may be an alias. +// +// Use this function to strip off the optional pointer on a receiver +// in a field or method selection, without losing the named type +// (which is needed to compute the method set). +// +// See also [typeparams.MustDeref], which removes one level of +// indirection from the type, regardless of named types (analogous to +// a LOAD instruction). +func Unpointer(t types.Type) types.Type { + if ptr, ok := aliases.Unalias(t).(*types.Pointer); ok { + return ptr.Elem() + } + return t +} From fb020a0f68342c7d7e382e3203ded5cb8c6b851e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 10 Jan 2024 16:23:30 -0500 Subject: [PATCH 30/47] go/cfg: record block kind and associated statement This change adds Block.Kind, and enumeration of possible control structures that give rise to blocks, and Block.Stmt, which records the syntax node associated with it. This allows clients to reconstruct the control more accurately, and compute positions. It also adds a CFG.Digraph method to dump the graph in AT&T GraphViz form for debugging convenience, and a simple helper command to call this function. Also, stop using ast.Object. Fixes golang/go#53367 Change-Id: I19557d636eb4c620899463c489411c360540289b Reviewed-on: https://go-review.googlesource.com/c/tools/+/555255 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King --- go/cfg/builder.go | 86 +++++++++++++++++---------------- go/cfg/cfg.go | 115 +++++++++++++++++++++++++++++++++++++++++---- go/cfg/cfg_test.go | 67 +++++++++++++++++++++++++- go/cfg/main.go | 72 ++++++++++++++++++++++++++++ 4 files changed, 288 insertions(+), 52 deletions(-) create mode 100644 go/cfg/main.go diff --git a/go/cfg/builder.go b/go/cfg/builder.go index dad6a444d82..ac4d63c4003 100644 --- a/go/cfg/builder.go +++ b/go/cfg/builder.go @@ -16,8 +16,8 @@ type builder struct { cfg *CFG mayReturn func(*ast.CallExpr) bool current *Block - lblocks map[*ast.Object]*lblock // labeled blocks - targets *targets // linked stack of branch targets + lblocks map[string]*lblock // labeled blocks + targets *targets // linked stack of branch targets } func (b *builder) stmt(_s ast.Stmt) { @@ -42,7 +42,7 @@ start: b.add(s) if call, ok := s.X.(*ast.CallExpr); ok && !b.mayReturn(call) { // Calls to panic, os.Exit, etc, never return. - b.current = b.newBlock("unreachable.call") + b.current = b.newBlock(KindUnreachable, s) } case *ast.DeclStmt: @@ -57,7 +57,7 @@ start: } case *ast.LabeledStmt: - label = b.labeledBlock(s.Label) + label = b.labeledBlock(s.Label, s) b.jump(label._goto) b.current = label._goto _s = s.Stmt @@ -65,7 +65,7 @@ start: case *ast.ReturnStmt: b.add(s) - b.current = b.newBlock("unreachable.return") + b.current = b.newBlock(KindUnreachable, s) case *ast.BranchStmt: b.branchStmt(s) @@ -77,11 +77,11 @@ start: if s.Init != nil { b.stmt(s.Init) } - then := b.newBlock("if.then") - done := b.newBlock("if.done") + then := b.newBlock(KindIfThen, s) + done := b.newBlock(KindIfDone, s) _else := done if s.Else != nil { - _else = b.newBlock("if.else") + _else = b.newBlock(KindIfElse, s) } b.add(s.Cond) b.ifelse(then, _else) @@ -128,7 +128,7 @@ func (b *builder) branchStmt(s *ast.BranchStmt) { switch s.Tok { case token.BREAK: if s.Label != nil { - if lb := b.labeledBlock(s.Label); lb != nil { + if lb := b.labeledBlock(s.Label, nil); lb != nil { block = lb._break } } else { @@ -139,7 +139,7 @@ func (b *builder) branchStmt(s *ast.BranchStmt) { case token.CONTINUE: if s.Label != nil { - if lb := b.labeledBlock(s.Label); lb != nil { + if lb := b.labeledBlock(s.Label, nil); lb != nil { block = lb._continue } } else { @@ -155,14 +155,14 @@ func (b *builder) branchStmt(s *ast.BranchStmt) { case token.GOTO: if s.Label != nil { - block = b.labeledBlock(s.Label)._goto + block = b.labeledBlock(s.Label, nil)._goto } } - if block == nil { - block = b.newBlock("undefined.branch") + if block == nil { // ill-typed (e.g. undefined label) + block = b.newBlock(KindUnreachable, s) } b.jump(block) - b.current = b.newBlock("unreachable.branch") + b.current = b.newBlock(KindUnreachable, s) } func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) { @@ -172,7 +172,7 @@ func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) { if s.Tag != nil { b.add(s.Tag) } - done := b.newBlock("switch.done") + done := b.newBlock(KindSwitchDone, s) if label != nil { label._break = done } @@ -188,13 +188,13 @@ func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) { for i, clause := range s.Body.List { body := fallthru if body == nil { - body = b.newBlock("switch.body") // first case only + body = b.newBlock(KindSwitchCaseBody, clause) // first case only } // Preallocate body block for the next case. fallthru = done if i+1 < ncases { - fallthru = b.newBlock("switch.body") + fallthru = b.newBlock(KindSwitchCaseBody, s.Body.List[i+1]) } cc := clause.(*ast.CaseClause) @@ -208,7 +208,7 @@ func (b *builder) switchStmt(s *ast.SwitchStmt, label *lblock) { var nextCond *Block for _, cond := range cc.List { - nextCond = b.newBlock("switch.next") + nextCond = b.newBlock(KindSwitchNextCase, cc) b.add(cond) // one half of the tag==cond condition b.ifelse(body, nextCond) b.current = nextCond @@ -247,7 +247,7 @@ func (b *builder) typeSwitchStmt(s *ast.TypeSwitchStmt, label *lblock) { b.add(s.Assign) } - done := b.newBlock("typeswitch.done") + done := b.newBlock(KindSwitchDone, s) if label != nil { label._break = done } @@ -258,10 +258,10 @@ func (b *builder) typeSwitchStmt(s *ast.TypeSwitchStmt, label *lblock) { default_ = cc continue } - body := b.newBlock("typeswitch.body") + body := b.newBlock(KindSwitchCaseBody, cc) var next *Block for _, casetype := range cc.List { - next = b.newBlock("typeswitch.next") + next = b.newBlock(KindSwitchNextCase, cc) // casetype is a type, so don't call b.add(casetype). // This block logically contains a type assertion, // x.(casetype), but it's unclear how to represent x. @@ -300,7 +300,7 @@ func (b *builder) selectStmt(s *ast.SelectStmt, label *lblock) { } } - done := b.newBlock("select.done") + done := b.newBlock(KindSelectDone, s) if label != nil { label._break = done } @@ -312,8 +312,8 @@ func (b *builder) selectStmt(s *ast.SelectStmt, label *lblock) { defaultBody = &clause.Body continue } - body := b.newBlock("select.body") - next := b.newBlock("select.next") + body := b.newBlock(KindSelectCaseBody, clause) + next := b.newBlock(KindSelectAfterCase, clause) b.ifelse(body, next) b.current = body b.targets = &targets{ @@ -358,15 +358,15 @@ func (b *builder) forStmt(s *ast.ForStmt, label *lblock) { if s.Init != nil { b.stmt(s.Init) } - body := b.newBlock("for.body") - done := b.newBlock("for.done") // target of 'break' - loop := body // target of back-edge + body := b.newBlock(KindForBody, s) + done := b.newBlock(KindForDone, s) // target of 'break' + loop := body // target of back-edge if s.Cond != nil { - loop = b.newBlock("for.loop") + loop = b.newBlock(KindForLoop, s) } cont := loop // target of 'continue' if s.Post != nil { - cont = b.newBlock("for.post") + cont = b.newBlock(KindForPost, s) } if label != nil { label._break = done @@ -414,12 +414,12 @@ func (b *builder) rangeStmt(s *ast.RangeStmt, label *lblock) { // jump loop // done: (target of break) - loop := b.newBlock("range.loop") + loop := b.newBlock(KindRangeLoop, s) b.jump(loop) b.current = loop - body := b.newBlock("range.body") - done := b.newBlock("range.done") + body := b.newBlock(KindRangeBody, s) + done := b.newBlock(KindRangeDone, s) b.ifelse(body, done) b.current = body @@ -461,14 +461,19 @@ type lblock struct { // labeledBlock returns the branch target associated with the // specified label, creating it if needed. -func (b *builder) labeledBlock(label *ast.Ident) *lblock { - lb := b.lblocks[label.Obj] +func (b *builder) labeledBlock(label *ast.Ident, stmt *ast.LabeledStmt) *lblock { + lb := b.lblocks[label.Name] if lb == nil { - lb = &lblock{_goto: b.newBlock(label.Name)} + lb = &lblock{_goto: b.newBlock(KindLabel, nil)} if b.lblocks == nil { - b.lblocks = make(map[*ast.Object]*lblock) + b.lblocks = make(map[string]*lblock) } - b.lblocks[label.Obj] = lb + b.lblocks[label.Name] = lb + } + // Fill in the label later (in case of forward goto). + // Stmt may be set already if labels are duplicated (ill-typed). + if stmt != nil && lb._goto.Stmt == nil { + lb._goto.Stmt = stmt } return lb } @@ -477,11 +482,12 @@ func (b *builder) labeledBlock(label *ast.Ident) *lblock { // slice and returns it. // It does not automatically become the current block. // comment is an optional string for more readable debugging output. -func (b *builder) newBlock(comment string) *Block { +func (b *builder) newBlock(kind BlockKind, stmt ast.Stmt) *Block { g := b.cfg block := &Block{ - Index: int32(len(g.Blocks)), - comment: comment, + Index: int32(len(g.Blocks)), + Kind: kind, + Stmt: stmt, } block.Succs = block.succs2[:0] g.Blocks = append(g.Blocks, block) diff --git a/go/cfg/cfg.go b/go/cfg/cfg.go index e9c48d51daa..01668359af2 100644 --- a/go/cfg/cfg.go +++ b/go/cfg/cfg.go @@ -9,7 +9,10 @@ // // The blocks of the CFG contain all the function's non-control // statements. The CFG does not contain control statements such as If, -// Switch, Select, and Branch, but does contain their subexpressions. +// Switch, Select, and Branch, but does contain their subexpressions; +// also, each block records the control statement (Block.Stmt) that +// gave rise to it and its relationship (Block.Kind) to that statement. +// // For example, this source code: // // if x := f(); x != nil { @@ -20,14 +23,14 @@ // // produces this CFG: // -// 1: x := f() +// 1: x := f() Body // x != nil // succs: 2, 3 -// 2: T() +// 2: T() IfThen // succs: 4 -// 3: F() +// 3: F() IfElse // succs: 4 -// 4: +// 4: IfDone // // The CFG does contain Return statements; even implicit returns are // materialized (at the position of the function's closing brace). @@ -50,6 +53,7 @@ import ( // // The entry point is Blocks[0]; there may be multiple return blocks. type CFG struct { + fset *token.FileSet Blocks []*Block // block[0] is entry; order otherwise undefined } @@ -64,9 +68,63 @@ type Block struct { Succs []*Block // successor nodes in the graph Index int32 // index within CFG.Blocks Live bool // block is reachable from entry + Kind BlockKind // block kind + Stmt ast.Stmt // statement that gave rise to this block (see BlockKind for details) - comment string // for debugging - succs2 [2]*Block // underlying array for Succs + succs2 [2]*Block // underlying array for Succs +} + +// A BlockKind identifies the purpose of a block. +// It also determines the possible types of its Stmt field. +type BlockKind uint8 + +const ( + KindInvalid BlockKind = iota // Stmt=nil + + KindUnreachable // unreachable block after {Branch,Return}Stmt / no-return call ExprStmt + KindBody // function body BlockStmt + KindForBody // body of ForStmt + KindForDone // block after ForStmt + KindForLoop // head of ForStmt + KindForPost // post condition of ForStmt + KindIfDone // block after IfStmt + KindIfElse // else block of IfStmt + KindIfThen // then block of IfStmt + KindLabel // labeled block of BranchStmt (Stmt may be nil for dangling label) + KindRangeBody // body of RangeStmt + KindRangeDone // block after RangeStmt + KindRangeLoop // head of RangeStmt + KindSelectCaseBody // body of SelectStmt + KindSelectDone // block after SelectStmt + KindSelectAfterCase // block after a CommClause + KindSwitchCaseBody // body of CaseClause + KindSwitchDone // block after {Type.}SwitchStmt + KindSwitchNextCase // secondary expression of a multi-expression CaseClause +) + +func (kind BlockKind) String() string { + return [...]string{ + KindInvalid: "Invalid", + KindUnreachable: "Unreachable", + KindBody: "Body", + KindForBody: "ForBody", + KindForDone: "ForDone", + KindForLoop: "ForLoop", + KindForPost: "ForPost", + KindIfDone: "IfDone", + KindIfElse: "IfElse", + KindIfThen: "IfThen", + KindLabel: "Label", + KindRangeBody: "RangeBody", + KindRangeDone: "RangeDone", + KindRangeLoop: "RangeLoop", + KindSelectCaseBody: "SelectCaseBody", + KindSelectDone: "SelectDone", + KindSelectAfterCase: "SelectAfterCase", + KindSwitchCaseBody: "SwitchCaseBody", + KindSwitchDone: "SwitchDone", + KindSwitchNextCase: "SwitchNextCase", + }[kind] } // New returns a new control-flow graph for the specified function body, @@ -82,7 +140,7 @@ func New(body *ast.BlockStmt, mayReturn func(*ast.CallExpr) bool) *CFG { mayReturn: mayReturn, cfg: new(CFG), } - b.current = b.newBlock("entry") + b.current = b.newBlock(KindBody, body) b.stmt(body) // Compute liveness (reachability from entry point), breadth-first. @@ -110,7 +168,15 @@ func New(body *ast.BlockStmt, mayReturn func(*ast.CallExpr) bool) *CFG { } func (b *Block) String() string { - return fmt.Sprintf("block %d (%s)", b.Index, b.comment) + return fmt.Sprintf("block %d (%s)", b.Index, b.comment(nil)) +} + +func (b *Block) comment(fset *token.FileSet) string { + s := b.Kind.String() + if fset != nil && b.Stmt != nil { + s = fmt.Sprintf("%s@L%d", s, fset.Position(b.Stmt.Pos()).Line) + } + return s } // Return returns the return statement at the end of this block if present, nil @@ -129,7 +195,7 @@ func (b *Block) Return() (ret *ast.ReturnStmt) { func (g *CFG) Format(fset *token.FileSet) string { var buf bytes.Buffer for _, b := range g.Blocks { - fmt.Fprintf(&buf, ".%d: # %s\n", b.Index, b.comment) + fmt.Fprintf(&buf, ".%d: # %s\n", b.Index, b.comment(fset)) for _, n := range b.Nodes { fmt.Fprintf(&buf, "\t%s\n", formatNode(fset, n)) } @@ -145,6 +211,35 @@ func (g *CFG) Format(fset *token.FileSet) string { return buf.String() } +// digraph emits AT&T GraphViz (dot) syntax for the CFG. +// TODO(adonovan): publish; needs a proposal. +func (g *CFG) digraph(fset *token.FileSet) string { + var buf bytes.Buffer + buf.WriteString("digraph CFG {\n") + buf.WriteString(" node [shape=box];\n") + for _, b := range g.Blocks { + // node label + var text bytes.Buffer + text.WriteString(b.comment(fset)) + for _, n := range b.Nodes { + fmt.Fprintf(&text, "\n%s", formatNode(fset, n)) + } + + // node and edges + fmt.Fprintf(&buf, " n%d [label=%q];\n", b.Index, &text) + for _, succ := range b.Succs { + fmt.Fprintf(&buf, " n%d -> n%d;\n", b.Index, succ.Index) + } + } + buf.WriteString("}\n") + return buf.String() +} + +// exposed to main.go +func digraph(g *CFG, fset *token.FileSet) string { + return g.digraph(fset) +} + func formatNode(fset *token.FileSet, n ast.Node) string { var buf bytes.Buffer format.Node(&buf, fset, n) diff --git a/go/cfg/cfg_test.go b/go/cfg/cfg_test.go index f22bda34113..536d2fe5df7 100644 --- a/go/cfg/cfg_test.go +++ b/go/cfg/cfg_test.go @@ -2,15 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package cfg +package cfg_test import ( "bytes" "fmt" "go/ast" + "go/format" "go/parser" "go/token" "testing" + + "golang.org/x/tools/go/cfg" + "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/testenv" ) const src = `package main @@ -140,7 +145,7 @@ func TestDeadCode(t *testing.T) { } for _, decl := range f.Decls { if decl, ok := decl.(*ast.FuncDecl); ok { - g := New(decl.Body, mayReturn) + g := cfg.New(decl.Body, mayReturn) // Print statements in unreachable blocks // (in order determined by builder). @@ -165,6 +170,57 @@ func TestDeadCode(t *testing.T) { } } +// TestSmoke runs the CFG builder on every FuncDecl in the standard +// library and x/tools. (This is all well-typed code, but it gives +// some coverage.) +func TestSmoke(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + testenv.NeedsTool(t, "go") + + // The Mode API is just hateful. + // https://github.com/golang/go/issues/48226#issuecomment-1948792315 + mode := packages.NeedDeps | packages.NeedImports | packages.NeedSyntax | packages.NeedTypes + pkgs, err := packages.Load(&packages.Config{Mode: mode}, "std", "golang.org/x/tools/...") + if err != nil { + t.Fatal(err) + } + + for _, pkg := range pkgs { + for _, file := range pkg.Syntax { + for _, decl := range file.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok && decl.Body != nil { + g := cfg.New(decl.Body, mayReturn) + + // Run a few quick sanity checks. + failed := false + for i, b := range g.Blocks { + errorf := func(format string, args ...any) { + if !failed { + t.Errorf("%s\n%s", pkg.Fset.Position(decl.Pos()), g.Format(pkg.Fset)) + failed = true + } + msg := fmt.Sprintf(format, args...) + t.Errorf("block %d: %s", i, msg) + } + + if b.Kind == cfg.KindInvalid { + errorf("invalid Block.Kind %v", b.Kind) + } + if b.Stmt == nil && b.Kind != cfg.KindLabel { + errorf("nil Block.Stmt (Kind=%v)", b.Kind) + } + if i != int(b.Index) { + errorf("invalid Block.Index") + } + } + } + } + } + } +} + // A trivial mayReturn predicate that looks only at syntax, not types. func mayReturn(call *ast.CallExpr) bool { switch fun := call.Fun.(type) { @@ -175,3 +231,10 @@ func mayReturn(call *ast.CallExpr) bool { } return true } + +func formatNode(fset *token.FileSet, n ast.Node) string { + var buf bytes.Buffer + format.Node(&buf, fset, n) + // Indent secondary lines by a tab. + return string(bytes.Replace(buf.Bytes(), []byte("\n"), []byte("\n\t"), -1)) +} diff --git a/go/cfg/main.go b/go/cfg/main.go new file mode 100644 index 00000000000..e25b36830be --- /dev/null +++ b/go/cfg/main.go @@ -0,0 +1,72 @@ +//go:build ignore + +// 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. + +// The cfg command prints the control-flow graph of the first function +// or method whose name matches 'funcname' in the specified package. +// +// Usage: cfg package funcname +// +// Example: +// +// $ go build -o cfg ./go/cfg/main.go +// $ cfg ./go/cfg stmt | dot -Tsvg > cfg.svg && open cfg.svg +package main + +import ( + "flag" + "fmt" + "go/ast" + "go/token" + "log" + "os" + _ "unsafe" // for linkname + + "golang.org/x/tools/go/cfg" + "golang.org/x/tools/go/packages" +) + +func main() { + flag.Parse() + if len(flag.Args()) != 2 { + log.Fatal("Usage: package funcname") + } + pattern, funcname := flag.Args()[0], flag.Args()[1] + pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadSyntax}, pattern) + if err != nil { + log.Fatal(err) + } + if packages.PrintErrors(pkgs) > 0 { + os.Exit(1) + } + for _, pkg := range pkgs { + for _, f := range pkg.Syntax { + for _, decl := range f.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok { + if decl.Name.Name == funcname { + g := cfg.New(decl.Body, mayReturn) + fmt.Println(digraph(g, pkg.Fset)) + os.Exit(0) + } + } + } + } + } + log.Fatalf("no function %q found in %s", funcname, pattern) +} + +// A trivial mayReturn predicate that looks only at syntax, not types. +func mayReturn(call *ast.CallExpr) bool { + switch fun := call.Fun.(type) { + case *ast.Ident: + return fun.Name != "panic" + case *ast.SelectorExpr: + return fun.Sel.Name != "Fatal" + } + return true +} + +//go:linkname digraph golang.org/x/tools/go/cfg.digraph +func digraph(g *cfg.CFG, fset *token.FileSet) string From 509ff8bfb1cecacfe066f536412411616450aff7 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 26 Feb 2024 16:12:07 +0000 Subject: [PATCH 31/47] gopls/doc: update workspace documentation for zero-config gopls Rewrite the now-obsolete documentation. Notably, remove the section on experimental workspace mode, as it is long unsupported. For golang/go#57979 Change-Id: I8ba77d626d0b24b0ab34a78103985a5a881def21 Reviewed-on: https://go-review.googlesource.com/c/tools/+/566936 Reviewed-by: Hyang-Ah Hana Kim LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley --- gopls/doc/workspace.md | 195 ++++++++++++++++++++++----------------- gopls/doc/zeroconfig.png | Bin 0 -> 35409 bytes 2 files changed, 112 insertions(+), 83 deletions(-) create mode 100644 gopls/doc/zeroconfig.png diff --git a/gopls/doc/workspace.md b/gopls/doc/workspace.md index 4ff9994f939..59854b6c307 100644 --- a/gopls/doc/workspace.md +++ b/gopls/doc/workspace.md @@ -1,101 +1,130 @@ # Setting up your workspace -`gopls` supports both Go module and GOPATH modes. However, it needs a defined -scope in which language features like references, rename, and implementation -should operate. - -The following options are available for configuring this scope: - -## Module mode - -### One module - -If you are working with a single module, you can open the module root (the -directory containing the `go.mod` file), a subdirectory within the module, -or a parent directory containing the module. +**In general, `gopls` should work when you open a Go file contained in your +workspace folder**. If it isn't working for you, or if you want to better +understand how gopls models your workspace, please read on. -**Note**: If you open a parent directory containing a module, it must **only** -contain that single module. Otherwise, you are working with multiple modules. +## Workspace builds -### Multiple modules - -Gopls has several alternatives for working on multiple modules simultaneously, -described below. Starting with Go 1.18, Go workspaces are the preferred solution. - -#### Go workspaces (Go 1.18+) +`gopls` supports both Go module and GOPATH modes. However, it needs a defined +scope in which language features like references, rename, and implementation +should operate. Put differently, gopls needs to infer which `go build` +invocations you would use to build your workspace, including the working +directory, environment, and build flags. + +Starting with `gopls@v0.15.0`, gopls will try to guess the builds you are +working on based on the set of open files. When you open a file in a workspace +folder, gopls will check whether the file is contained in a module, `go.work` +workspace, or GOPATH directory, and configure the build accordingly. +Additionally, if you open a file that is constrained to a different operating +system or architecture, for example opening `foo_windows.go` when working on +Linux, gopls will create a scope with `GOOS` and `GOARCH` set to a value that +matches the file. + +For example, suppose we had a repository with three modules: `moda`, `modb`, +and `modc`, and a `go.work` file using modules `moda` and `modb`. If we open +the files `moda/a.go`, `modb/b.go`, `moda/a_windows.go`, and `modc/c.go`, gopls +will automatically create three builds: + +![Zero Config gopls](zeroconfig.png) + +This allows `gopls` to _just work_ when you open a Go file, but it does come with +several caveats: + +- This causes gopls to do more work, since it is now tracking three builds + instead of one. However, the recent + [scalability redesign](https://go.dev/blog/gopls-scalability) + allows much of this work to be avoided through efficient caching. +- In some cases this may cause gopls to do more work, since gopls is now + tracking three builds instead of one. However, the recent + [scalability redesign](https://go.dev/blog/gopls-scalability) allows us + to avoid most of this work by efficient caching. +- For operations originating from a given file, including finding references + and implementations, gopls executes the operation in + _the default build for that file_. For example, finding references to + a symbol `S` from `foo_linux.go` will return references from the Linux build, + and finding references to the same symbol `S` from `foo_windows.go` will + return references from the Windows build. This is done for performance + reasons, as in the common case one build is sufficient, but may lead to + surprising results. Issues [#65757](https://go.dev/issue/65757) and + [#65755](https://go.dev/issue/65755) propose improvements to this behavior. +- When selecting a `GOOS/GOARCH` combination to match a build-constrained file, + `gopls` will choose the first matching combination from + [this list](https://cs.opensource.google/go/x/tools/+/master:gopls/internal/cache/port.go;l=30;drc=f872b3d6f05822d290bc7bdd29db090fd9d89f5c). + In some cases, that may be surprising. +- When working in a `GOOS/GOARCH` constrained file that does not match your + default toolchain, `CGO_ENABLED=0` is implicitly set. This means that `gopls` + will not work in files including `import "C"`. Issue + [#65758](https://go.dev/issue/65758) may lead to improvements in this + behavior. +- `gopls` is not able to guess build flags that include arbitrary user-defined + build constraints. For example, if you are trying to work on a file that is + constrained by the build directive `//go:build special`, gopls will not guess + that it needs to create a build with `"buildFlags": ["-tags=special"]`. Issue + [#65089](https://go.dev/issue/65089) proposes a heuristic by which gopls + could handle this automatically. + +We hope that you provide feedback on this behavior by upvoting or commenting +the issues mentioned above, or opening a [new issue](https://go.dev/issue/new) +for other improvements you'd like to see. + +## When to use a `go.work` file for development Starting with Go 1.18, the `go` command has native support for multi-module -workspaces, via [`go.work`](https://go.dev/ref/mod#workspaces) files. These -files are recognized by gopls starting with `gopls@v0.8.0`. - -The easiest way to work on multiple modules in Go 1.18 and later is therefore -to create a `go.work` file containing the modules you wish to work on, and set -your workspace root to the directory containing the `go.work` file. - -For example, suppose this repo is checked out into the `$WORK/tools` directory. -We can work on both `golang.org/x/tools` and `golang.org/x/tools/gopls` -simultaneously by creating a `go.work` file using `go work init`, followed by -`go work use MODULE_DIRECTORIES...` to add directories containing `go.mod` files to the -workspace: +workspaces, via [`go.work`](https://go.dev/ref/mod#workspaces) files. `gopls` +will recognize these files if they are present in your workspace. + +Use a `go.work` file when: + +- You want to work on multiple modules simultaneously in a single logical + build, for example if you want changes to one module to be reflected in + another. +- You want to improve `gopls'` memory usage or performance by reducing the number + of builds it must track. +- You want `gopls` to know which modules you are working on in a multi-module + workspace, without opening any files. For example, if you want to use + `workspace/symbol` queries before any files are open. +- You are using `gopls@v0.14.2` or earlier, and want to work on multiple + modules. + +For example, suppose this repo is checked out into the `$WORK/tools` directory, +and [`x/mod`](https://pkg.go.dev/golang.org/x/mod) is checked out into +`$WORK/mod`, and you are working on a new `x/mod` API for editing `go.mod` +files that you want to simultaneously integrate into `gopls`. + +You can work on both `golang.org/x/tools/gopls` and `golang.org/x/mod` +simultaneously by creating a `go.work` file: ```sh cd $WORK go work init -go work use ./tools/ ./tools/gopls/ +go work use tools/gopls mod ``` -...followed by opening the `$WORK` directory in our editor. - -#### DEPRECATED: Experimental workspace module (Go 1.17 and earlier) - -**This feature is deprecated and will be removed in future versions of gopls. -Please see [issue #52897](https://go.dev/issue/52897) for additional -information.** - -With earlier versions of Go, `gopls` can simulate multi-module workspaces by -creating a synthetic module requiring the modules in the workspace root. -See [the design document](https://github.com/golang/proposal/blob/master/design/37720-gopls-workspaces.md) -for more information. - -This feature is experimental, and will eventually be removed once `go.work` -files are accepted by all supported Go versions. - -You can enable this feature by configuring the -[experimentalWorkspaceModule](settings.md#experimentalworkspacemodule-bool) -setting. - -#### Multiple workspace folders - -If neither of the above solutions work, and your editor allows configuring the -set of -["workspace folders"](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceFolder) -used during your LSP session, you can still work on multiple modules by adding -a workspace folder at each module root (the locations of `go.mod` files). This -means that each module has its own scope, and features will not work across -modules. - -In VS Code, you can create a workspace folder by setting up a -[multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces). -View the [documentation for your editor plugin](../README.md#editor) to learn how to -configure a workspace folder in your editor. - -### GOPATH mode +...followed by opening the `$WORK` directory in your editor. -When opening a directory within your GOPATH, the workspace scope will be just -that directory. +## When to manually configure `GOOS`, `GOARCH`, or `-tags` -### At your own risk +As described in the first section, `gopls@v0.15.0` and later will try to +configure a new build scope automatically when you open a file that doesn't +match the system default operating system (`GOOS`) or architecture (`GOARCH`). -Some users or companies may have projects that encompass one `$GOPATH`. If you -open your entire `$GOPATH` or `$GOPATH/src` folder, the workspace scope will be -your entire `GOPATH`. If your GOPATH is large, `gopls` to be very slow to start -because it will try to find all of the Go files in the directory you have -opened. It will then load all of the files it has found. +However, per the caveats listed in that section, this automatic behavior comes +with limitations. Customize your `gopls` environment by setting `GOOS` or +`GOARCH` in your +[`"build.env"`](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#env-mapstringstring) +or `-tags=...` in your" +["build.buildFlags"](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string) +when: -To work around this case, you can create a new `$GOPATH` that contains only the -packages you want to work on. +- You want to modify the default build environment. +- `gopls` is not guessing the `GOOS/GOARCH` combination you want to use for + cross platform development. +- You need to work on a file that is constrained by a user-defined build tags, + such as the build directive `//go:build special`. ---- +## GOPATH mode -If you have additional use cases that are not mentioned above, please -[file a new issue](https://github.com/golang/go/issues/new). +When opening a directory within your `GOPATH`, the workspace scope will be just +that directory and all directories contained within it. Note that opening +a large GOPATH directory can make gopls very slow to start. diff --git a/gopls/doc/zeroconfig.png b/gopls/doc/zeroconfig.png new file mode 100644 index 0000000000000000000000000000000000000000..49d4f8ead741c5999737582884c2f095c7c38960 GIT binary patch literal 35409 zcmeFZXIPWn7A>kGg3?8L2Nf0R9fSx7s5Fr#(gdVe4Lv{vLQxP@KsqQWRS3O@UKB!; zUPBK()KC(V6Rh9fXYYH@xzGJ`i$8es;G4DPnrp5(#~5?HynLXkN^ybt!l_fIDDK_8 z^YGLuqNY=)PG2THNBGUWT^P%$Q!h^4yQ84%Wx56<8PQX&gPyhmV$POpbX}j7?@?++=JvCZzXMWr4_q1e;9j zdg3y}8hg~q(orW@^)6aiAQ@?6Zq|Yz?|L%WPtOki>IIAM zO@o;V&6#-BXnLo%L&dV>0wTM%Hz#X2u}IrF(**WS%ywWGZ~y(%0;5^s z`(|O@gSmiqIBu$*`uA|@#ql0p*5jhdec5t;Gl}K08QMD%fFp)KtJ0P5h-d9lXW`KYTI zhIkjVmdu$z`0ou1TRD|D$WUZg$;tC)B$*f1d&-W?0$=a`iU&wGrr6&kd;(otK6iU+bENNqx2Cf6_XiQ;ioaOEhx z1Zmzi-;<~z_)o0F#k=q3C|ZX)HHNI**TGrLrxxZAy28f2C zh|Pb_8^nYJFPi zBdpQ-ahlggE+;u9yIA*t^z#lx^@!BX#J@8NDv^+_`poi*h>Hpwn{<%}?JOrs}} zOJ_0RahXGs08v}KTKO^SryNaA_>#u%u>&_!4y&e9(|tM!v&Vrl=32^EJAkgVIy`1E*F~ad?oBi zrifc*|3@D6$#WzL;p}*>cJK<_>iSN+5ZHSN6($NT@sDOe#L~v4VnezfCj}*zmx4SF zy>-tjb3P?dIDvblu z^>{CN*>~w{#Mj5sb3Mkh!dYhKWBaU*k3Ck*k0Rvy(07iOPb*0H3@v0Tp+0)xcwq^& z-G*-sl4m>zr1_i##hxt>vigmEZrpT~y(9%R(LQ>eMw2Yq@ogAj?-LpC_}ERV#cd5# z$s~)pUm~RwY0Jt?EH(U6jYA>}fel<1Uy+6AJ2K$L=i@cp#@dPF{xNs4$1j(YRn3(6iJpP_ppR*#D7orGu1vE1ENcYZ$@? z{^lo*gPX9s!HxSfjR5`C4gk^3rP1-rvIAcs8=0z=6(sbZhq>x+nHh$hV!F+x#7|RP;-;scZwEf|+g|s*8Gc7?w*`PX$nP zgT$QbmIy4}|Ix)U4~{1wz@+g0D+eW4WJ?$6)3dVXEm<~u z$?hD)NWR!0lE^YE^zqKSs&lFM0F6ZCgrtvOLcO}6x7#y*UPte9-BG6a^3Yu>NFhj* z$J#h1npcqpugF95D+3Rbt)TR)GS>nl?w{W3b6Pq(PWH7bv3%X}YKo_tCTn@$M~0{= zMS$Fn{=IgzTDMC_>;5A^i=mi+T%h9)ZHlGLKH9+1n116GCBF(MjpU6&-&sf@i?b}U z+#pa0!Vimg4$G%)x=Fdv{ux=N(0nxjT=nFU={S5L&@pQBQ?TT? z!(M1x^S5C+`{qXj;gpMB)9Pnq=6FFSx?sfzEjcr02vKM z*asU&52ZL7FTReH*?TuaoIMK>QmlQ@lR3`*^*nUtG9&<_-m_LkKejbX}C&Z1!zS^UHv_^qHE4uJU^*RWp3Sx3DAO;3*AxE_I%B za!okU;*;(|F#9jZ?*&!koYxa`$wY31TxUQyfkxtcmP3mygplqQ&G|yFpKQ(1lQo4+ znctx&yLmXH-Cd1zs>!DJZp$BI<%JmbhVLR>j&6`DB?H+Q9C2;z29jLrR5FThOw9}U z5WbJ-Gq;gFG84welC{;BCd$>nux^BEz-AR>ITED%SZfF&gTCnz?p+l^rE>m3rZ46) zlC}vx?Cj@?;4Jv%S!)FoaGaaQFrh>djrYFsn1^3cV4RH>lKc`&9Q6nrHuV^+#^d8? z*d@IZ+K_3`kD!V(wd`i0)7)j`E?D{&(e^s2Vl|AmseoLS|53tRkFSl7x?!u#SH6$b znI9Wou86t9a&~QbNO$&LaoloErh9+5ND1uPz05l*|8i#b<|}KYBglM>Up;lB7U_cR zIDrf>dVq7!Tb(uUbPE2IBg&qb9PL6X;XGBHDOqb;#9 zD=r}btG+n_xjT;~x7%xF$>T1m7KOFmtW=&T*_g6pSe#|;JIsQ!SfA!6*-4=3(*{)B z7hbS5zZ&wWX5r^L`3rxokhuHh8S8iu z;7^lj^)8m{rc@MBz^(nr;lP7-Q?D$#t9_N4VMot+lVR`+^R~u|q@quUqB>_EX&y*Z zd*gEZox?m>`;^NceIDZc;GgFdHZTIcGf>2^8b|2`LHUpaPo*joV+g;64B}X7`^)Njm9a6T z*5p*4ex-;6it{y7-*z976C(T;Z2tEO+Bd3wR|sWSH(oV7-s;(xSLkLdnk$P@B3dz~ zFkg%IM)~I5ZEm-hMz({tA4uX%HD^UwB@nrN^IdzbZ?!o4`nv+nvI=}$i7#mMod^ITu|zSFLRu2YaJAQNXdph4_zscdq4CJO#65!plk2jKhvMjZeD z!J#r%x(S?QNWI=8oQVLHmwSP4?8d5tYNUV03Mto(w_^k}zqno?kS&B!}@0+*>4iHa#pS}Y&sm}D^gkX$52Uz$>LEl=cx`jY;N#2 zijL^d(`SHzMxN_7nkLVIuA;%e$mIYzJ#_3T!+;EU&LChTk5nvA(4yZ}L1mDbzHLR^ zQPGw5&yzPJ6Wr^V+~CQ#_w~;H8TFLWWpU@e?wjM=nyy5DpB;j2f%>!U)JmQlX9eKG ziMm$8N5lvu!~MzFV3K-8Kt^B^@2{9mw-zx!x&@p!940vur2Jz*K!9qG1h3#@nhB|2 zE~FNZR?cIW@Ke#G+%;qU&0ue^pkrmwP2!n&ntpZ6MrdMY{#Z@ZZ+4!2`LXAc#SuA^ zRb&$IqhZ}b{_U?D{1t#5vi@SF;oG!rMVPS@J{a42!g&>FZ4y;9jN)gaGn0si5f08{ zN2yiF&t}AuzxX599dE-OtifWUyDYpESRl4u?IxK*xojx_+j+@p`jCC(FvD)-llKRT zxj#GW{N54sEh5D|!R~u{ne;-fL)TG-Kc(|;og*xF;T5PnDRR;$=`>H2h=UnUfK)J+ zp99}KkORE^f#&Z(ngqPh*Y7@U+mG7h>4HIhIVKo?duA}Si{rQp{>ZD$Zm=NQ@Nj7g zwW5=I?n>Ry-CUSWZwCvR97TymTMHEOpT<$p{0bOtxvHi2{F!#)LyK*%p(N&?AN=E6 z>4?f`_*YAtME*3aXeHC%7>h&f#n?^drc^EDXayEpT-OV_i1?vHWIT7>c8^LNVyj7b z&4G1r<@ueL2FSZ%GxD-7QaR;(f1e&0MVHu(7S%Y(ZMp>hipa3U)~^{^|+TUwnpNtrvZrC?UBILn~41(|opnFpK$dpkgGHGU1~A45qXb zr+zz0Jeo%<*+J8&_U0xu?4kIzt)HsM{Ul94O3`5#FY(ILzn5r#Rc%<_(ub0j{lMT( z4s*=G&jWsC0jpt_Kmci0=OWXbW!wM*Vh)p2CZ)ekzdzp)@>*k+(nKAqzylY~;uj0o zmrM2v$n%{zoQU)ADn*&m7$-rm%#E0&4w$^r&PlG{;uG~9^UgGnUbG&8(893}D2Y6F z5Mp9y8yckeJ9%6KP{A;~eELwsie|WJ@pp1&@xBcrUqUK^AEf|@x74h5RKcQUs{`k; zeCfIr_kDkfJ2NpXX+p1!N`+Os;$4R7uqF@g{&vP-kZdaEGm|IwW`D7h>Pga5@XGnX z>sSBkhtvBDb1FGAETk6$$2I!(*Wuz+wTzcE|Kz3lTB}t@)Mke&;(XKn({jrgy&qmm z4bf;l0f};r8b;;fp0_Z{4}|_qm=x|jYVo)XRcoO0xG%cSOgSc3;U&s$M3;~jl!bUJ z@2Q*>9hwk~`R&Za#)zE);p5OKDr`9MzfGan5@v@?dMUa{Li}6Q(gu!Tw-6dg_G9?F z@rrZituIn{k8k|&hE{XtY+%IflkiU}U5_{P_J0e1VJoLGr#+X(L^lP`?S4WT{D>bA zvjiQ((@X8o7!a0}EMUb4e-4>=G~tvcSS3x7WBJbfn?bF@u2{DktwOVZJiUw*5ZAF5 zyzn2M?kiFS1N(>e1Dk%i6q21_yg#j$xj+3j0IdA)`yiEA?r6E;w}AN|^fy8sOb^DK zKhI+f&iZ`c&-)S3aui9g$V6UcXr>Us-$&tTE~)(9u`bA9#wzOr$xSHfzps>J>p1X_ z6{DB?eD3uh$cR$T{*r}QTlUGg|D@{ZJF8bT=2qcSVQCppbe=1yJflVk-qkDF3aU@c z8-~3(#6T6|M?i#Fvng@xM-kyI2>BC`L}%nrj{Q{~2174D7PMnFvuRVnvJxk~BN!ZX z;IDAQqOB>ft4eah1`n{qEm$}1?T_KJy6T&5KHhVsuGii0J{(3@$4{?^zBCSr@VgGg z*OO!CIQ`q5wTN-8&xgMEXzVMcoyX?!Nje>Wi;%_bH30YvK$|2$c#aDR?x){$uiPs& z{VrD59T9Y(eLCs=@406yr5O@miguUdJ=ccDBjUxna0rLqyTHa*BtZwCxw(vQA^Gtg zU;Jm04IJkpefrb^`^^ntFl|$`rZ*qT?{4_p_FC)iO6*R9*$-ESm?eUwcZ6H7Kw5TM!W%`@9+X~ z+k4jQod~OfJ$m3GT}1-M4DX~lM#$g&#{R=Pi}OHeF%k`>dhg`ZZ;Hn{Y>2E*kvBWx z7^ZAHw|Zs4d{q42A2CxFrtjzFZghmvDLEJa)`CV|RoLJbxVbq?z3Z>5_1( z9zIKE9wIk~sA`uGb;Nd#daX`j^C&zO4+2ua;x=Z-!P2W8J)-;Ap3z-GDo_P(&xzs=g53kqFOxMExc?XU(I)% z(sTUHIKHZ9r{E}fbaM(JiaL*4h_gO6s@aGQGDUoLXm)C=%%Wg%`8G9fi=~UgPE8ry z5!t`a9J*~%_}t*I0DNtK(yiyHxTdbtmw_X}Vd$88zSDhsO|$+WdiwBZZa)2A;_16= zno#I2%R3q#RHA?A>oO1x0E;tIhaFMCCj~byJ}41*oJzyV-?ar~NNSEsv`?(Cw{Zsh zXhE5pVrogE}tNfeuB-R#r0mb8rY!ePIq&DBzS4EyFvEQnDb)MIwywudA*R>9 zUnp9L28#x@*V>M`dEEjYqaO9Wj8}SbF_P3GTTslUZ1B|=+aof4U#sTFrQSDzROL2v zQ!vZ85oJ=z&QGhN^U~u+V;Oi+^exTolzJ>*|Hsv6X{6)eM$s3kMHyLOB+g#O9btW}CAA|5LS+daa3*;y(Fod1<*RlTZb9SE_j@KD zkvNNFDW?ikP-`Rt-W>{QH-zVYqXlIbR3}jcs2+^4e9yyp3O`ki1!RL?5cg2*|Rk>rbMmPPN z+YGl@VJaiLGiGFHdIpPS7$WJ2Lc$7Uw=8$(k}MZ1@#$qUbJdAnTgXt53+f`0ROc*9 z^A~9W6;hx&>P9bok(^3mz+WZhrN;3TcHS=8>jWO;B5Cqoo;~oyU?r-6e+ZLB2poEj!se4xOT~|UDys8&o zv(l~J>R)$UuhCv0kxX3jxNJ2hI>dBdMI+HFDuvp^Li`nN(=NTFRG~RRPfe(s?uh7J zvSrvv=VcULZAzw|+MHsiQS3pzprI{pUqu{^n##(xg<1G`GFfOaHs>zS?CrP-iPaL& zI+~eP>>N20$dOv|d{#34?K;KlqdvamOlmZebf0-QIRUs_VA^W|DFmo8(sT%!0ieFD z!h00Jf!&HTV3OgcH=EfUuvQsIm;2z85Y3Ej`N-1O*&7o4GZI@ zDwg~9VVaOntzf-FoDg8`YaAhEHBVWG7*uqB@(h%_pvc&`iXq}oQtnk7ao7p z;I5ujpB2h@+PDUNuI8bn_h9~hAwOt0*;p`evPbPlfn2Nq3p7w~q03dA8B$>=hEI&l z;!e(?>}LDZolNF#HOm7qDBD#HMxeE<+j;^mK9R(|MUg*=Lkml?m{R&Jl(K62jNr}}zSni<+S z+*v4p%(I)Osc^Fm6I6Nc_E}*unH7V{V4)_;k1NPnst`L4;Nuy+_sdp$ojwN;gD;Hy z%A`QKzEquSDx^f-TjXC!-uetjngAm*TsM~9EM6m>QRKTP=>i*!h(EX=NgZNTfnq*F zk(sv$%C*6-WbRMBoL!PODxI_!sJ804<^2;Fzq5sXvhz)0BH7yiI=uV))RUWlsNlL^ z9mcsjhYP^pa9TY#%=yY|okH&+Fe|Cbk<{t%LHG7n(wXscuyrW!Fkq9DPt!zd=PgZY zpEOK0Rce^kWBwT$|ApG$Gs;Waw@FtN_epdOZ!n3kHJRs2QSvx+f?us-5X@mj0lN(Awiv?|92#&KY986sFUXW%iF zOJ*4+q!gE-SgF>t4r)qJN}}l?b0G)q?a5n2};`^T; zajh-xeRp8#%6h~F_g$&plNjUb;JLb<2x1R5wXPu@-uZYtM2ZWJwO#aAJx=dQT$Q@c z4T*EX!E#KH=0Jo)O^Bu_hm4F_LU6;%(iW+~Uf4`gS7xcUchfunpidWe}?6@2SC zTVE_awH3jaq>CWU4~oca-ewb6c5(B2!`KA&`EZxXD~~@ga;@pVzUp^;j^uH``mEck z=+|!PMXbRDtV_VxeYRH3DV}g<55AAfg1+77gS$*rItY%|c=|ekvJ(113u9UNVFe~x zk?YG>lIcD#Pg+|!cK1B6qsMUz%)q5p2Kt+QTjj>{Z_B$KytQt0cL*vbX8{Mov_5S$ zUuZPey~8S(^(9`t=jv?N`I)8MKDHV;yI^KK&ssWCLA`7p3t^k>EHtcm{#;oxzABf3 za^k6h-c>Mtv_+(|E-4TE(b7xT5E-82W4$Doi33?3Ibf;TV03|hqWP_nspk_Wjj~fv03e!G4cHu%FRpW z=$)UU0!H#LCNkVhO#g+k3EF#urqr1o2u;dH{M3lu4zw^&L3B8debCm2{!Kc`A#Pad zt%9Jad7oaqjbPQc)E+k4HvhI{j=nAl)dGW|^XBDp7N$Zo+4jazPmj<$Pb>3+AjgKcDchk@Y<&>8M4-ODXz^i#z@yJ;<%+ulwMfk z%FIS$KOX~6?FX+%`QjbN9O}Lb$iJs*5vO2`i5g7HFyiEb*cngDoL7BN6Wqlb_B54h z=2p$pE!&^@g!9ia_$Tks;6<>`B%ZOL>w8K49H<(Vgek}|+BfHmzHgKO_7#||3C_Yk%kQXk}#4eax7a`GB{s) zca!G~{+gt-@8rbhrb7T&Bj7l6ZbL!()ieGiSyUT{*`g^AcYg5-0X{Rj_;Bcf%G0qcpFjTN|%i_COf1Mf?20)#* zDK6;i6h)OLH0ZWAB+C-Hp?&w@!IX(U%#=K8)Ol#4&gVq76~%Lm>0=Fz72A=TIxXf6KKUSF22z+s%Y4!DS^SJ zISMex-Cw@LWj%)FgICgON>WhTKLyntKj$7|k#w+A{>-6IK2IRqTZE-GB+tu`40IX& zZs2$Qt$=`k#>4Gf?n`6+VlIQ-tg@@Ve9k5#K`!`4)`9GoRkIsThJeLwZsrW+T}xXr zWETNP53jTXz8(oXj(1&Au^lcjkQ?dk24R;)Y`i3u2?lf6IBZzlJ(0uDq3%&kGQ z+b$Xx-yqGqd2N3e7Vj|23N3}Nxn#_QX<5rG@=#vf5oiXn|bQF}Jo!g9sC znblJhG_+bGPBN^?(RCAwj0ziAYJziQyKwacjHXc|FbVulp5|rI%cCB(_0q=7I@B}^ zmcH{HlqOCh;pm7``))pH*YL)LS(E_0FF}4?IkXq}TaW&!`2?n_>&r$S2^&z#iTCN6viRW_%+G?dHvE9!4-^?0Uv>4``8d&R<)s}6eFa=+nZlG9 zR->8t6rsi#H)>NuR_|G3QQLtQ-Oj7Ol7Y`(Y?pCHg8w&NXt8ogEmf4P78=SQ2b3Yx zL$|t-bz%hZmY}kL0K6%HG%xhKq_vfP+#y?*saM9bcy{4~kjsh(M-}l;)3?13#z_7!%g*&lb8=PxPw6YnFX%6Q z3G>(J`jye>8s99*E2$(X9Z?7q)M`h1=+x8dJvK*%tmn_JsslO;xVvf}FJ9=MldG{&uS>+iguG8d>>&4F*JNza_1$`eLva>%sBM(5}}xD=3aHIe-|bQHhE^M#u$ z;?$b7YvrU;oI_I4rhScV3~dv+o^_dhi=a}xV6|1rI*t6?w$bwfDn|M`$#5%S-`d{X zzApu@pQx^3%x?!NP5SJK4#O^!5-mCb!G;NAR74f_Zf*i)KXbJO;vY3H-Ao7gOKTJ6 zOn)@LyqS?XrWeJuh~311zlHEzr6~+j(JIap>Boe>K=%Z_P$sks)-ZzL{r!5=cY^BubOY}ZF>f7Af_jlINkqfe9}{zS}y)swE!B25x0vMLw@zI6=aGYxM8qET0H=6@-xwD@BBjoxG+-siwqP48nDK^rlT&E-m7SjPF{d%Rh9dAmWM-T znIM<}63fEORlR#lY?I;VJ`ec*4Ljf^$NK_buTjooh)0LbZN-!lW%rkCK#}7fE7l z`2Vbw{+x%D%U6SgWXmf9uZUy@5K`ZHVJTKvTa1dgp@2-l0q)z7cj)#bwePyE||{ z=rOe~UYufWkjsxGi9UZkkycZ#e`0S_=Van1$*{+XMFv5l=v>1#Q;pOz)M#E#R;ty; zSoTpYwK$Uvw7}RulqsgnfUMI3`X5j9_1L8HIvXD=+JhEX z0r>st#tU3%IaBPkktjObQX}oOQrB3VKix?1$^~?;w@Qnk84u`63ofwCfHqPXCW2l` zWwwRtPh2_qM~X$-ogxP~!)zJ8;>z|yAVsL1mS z=D}oxY1yh>>sUQuZ#>kK4m9xnt#YX9ZLGDM&o$4S$44{n)4V*;K_N_cFBPMyDgIzw!CMtbjYdr0LVfx{E*aEitYZTzhqXPm;dVSdvi%n0X{Tw{4W~ zwK0w2DYCh;^^w*IOviozEZe@&fIRd)?4sf+bgo7yti<3yW=q|(WL-DwcN(Y72?m?# zPnI;gElqTNK^*El5zT#9r=*%d=fHlUz<{&}U+lFk(hnYqZ!pyrKc-@_(w`!@A2QNB z%jY6?OsVGlpQG(%&=qXWKGTO_76|p}lEoC8Hmwx4k=MCrK!YV|Yq8Ya1eInxEwkrE zqxCl@Bcd8@J!22#T&6*N03qDNSPKnc7)vNS#hQw6Ozs}~>QCnQkF@lxFe>Fep@z-R zl_2mn%Wkfoz|llrqH%4y-6hG92|@dU{9*iYc}P++dT~)y2LO_Htv)EAVcS> zMb*7r)6046;`069yO$5cPNeWs$Nuhe^#dxxdlA!9{5^@?a_^W{#Tre8T?jizsvWig zJ4zrhnRDojV!{=VxJ-8=!Y1D_uijfp`f3`RzK53hpKK0}2QJ$Kzkl&=3?fJ$B+W-- z?kn~M)%e4P5Z$XbrsZSFzSRWRqzl=kRq_jzO<8EB@)OLy24^CRi`VHE zocwR&GkVNlDzR>kJ{dpk8q(AwBf+f$donr?FDox$h_K+95GB>uRBNhMqSmupC$4@} zXb`0G-Whto&j*zBvjO=JHlQek*dXLEO7;k9#XE-kEuEQ0X$k=pIqLB)KgP3c#~|Vh zEV|1m>9;l9Ah;`7Jd-w;B%B@kx+zTv;EI$H;MYGYG{1?XH)kd0OC?$B~FRpH{TcwqmF=+ zePGNT)BpM5-AG=C`j0kty-`i($}bkENIeYZY%QEShISFhnZ#LbeYaW+X!pE4c6B;N zdZ%k9BTC2&i#2jv` zRBg_3exZhoC(D7~N-qyoS$`{&v~rvR1WGA}ndp|~2Yy1YEP^B#M)LCXr_8f554LWc zsTsiZ1}v_`dM+Ly>iiudVKVDqf)&QD#UWfvUVC=>B?T2KPV6rMtrsVNUtrw$9x!&F zGm_eZ;oh2_)sLoA&H8=9y~-a@@%q_k$0QHQJzXZ=Ti#`xyQ(Q*NKPf~OSthV5VyT0928d*Nd>|`+SRT_jmPgEBV%L*WPHppn|b zcSK|gRXLuDUE(1jAD_8gWDuKhatPMS4NtE8{pc}Jx9oA;6_(=io%%dHG8Y9tsL5xY z4c$`KwQx*0yYYPuyAZYbx(A#jXH9c{RtkHIRnBLh?-~Sg(29av3c_6EkGc?!Qv?Oo z#>3xvPAA84;#1DCnc|}`ulq&(3j~{=D|)TJw^`d-O~XHVTx##MA}Q62-HyT!UNCf| z&QK!FYHHh(HN$g(BD3>-Ry)%IZ%s1*js+#s6crB+J?l%$vvj#8>MU)Ha^`(1%XzM9 zmM=@kaTt(l@0DM8H^#>cNl6*SPH2KAz?(uKEDnluT&FG%w^Ye>#m?-5%i0fA*Y!%- z>047I2~Cix*=VT0t?BWu07v|p7qcB7>}(8 zcmIA|>EH@m1-!A?3>HcjvMk>N9d-45ky^m(^xTqUnJNQm^%rVqtUaqgtu#SV(yO+L zBnpNI#f}s1TaK58?ygzl>G>dt!^J3%Ym&_$so31&HTbx0-nsR8NfU19{4rt$gC8P5DL$x?U{AxdND7O zzo=au6E>C?ZPP=x{X$VO{)lEzpeftZeYI~KXZD2}EsdKul#rtc@QiqJ_Arx>fsU8i=KB;%U2LavdD%SQ$q_oFm|ms+>}RyVO)k08f&gmbYlqgT`(O73V~ zW3?n6R%Jq@&;B~$9p`6E{V+zL;IJ=d^#Rg0TXYh&j9lcTT3EuGz9_!{WLQZFxZ9Qn zeTU8f#`G<<$1U7;LF&xIU?KF@ zhY*SqC8!!3^E#vS#$E+<^bSh7ez+43pKlT#!VJ32Dh!3cDdEzbSS# zQZjA-Ux%|pQY9wFTAB^s^nR zq9bXC$O&Jk%S_dE=7I;l_@Xu)8GXT=1UB}medEwvG3+TiSLA159KI3stJ`rw!5ANK zNlBgyei46QF1CC)4PKSJeoRloR9G?I=TcLgQ(7{@Y8YC+ zynK0Uj*RQw=qqeg%ugIb-2cC zQi~dlefM6iT{!MDQ6f~dyB5$JqCWv(z6jxF=VUZ z%$0A-aYt(<+8K$PNQGZJKrTtT+Dfp_ZG;Aq*|=!%y%VJyqqXTS+rb~qY(mSpA9cm< zR38c{hG9>@3gKiZ6B!MJxUGC2RF~hnR5Ew$Qv9sDe(kZ5q2(1jm9i(`FeOGgkvg_B z6;hmZw*cgY{2N$Zfl-q9of1-tnS;aO#3&leHWFmW!+iyM3-HIEIdZ@z9pPQU4s#UT zBtabKQP|M=IMTZF-MOkG8y0GNCE01v*XVSZbrVgkKVu+F&aA!-v#bq($ME|C|8SyMpEFf4GOB!^~$~8h=Rn6bhkk zeK1&l;m3zJEq_YR*E|Dq+ zV6u*?Z!ovBl>jvg5zf+~Y%_A#_FWXMik(+?3_;r?YABK!@z=TKdJpu_ua4mv=vS5i zP4^ED`>E1-dU0g?up^v!0*2jB00WahTnSaE%r^XknW(`P9vXxHQU$HhSY;2)8^Y_O zt)Jd?dt9yqV>C?&5I}7mGcy<41DvK@rbn9wi^;79Por$Q<=UOE&$Q=Vpkxtln{eA#3IP9H_b2o# zCskh~VgqQcXjmt10VlyFmMCRJ>3AqwBq-t)R`;9{d&3yyZd?bf(P|Db|7cXO{O_*^43~jF^VF=9ss; zo0PV2Q03RjcKBV7WM$ap&9R06Z%^$W8HnSZ^V`F(WG+d3aR~YY1Co(B@6H_;VqCiQ z+us3QG`{g^<6rp1_gr@)Aog2{hpuOu3tp z>Ih$zL*=FoHG&3nHDq^YNeGwplBxai2H^5#rlF0jyf7$!p?=h19Noq{`f+@BO?9qU zwD53LG6=)2tXOH0Of4WroDuV}=b0j-Yy;+m6$S}?ut{*eVeKDp*%iX;H1`o@Mow6) z#48jjORH>8=_SWFe4R8%_p7N3cut4UzlenJ7I}#a*nH>OuG|@vPn`2)7cJD68lwFX z>6b-f8UiILP@-;cP?(p+(f7^MzP;+~n7ZByxeqVjn=c)#w#b|d8`%tGhF zd2Gk_oGcd}1gvq$@~!KtKwq^~^0s~!cw13Qj=&Wl zu9(OaA}|TcWY;ee<44%Qk$_4BzyKVU2R1HQDQKX)q(%TKo*}T=q(!4y*kX$RiG1d> zMuWPscN-9~^JFg7XFVfOkrAYPIJ+i?{$y9O=$)0n1$zgsD||K0^ibZPZ??{$V|qOA z+QlZ!xxXp#-{Ix^SCCBtg)ZD-5aP~=O1=bznD_!rrA>><@jC_rO{Kud%P{n zPWqib&_+2Sd+iN-r99-)4C!fXr3&sLdQYpCF`48tgfEkuOI^9Gt3oE*ym>_0AsDjR zweKv_l~_^O6V!1&!zvwt`A%0cj{Lgc1Z>xz&uiG$G$%WGtL8FHA_$5(G~#eWw<|d zfL3JT+Q6zHSFOuGfY85l@5Z~@Z)e*l-$K)?<`ZKHN4P(HkW$%HM;v)>CDg&j!@roRHs!(GyHdB9?ql(E_AE<2j2k5 zhZUS7VSh>?(q2lwqcrdDdZ?iIv^tfsBa%^lOoX-dym`lM7qNxK)NKJ*t+3_?pc{U= zAEd4CoZ3&~(2n$$s09?vK;9d4Gto8aR-32^kZHY%szGQUyfyPSDCoUIxv#F$!C)Dn1gdIWBYF=!jBg6=rn?N7fwQq06-hgmc=x?B&vMtJsbrxMg zXwWmcB46I!1DP6cf;P5}L8{0E*W9ex+Z@wmr9%k$R*o^Y5OSP&~ z0^gNMxzvX}k?h7tA4I^;Fi&1(1u3FIpD9`Cn#9iBVPU5isJdZ$%k3^Hisdhmtk-Ay zeY5ohe}8&J-BkK$t51f}ezskW8zSilYGuBM;}$thqi)sG`f*#)VfvVfCc5XvXN6>H z51@)rgRaP-@U;KK!;k8_Z|?LbgSH-uS~m%!!Dj{O(rhaMV$3}ETE~v5zjwYtkwm@k z39YE7KhU|ZbhmnU*O*~NZ+FJjGCRYc)H@CkYl^R975BE+6ukeC5MaJLLF%pa;2Q@8 z$k3tbaH#3q=tvHUifb&xO;lNAJA4g?i=;ZIcllU!FC7HzAfUcLNKS;y2+lszjLVTn zbKa20zfztTLc``D2}q$p8b0^Gv4pE4WaqZ?<`9bk(}lds%QQ`JzRae1N4f~Wq8BEW zx?5I53dI`x-uEz*us-XN0#HwO1DAZt?YYM(*MPm$O2iR(8rKNwmmwIX44UBGz-@tT z=jbTFr+2Szphe7rXkXu|MSMcaUiB+dg4hkD_eGi}&Sj=q^R?d{YN{Zj^jAwq zM}&~iN}IoU{rOo}Fkz%38ttOS{SLYIscnGORRLIWtr@vzvQJ0y7r6z`NU8a^F1k9( zZvd~oH{wG`c_BsDBf?%$wtI%xFVIR{Y+8OhV9EtCfo2D_(F52CYZ6NqLW$c$y(CSP ze_7o=Z^{`T;KfW82grFv>P~>HDKa0 zff2hCL|}KP6*J8+Rr62hUk7b4g6{i(m)yAuDf;?iBAng+M|*Gm7IoM44T}gO-JOEc zNF!3xDbig-=g`uiF!TV5(kYEdNjD57NTYOjch3ON*E+BBe(&SCo`2xw;0JQYT5GTP zthLvcNbK@y9^df$NY7v!llA|QC>C<^UAc#l!mmU0xYu-dSlBqQ(v1(J&==n=N27;`jwo#7&?!g&D9T6W$|IT4L&R*L){H<$_%Bg`aIT-A zJZkSm!0%i_%8K6-3JOP95gQgT1BxnMS5$fd(L@=c%htl*!j^nK2xq|h!&w$m=vxqGCs?Si!2)~Va z0x3-W()m8XuMa85MNvB8#Fd1tdK17!P9F1o3RGal%Kn_AXhw?lX4tct%IQKPG2LWI z@Z;ASs$x%B0A7oTC~ApDY3H9+QJzEbzP;4iyj~S}*Ts1+G%^ObkaJ;}TNA%dAYx3} z+!TfmN=F*sawO?KKHdisX!){0&SiLLgGnC3xli9B-rN*%^Z4*ZhX2{+q>X;FpIlIm zMB<$77=Bou3jgyU>Eq;(;kY27o6%2GJ0Ct6gUFvUELV zG0B!lq+u{#kO?k8K1^~1bBLkcsE~V;Me@xFzu)k+3R{bLdJ<|zFPcqsn?pzs@iNY8 zUwI?gP$HcY!POpVm}rb7Q!5qv;z)FOd28)d5H~|ZCENcsZZtMu&SD_Vsju^~c+9I4 zqe~H3BZaE-x3_lW+(%d<3AiPRE*PM-ijlWV0&;?+E|f%66X+pl#{SFf;xkQjg}7__ z(anR;B`MoIE@_0m`h|x89GO?wSg5C#HAdqZe=_fg8YnciEM3a`MeF!-NhDk?5e3VG`_p7-Eo*AS5Ph!zQ zGOwk4d@oDNox$dmuikp+W1I(by2ll?T6{-s`8fQ}7%?dD!I4ho8Se(fxf`<$&_!4& zylk~H+<>(7!fI%UYLwy@efn#d^8b5?3EZv(>hJULpIgipJ8i&hu+}f2u1~(eVsB?z zGQ#M*l}W$ITA(dI&+Y4H&xZA+@`vn!d~tEZvI{PpX*sxjFW0oSq}1r3Z4x@5&vjZX z(387@k}vjgH<<{=Up_&NBLbAOIofkf%5@1|=RNC4b|T7-;+84egZ3~yk8x>7?9J@< z^*i)~ruf5i*sTYq&`pcBuj*XO-96U}Bk)!ndLoW!uC2cEL)PYtgpmrQPBf7JL|wpV zVMX5>+;=GieeTX8Gmhz@4FRfjeM3(|xQ~NE3-a!g8luGe#MA!vuM#>0)Q0Rd z<)Qtrj;*tY4xR@mI8sjsZ2&B#_bnrsF&0pOP=lpvFg%K6T>VlL)?wv#*2O<1P;F6) z5?3Z0X>q`)_UITMOfyK$wb!h@^H}5=crQ|4%RAgPD%Z2GCAqQX^oV`(=rVDR z)Olv|uIxqLi6i*H{R$M_Job9leUz<#zq{p6qZD3kOhUO(@NY=CjNx-}l>mJR8{)>pot(y41=(dkBQH{|!0`?v&n$JH4jD zta4_hka6xpS6vRR(-cKQlEYyky369yXBS>Qpjs}mpoZj9#Il~^(*=7#O4f3u0ze97 zDWP4r8YZ~B8qb-6iYVzM@Wf}#dV>p~>FxdBKzOX)Xa$W@C|IUZy-a~I+a~_bl|-?E zEZ~819MrU4;W&Mk4K$JTP_fiR8mgrgin9(CQGav1#t#W0F%8uIEELMC(cAgHn8)AF zIaD|T(43{6c4uTekj2a~3EVEFeR40H8L8QGHL!In>Ak7_*_eg)arW=PMl}1##$MnM zHt}kp;U?X@;(*9%4@60HrqQtOF%ub(I~?`rN_-epGp2$zT;jDaP%5l2I`FjbmD%X-J9 zDB;-fQ27dru~iOpC*(KO635}qg=cFZz)#Axp~Zn9B7LGJhnJ7kuK#^ni@l9+TC`#d5pQn`bz$5}$t0MxdaRp)9RYt|LVl^v1QeF|B8eM#J-5V?`c&GG`VurmEk09(qw zd^%Hu@#3QelTW#bT5ce2_+DJ^LqkO75%OL)`@Q1n#)L592OuOgkd%4zqOT@al(YOU z6KuNlyJWMVE1Vwfe1p?TDoBhk?;$By=dt(%pcR64G=;n$JM>x&1-qn++U3>o*iWNuvuiS!qyQMwTwIr>I0Xd{8<3AXpeOoMJ8jK=0yO~)7QL(ax`=O2gO zik3S{WHZrMlH&JPqI*XvI#9MhwxA$Yus`@54OD8}ohir=XT!a&fxZz)uGnncDVtcgDSPgXnVqM^#})4$VuKx2o? zpUH)v!$@)cTJp~?{u8{ z+@cCjL2ru~9!3e1a?$B)Gu~7Q`rBhg#rL7yj?PuhQ?aK9nLvHMQ> zts0Oo`c%~9g267BRW!XGR3#p!UDBETiM-|@XTPN-^UCPQf8jPWD&|91St_<~8Ez;q ziOo!zedjYe@9ChuUR|F|ZpS$nbY7cj)Th!UX4@US$!rdI0l8*5-i>5DbNrK!HMPUE zDw?niK~X?7r9}q+h`IKw(KVc5H-McrPc>ly=m&m8DBGWWBgu2;rOWA&REn|L?wc2rI;w1&jM1`~Gg=_OBWZlSPJ+FA3yD>gir{ z_zmm2g{+!);Xl53r|<8Z?DuSOK)7fP9FG*`Evkg~6`%+#p9q>_9l z6%*1>6o}gGySoFSH`H~i6OQ%u7@%tItizG>myu0GunC0z{yhH0I3WY1DE&VILT@k% zbfEj!2Am|G){{LWlB#xc>i%p%Ms;=G%Q(N@k$*qw=|P`<3)+!W$Ad*}vFoVf44)F@1mX+;HY=I zQ7;X!7X=DsCe6H?o(442NO+fr4c<=AU33T^Okk{~mEM}f_|f$puL;IoNwS@>|KYy` z8oh6}|Iak8_Rd;B^~I6)UJgm8Un%spb2a)>$S?qWdGlXTU&>%S0I_J~f^$__eu9xT z-@>mzG&ZEIMyiLaw*~A-JCzkRH-Z>%`kh47H2Tu|k@haN1Fm9r%Bv)kj@PH}U#`aZ zJM5Aj(s(U8RNRD%_f{cpNr$#mbN; z9+bp5W^7NF?GjN8c!&Xjd!AoD#~3!R!RiWojA`M*$vaXg6nPRQAtkF&B=8YpR|7cYVN`2>tHM($Yc3B)2?>DN(?9deQJ+_ zIa^K<@ujmzP!UVgz1H@S^_;fqr{WP8;n;qbMbu(AKwnj;9FRjvt&*HTZWinXdb`|v z1)$-55VMzP11ZGcfx9V^C2zcG#>eX&eBj0E3FcBu?B@>E&N}L-pm87_Go2FMRBttU zwj*eLkgtr2z&44=9z+K&K^tBdfU;#!WQ9hM0a03fwe-S@MaY@SF)xNcpto!(tMo$AzV$FH&?e1KqU z_8Jwl{Jc=HW)B~xsq}wOVgjXGEbM(|WPU3Qc5WLV9|09gJ34NcrvRj`CtCP+uGzmx zM22$Y+q;)&W}mMGxorXubB`!av)!X<&;%TlsTnad2w=66=#Er#hx8OT$nI@h|y)!sxsPfs%*4SFTWFQ z{q;63oa9d#++iB6+iYWlKXT}l$J8m476T+(g^NWKZ8S)=2Ha#{Br;D=G~^2)DVF1j zn%Fwu?xc{PW|NSsD!gRrK50}dcagXNDn6&s8Jj36)v;mMMuon#S>>uZO)i%XX93<_ zo5Ru*_@Qul>AuOtZsVKQxzY{4=V)ffo2v)MP7h;Dr0m9XHayi->Sx#d^!Pb=Qq!uhUB8&}7Bg;#nYS~PZlRm6R#vladkS`ERzZE%^Tu~#fhwumx;_^ zcys%lQkYy1K0SXZ#2s@>MkSXvGW%pJ98GKsM9G7XMz>3jT(~clL3H|c(JZ=_a zdto+-Spn&c0qeUGiPWUGGp#tt#kpp;w`T*qdY`tG+L@lR@^G_6Jmf)(K$mfTo=U%f zTS>$?^z=<&L`4`nQ&5E|=6mjr4!UslN~&eT{6x1Aswh@nU562Qk6Q(QcgChMa60|G z#e7rIK=HG5bT7d3odMfYMnhZtK^MLhE;?Hf|i!sW-V znR-bQ2Z)z4v(9Bk+%~x(g9hnHvI&tBYWbVHHMeBNOqP(s4NQHVa4VbfwoqaW78GVm z{2(Ul!8$LHdZe(VZAsJao+Vs`lRmAf8*Q|K5dqG89#)vQdijB8Ai6rTTDHlI==XGe zSS_mv>XwTeZAtD3ei*Nlic!V+DLK7TFeTC58~Yz)5Bu!q35}d44Fp#ET;GqUzOPD0 zUX^0G+Ii;0eoZ{*1m$FJ^SAHHj3>7huQ?QNm*Izh*0|A_OTz1dj><_`WIE(`W*9}< zNZ!L5NQyT%Iu^2Q_qmu|ybUawO2vo^tj598@Rd1tIeK|?bfi7LarOR@(GQo5A*(#` zt5+yYy-Zjmp^R$SBOwAg_zLb(c|%g#Cbp-M{II?$1@^*QT@ff}u|Op(1S$3|FQ40u zRWn{5KPQ{#h@ON%8fT0eo4=F0nrA13Udx%H{ zktum@1guej&{H}{0^kqSn^y`d=K6eI>cujt%nE)wi5cfa zJa~uRcj>#$um`ft#1c^phK<=n>FLb5a`Z16JVVoQMc|pS;)eO8dyL^386Z=^NU32HWAMcsN9q2WzB;^|`8y&0A67 zcP?-F8|mUydeKJxLXsbED@$TP#rEF3j!(eL+om*(ygq%l4T9JVGsUehg^n?>E8jd7 z_BZH58?4(Luq22?;+YIttEF@!Te;ubH3zEas;m5PoKAzH=Zx?xSUv4`R+OpL&hVG# z%vFp>Pq_Ps#BtoP6Qhq#9d@=$;fDU_0hne+GNc4yho@pip?H>!sYIxL1v%y^w@HqW z>+nv@uBLwdGTy5aH5KQc(;Vr<#INp*{<;YYrbJEVY$EKOC!9w=Cn8<$^B}MS+3Aui zm}mc{t(uJIQRU{|MUNFjDH*zjVIWor<%$KQ=i9!B77l&i@wcb?Ezk#Xq#!-tYmns! zd9Gy3=t8MXp)U?NQJmv*G1mx=p}?IK#gO%3)^~I^XG!A0G;1Ssqb~L`iDM4+x;(rc z)uWQ3^BDn3a42|C+=i)17w~xV%dP<{+2U2~KjQ5TwVR9JRtP#- z4F3>RxrkMGj|V2KR2jjI%#4H2cO4&M5?GgAg!+ay3AdLA#&5^Vjx0RUcxDC8m5CZ; zLU1ZW#e|DXVwS^Cq}DmsP*cgT>bRwT?2x(rqEJuPmap*L3LlDV^L!xpd#(dN zLT^E3sNjB*&Cr=+58AZ3uKc140&3fgDc4ql+q7nQx`fTh(|zsNp^De8lx;dAYG}%o0-PuS6=j7-iSy{%|g&;;XAj zK`Q+mJYzKX{jr7EociHrce;%jDHi>FR)}ah&cGElFAu~UAOEha;X z{4LS2g#A2_&Ow&S!{tQe1BoCw;4}_6#ifNEna&WhJZ3vJ|2&uW<{-Y6Is<}2xsQjOl zRr8F0pL3<8q<=38v5}tsm^`6&Pj{K9Ns}!E7lX-DWpX+HBX@+BUOEYs2nBO&+9E~F zt}+ITFS8X7jNBr`JG)%){h?9X)6>2c()bb`5-Qfm!vSZX-b8SCyId6Gp&koDit9xy zr%aU#X0cR}#Y+Nwrd*5^{E&@a9yd?q@AU9iJb!X}Ii$hgNok76q!D9qYUM zS#xIg$xX)Wn{;B6i`olC(O@RDeRjnpRZsc)Ep&T8q|BoHwQDtXEGc+fUeasG8$P5W zt()T0y^qwrGRf@zT$B&HollJYyQ$dp3q2i?EAnDjSbP zQ~iy@#-#YlbI}M(Bi?{VD~*QgTX*^^q40JCh_%CaA+0w{n$r+!!4KyfBg(A8vxCLw z>A1OH@k?#%UT*M=(q3EcCl_TwM`-;#AFk3mcuGo%4iTluyO7o>2~f0zsDzEl{TaQz z3zeXhsnSOD&Tw*}$LG79Mb#LRqon#+N?Jr<5*VXmQnqwvnq`WR=3DF@+GQM_Ge2)t6y|DLUJ=<4$^IX zt7rN!e5S=$@1FcxQ{2GW^!{6e?8?cV>1TEse(XtjDOLX0Nu^kpTUJ8LvARvIqE?=f zofPi*{L{k7|Y$mz3k)Vznxz#`7IJkKSkoL0_Y*r*ysu9K`y7E9V)$nt%E7e z*x7qLVuv?>9M--t$}X0PDNE;Wv}t^Dy1sia`b3>(Ks|b$pJ_H7twdhLyPLu*k6Y3{ zI|AQM;-8rbIlto@xZa)j*_<`MR;aG{AX~d;P0bm}rpP+!ov#ZL=Dk&9!yo~(lE-6x zz~e~w?q&gujF!l?0%1{WyzG=-D_TXL<%OV%bH^7|;ur~nFeLL$hUyC5R88A*sTB6? zj%TemcKV{|Is?(UTRK+leQcx&uBQ*Qk)1($cs$r!b6%62PCjNh`=SnY??v(4+; zk`ZoqgxcN-aPCs+;Q^8KGr_M0pPVA9!G;ephhwOa3w0 zyhk70#h6Vd>oW;gbOvLq822yt&SxOu-HdiI`iln{VmA+UU1QySCzW54Vy3avNG!VM zr+K_0G2jShZ02r5Tyhj#Cl{yR9U4IO&%@H>yzPD%ITz=S$^U z&Gp9er+VhTw?sVG!hJ>gHPTh*%Tla0ll@%0Bf z8Hlc01vqHHwtJyYb)-k5#hV&AIg>DbhX7%Ph9hwX26=y3?6gXF;U>yV+)vKt zN4<>px-_F8D7dq5+a>@bjGuHo3@VysrMa%&JWlXGOUXO^Fn`@|Ji}8OklJX;EPQ(& zgV3_SaCW)P@LPUw=>`9aHXic(7;wUn@8d*@3S4BY1VC@we)>DYq5k&StDHN-;!n`w z^QRy`=k)M{V?C7tG&3FD-u}5`do?Z-5zjC~|7{M7Z`f4TioJ0icS?lvl86mO8E;(E zE#qZls1ho-rfv*KFe^06KoBbf=NF@|n%|pMMX1lCjq3HQ$^R%phaEcahm|94E~XQm zlXW`0Ocuhu+}>61l6sM+O+@Qj`4o54UA1Fkf0EhD>PgF# zoNc4pVrz3q@Px!qB@Tp2@z8_pd7nAp${!Dx<4;%VOEkxRv5C}2tDTGXlwHY>pT192 z1D6)+fD&}sjPXDhnZ(*&HSmoUVRIi^wD3YE!_C$7OOb6~kFJxX9=k5eLE|gAid{l( zo3vFvJDky_9SXS&tS8M>RbZq|fyM)M0h5IV2C5cD!+Q6ZN}R!Zef${&X3V~KTTno4 z(Qv1skFpHxlAi&fm-Bi&(gpzSx1~2fU`wc4sRb5si~K9gqfUqm)K$ zAmWLm=HTr;n1gb1L3Qu|KIFCQ+}9H979@DWh0x#}IZ_lZzb>rI>Fc|VsgfCeF;lI+ z`E)Jwu;WO8y$yc-*>|f@E$NI$6qqi~c9bi<2r-(BDKZhO99Kpr*B;dm9 zOFy*Cm}yzu0!miOys=X?*qTRtKlAnTUF}4iH>|}WO>+Md0ahLuH1~^+MKSVgygVoK zCJm-Clhl%CcjqJOY^g;I*DfDZd=%QQ&JeGr!$>g(v)JFKeWvJTOOB%JMVG+$L8*=q- zB4=Oesovr(;jx_J!~M-464L3{Ed=9X+POD0;WnU1^I|3o2w7K~J1dE#pmud=9GJ9M zv9wvx>&2tHTxpAxYoO7=PHXzY1ikD@qh$xR){WZ2w zy`!sM@*;h_|D1zjEa}d92t&Mc>dTvzrVaQTV(aPz+tu@TzmK@| zeuc@dUC+hBw~ULLuK-v>y^F5$L6tlQ=Vbw94wj=_?gw&ud!!%GGDj0DLl6CmWZ6LM zw(xN`&FnkoX3xn{{-w1)eJ2IYLshI{&`Y?=kl^iz;EVmjj)sd*+waFKF-N>f*VG*W z1BTvDs%Z4Y-#Ti0)N$~h-SFNI4;+vAEgIhGP>Edk&^(nW!IPT7RFP8MBGw^O2@B-M zvGJc2mIc|1wle})ZRnDO9lS@pU$kY+LpdD))t^*#u3_%fViUe5a&9}&kC9RxY6jt| z_s?Y;GBsnDYJAKtf|7{4bHn%YQLq_BOXy*j^-$V-qnC>Z)HGy^Q!4v9D7wpF)0U}qc=yPNsA=#ZD8`MzYL42gFP=8$AM8xy7j~w` zvPr&60SXjo>zW%ijG~o%-mb!p zv93|Ir&uldbAZ=ExB@otbUaGFOI!TVEiH%QX~uB>NIemGhiFLdf%n4O#dlt2h*I}7 zY`VPt^6}Ss5yS-{=3*oLZ^mxjps`-a~Lue&5O|RM5$J0IA9g#|lt)SwM zOi8z2`-m$F+>(GI`Qt*zPRY$7)5N+J6Ck`YdWO=Y=AG=isGn8D%JoUZ&uJSfl6``u`|giU>I(yF*>2+wpr1<99B4EVb>RaObv z$bZ!Q(Y3%uowP0WMpQUnZRkrxTNN=?J*BC#LxY$dFN%zwzLx{aH~`fcN-_J6*N zX#bJ$rMk9>t8(F|y zZnVgKE8B3F(BcE?X8U{A`!?+qYRrUZu%0`gIbk`NjrkbKxs29v=(YDUowA5&(5*-p zZM#4#!T%EZP6#LH^Id(3AY#S&i92R}oV+(y(ABIV({Gy}01WUnIk+kn54G>{TJt{j z^?NdN-Rw~)n@VQRbGdWJ`n&LNvq zB9p)i&xVUS9%HUd!Mi0Bte|WJ8<$=mf=-lVmgd0sAf*%1tc;b@H+@gN7~Rv74t{pHB<$8W$+xW1rzXop#!BAgGV&} zTVTUeg)hnn!0pbLdT(OV%V|dw7jTa_kCXF%gDyv_nGM=gmp=Q(xhUxQA?W7#)^hA zh7DJci7<&Bx2g~Y1<=ojpU6ws_mR`QPn`Wwqg}EeE4#igHP$@Em$$JWp!fJ4R;+v* zb~rb(6~UFLL^GplINKrUe5(iBNs9pPgY$;r&H9{0FUb~z3<{Tkc0*XE_l($}H0-KT zMsxL$#cPh<}K`>3AtcNU$GM1YOk%!j72b_dtD( zuT%-eZ2P5;iJ5=@Bvs&ifXTm7iq8lqUJPEiJ<;T`LUtUoj4o`?@f+(xVd{>}7j>ES zKC#t%qKZZ*F?aPN0h}=Vtd6Mkh65Piyl0f=(K6)c-g9Zmk31{x+(E~bmlD_D$EDx% zI$Qop8o2*OIY8kP!PBO$}LLz>C{{ON&UcXz)ao$ z8ZL@8;j=4pD5ggSVzG4i1-?Y3gVEKbqd`8$EYA-e;I{KN^-0iGd)dxSkn?#XAYI%r##g3p}?anCF{3O3c?rvqr z5ZCgI!#6D{0R^<->Lkuh1_Af3MkdSe@A2;c5~LcPBngbNS?EO>#7Q4fxI5INR@*BR*z+#nWsZ>A1x?N0e??mnuQ zjntiy3WeXdRsFYzJ)Q`xhVZ>JX1n`u@@L_f=+us!?$zY`oUXS-f|GHx9JtQorNM)K zs15gxuh=4pqFOa0wd+KI!&K=NhSU*Q=TREHB$e&78~fwu0b@X2sIq*bN7fl`ThOsx z?7cn+MmXm;fI|Ivjv}Pn{zWrWjb7af@C)$Jxv{@ky_ZB-aRyHFg`>df=VOCgKh#H~ zyP;fJ?-Ko29po3)4i&B z8=h~taYpW6)_@2Ph-x%dbuxGp+AsYptJz5VgVSeGVY~{yCx7ey;(%6a8Z_(bST8Oq zTTlN|c(&Mc_gvN9TB@YpE)I}a2h4Q$bg%Sl8Sn#apiP(U&-qCG8|z&T$L${Qk2KEt z4PN(RQ3$nRk@C$Mez_uf_mU0&-^9$-+w(+3nd1sn$j_G+-Ud&6YP&<@f8t!CJufn| zS|dW(86*?R)vlD8>-nf-3}^7^;4=a!&5H?Q2=T)QnN;=RU)F4&9mx{g`wxkmm#g zXwz&6$^5IIVYORDFm4pi;9gh4ob!3fRkM)URbNhua?1_!%p(B~*LlC~Z%n9_k4p?o zey-`>Ajcr*Aon2eApiC~ff0Op-MX8()1Ep&pv&D=Sd3I3*I9a1KZ+e`-JU(pHm*gn zGxi{v^UM(PTr#h*%jei@mA9(J*z@5#3aJYGt5(dW9i|!m+S?<~+;y(L%kBl*5!VGV zDoqjA$~MgUoR+^QlBj(6YX64-voDqKBB@7P>h#PA<9t;5`Pjgbm8c8PI-tIWrxquv zgiR}j%ttGPUFViw44!olAJ0wIS&a%By3J}I+Li$C5f{kei&UVQ+8G=Vp93nBY%8)!}El zS3CLO@e-iB+Bn*Rt|UQ9C}1KscuWc!t`Elq!2dkOW>cbn7`R=vU-0#o@2=(>ek*+)wt$^_aaU+q;E>`S}Q^x^W#X9RdsxQ zkY8(~-50}=`L_lBSZA4Jv@^j#6Z4T@)ZKJG-f}Od-hxL1x>s4--;48o4sr*xBANbK zgj45IRf%iq4c0U{=HG28JT>p_1fPJqrAgHOH1w5PcI_r`tTz<$@EG0meSG8-b?@)c z_yt@W0@js6hpD>5C`5k=TRT&}$3AXx?!R|2c`j{KI{bIdn!VC7ek)g)l&jH+z}ZA> z(qu=n{#{OaqWkz%&ZCP9;1w(CQoOc5`_ydDwY%xWq7)iFowE}Y*l)ASie&ryKR0YM z2hkKBga9=9R;*tH&mi(I3;aF_2o-y34a<0+6|+XJ;r%m`jWKFZYrrFM@@3fYjkI}|B{jDXYfr&RByXr~%6gi>s$=>--mc11*naWs0m8L*Jr@pX}4DS(T4m>(vq%vIHy}v zRtrBw&Zy5+yQ%$dt4G0xH_e+oe~+ewIf`R=_fGTrTbyU-)SOnMR!vj#u?+wJ=N}5F b4O|82styj8_|B)@0Y2oVm8D7~OalK8hv0q> literal 0 HcmV?d00001 From 97c51a2bad9177ade994e5ef5e9d7f8303b94d16 Mon Sep 17 00:00:00 2001 From: Tim King Date: Mon, 26 Feb 2024 11:24:26 -0800 Subject: [PATCH 32/47] go/analysis/passes/shift: support constants in float syntax Adds support for integer constants expressed in float syntax '2.0'. Also fixes the same issue in gopls/internal/golang/hover.go. Fixes golang/go#65939 Change-Id: Ic42b4cc7c6aedf7f115e3195044f06001cb6fb12 Reviewed-on: https://go-review.googlesource.com/c/tools/+/567115 TryBot-Result: Gopher Robot LUCI-TryBot-Result: Go LUCI Run-TryBot: Tim King Reviewed-by: Alan Donovan --- go/analysis/passes/shift/shift.go | 3 ++- go/analysis/passes/shift/testdata/src/a/a.go | 5 +++++ gopls/internal/golang/hover.go | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/go/analysis/passes/shift/shift.go b/go/analysis/passes/shift/shift.go index e272df709f3..61f16501bb3 100644 --- a/go/analysis/passes/shift/shift.go +++ b/go/analysis/passes/shift/shift.go @@ -89,7 +89,8 @@ func checkLongShift(pass *analysis.Pass, node ast.Node, x, y ast.Expr) { if v == nil { return } - amt, ok := constant.Int64Val(v) + u := constant.ToInt(v) // either an Int or Unknown + amt, ok := constant.Int64Val(u) if !ok { return } diff --git a/go/analysis/passes/shift/testdata/src/a/a.go b/go/analysis/passes/shift/testdata/src/a/a.go index 796fcaa6ec4..558ece6bf8f 100644 --- a/go/analysis/passes/shift/testdata/src/a/a.go +++ b/go/analysis/passes/shift/testdata/src/a/a.go @@ -153,3 +153,8 @@ func ShiftDeadCode() { _ = i << 64 // want "too small for shift" } } + +func issue65939() { + a := 1 + println(a << 2.0) +} diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index e7b903df26d..8e842739276 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -873,7 +873,7 @@ func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file case *types.Named: // Try to add a formatted duration as an inline comment. pkg := typ.Obj().Pkg() - if pkg.Path() == "time" && typ.Obj().Name() == "Duration" { + if pkg.Path() == "time" && typ.Obj().Name() == "Duration" && obj.Val().Kind() == constant.Int { if d, ok := constant.Int64Val(obj.Val()); ok { comment = time.Duration(d).String() } From 9b589093a7304ba706b5de90b7de4cff26f926bd Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 26 Feb 2024 17:35:03 +0000 Subject: [PATCH 33/47] gopls: upgrade dependencies following the v0.15.0 release Fixes golang/go#64973 Change-Id: I2af27de59dc48e48c49e425342b6a25486b1656d Reviewed-on: https://go-review.googlesource.com/c/tools/+/566956 LUCI-TryBot-Result: Go LUCI Reviewed-by: Hyang-Ah Hana Kim --- gopls/go.mod | 6 +++--- gopls/go.sum | 14 ++++---------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/gopls/go.mod b/gopls/go.mod index f17fdcaa522..6a6bec7c5f1 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -5,15 +5,15 @@ go 1.18 require ( github.com/google/go-cmp v0.6.0 github.com/jba/printsrc v0.2.2 - github.com/jba/templatecheck v0.6.0 + github.com/jba/templatecheck v0.7.0 golang.org/x/mod v0.15.0 golang.org/x/sync v0.6.0 golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78 golang.org/x/text v0.14.0 - golang.org/x/tools v0.17.0 + golang.org/x/tools v0.18.0 golang.org/x/vuln v1.0.1 gopkg.in/yaml.v3 v3.0.1 - honnef.co/go/tools v0.4.6 + honnef.co/go/tools v0.4.7 mvdan.cc/gofumpt v0.6.0 mvdan.cc/xurls/v2 v2.5.0 ) diff --git a/gopls/go.sum b/gopls/go.sum index 9ab4bd75926..ca69d20cfc4 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -3,13 +3,12 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 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.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/jba/printsrc v0.2.2 h1:9OHK51UT+/iMAEBlQIIXW04qvKyF3/vvLuwW/hL8tDU= github.com/jba/printsrc v0.2.2/go.mod h1:1xULjw59sL0dPdWpDoVU06TIEO/Wnfv6AHRpiElTwYM= -github.com/jba/templatecheck v0.6.0 h1:SwM8C4hlK/YNLsdcXStfnHWE2HKkuTVwy5FKQHt5ro8= -github.com/jba/templatecheck v0.6.0/go.mod h1:/1k7EajoSErFI9GLHAsiIJEaNLt3ALKNw2TV7z2SYv4= +github.com/jba/templatecheck v0.7.0 h1:wjTb/VhGgSFeim5zjWVePBdaMo28X74bGLSABZV+zIA= +github.com/jba/templatecheck v0.7.0/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -18,7 +17,6 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf 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= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -27,12 +25,8 @@ golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509 h1:Nr7eTQpQZ/ytesxDJpQgaf0t4sdLnnDtAbmtViTrSUo= -golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509/go.mod h1:ZthVHHkOi8rlMEsfFr3Ie42Ym1NonbFNNRKW3ci0UrU= -golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78 h1:vcVnuftN4J4UKLRcgetjzfU9FjjgXUUYUc3JhFplgV4= golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= @@ -49,8 +43,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= -honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +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/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= From 4db45793ff6b9988dcf2ca4cfc42d2ef51ab971e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 26 Feb 2024 16:43:25 -0500 Subject: [PATCH 34/47] cmd/deadcode: avoid ssautil.AllFunctions This change causes deadcode to enumerate the "universe set" of functions based on the source-level declarations, not the ssautil.AllFunctions helper. The former set is accurate (and also much cheaper to compute), whereas the latter is a mess (as its doc comment attests). In particular, it omits unexported methods of uninstantiated types, which are of course unreachable. This makes for some sense for whole-program analysis, but none at all for a dead code tool that specifically wants to know the useless functions. The new logic is pleasantly simpler. Also, a test. Fixes golang/go#65915 Change-Id: I7a4a64fd3da18d089044530911cdb2e3b42d0db5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/567156 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- cmd/deadcode/deadcode.go | 62 ++++++++++++-------------- cmd/deadcode/testdata/issue65915.txtar | 44 ++++++++++++++++++ 2 files changed, 72 insertions(+), 34 deletions(-) create mode 100644 cmd/deadcode/testdata/issue65915.txtar diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index a8b4f073831..38a067e69f9 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -132,16 +132,6 @@ func main() { log.Fatalf("packages contain errors") } - // Gather names of generated files. - generated := make(map[string]bool) - packages.Visit(initial, nil, func(p *packages.Package) { - for _, file := range p.Syntax { - if isGenerated(file) { - generated[p.Fset.File(file.Pos()).Name()] = true - } - } - }) - // If -filter is unset, use first module (if available). if *filterFlag == "" { if mod := initial[0].Module; mod != nil && mod.Path != "" { @@ -169,6 +159,32 @@ func main() { roots = append(roots, main.Func("init"), main.Func("main")) } + // Gather all source-level functions, + // as the user interface is expressed in terms of them. + // + // We ignore synthetic wrappers, and nested functions. Literal + // functions passed as arguments to other functions are of + // course address-taken and there exists a dynamic call of + // that signature, so when they are unreachable, it is + // invariably because the parent is unreachable. + var sourceFuncs []*ssa.Function + generated := make(map[string]bool) + packages.Visit(initial, nil, func(p *packages.Package) { + for _, file := range p.Syntax { + for _, decl := range file.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok { + obj := p.TypesInfo.Defs[decl.Name].(*types.Func) + fn := prog.FuncValue(obj) + sourceFuncs = append(sourceFuncs, fn) + } + } + + if isGenerated(file) { + generated[p.Fset.File(file.Pos()).Name()] = true + } + } + }) + // Compute the reachabilty from main. // (Build a call graph only for -whylive.) res := rta.Analyze(roots, *whyLiveFlag != "") @@ -195,10 +211,7 @@ func main() { // is not dead, by showing a path to it from some root. if *whyLiveFlag != "" { targets := make(map[*ssa.Function]bool) - for fn := range ssautil.AllFunctions(prog) { - if fn.Synthetic != "" || fn.Object() == nil { - continue // not a source-level named function - } + for _, fn := range sourceFuncs { if prettyName(fn, true) == *whyLiveFlag { targets[fn] = true } @@ -263,26 +276,7 @@ func main() { // Group unreachable functions by package path. byPkgPath := make(map[string]map[*ssa.Function]bool) - for fn := range ssautil.AllFunctions(prog) { - if fn.Synthetic != "" { - continue // ignore synthetic wrappers etc - } - - // Use generic, as instantiations may not have a Pkg. - if orig := fn.Origin(); orig != nil { - fn = orig - } - - // Ignore unreachable nested functions. - // Literal functions passed as arguments to other - // functions are of course address-taken and there - // exists a dynamic call of that signature, so when - // they are unreachable, it is invariably because the - // parent is unreachable. - if fn.Parent() != nil { - continue - } - + for _, fn := range sourceFuncs { posn := prog.Fset.Position(fn.Pos()) if !reachablePosn[posn] { diff --git a/cmd/deadcode/testdata/issue65915.txtar b/cmd/deadcode/testdata/issue65915.txtar new file mode 100644 index 00000000000..a7c15630bdd --- /dev/null +++ b/cmd/deadcode/testdata/issue65915.txtar @@ -0,0 +1,44 @@ +# Regression test for issue 65915: the enumeration of source-level +# functions used the flawed ssautil.AllFunctions, causing it to +# miss some unexported ones. + + deadcode -filter= example.com + + want "unreachable func: example.UnUsed" + want "unreachable func: example.unUsed" + want "unreachable func: PublicExample.UnUsed" + want "unreachable func: PublicExample.unUsed" + +-- go.mod -- +module example.com +go 1.18 + +-- main.go -- +package main + +type example struct{} + +func (e example) UnUsed() {} + +func (e example) Used() {} + +func (e example) unUsed() {} + +func (e example) used() {} + +type PublicExample struct{} + +func (p PublicExample) UnUsed() {} + +func (p PublicExample) Used() {} + +func (p PublicExample) unUsed() {} + +func (p PublicExample) used() {} + +func main() { + example{}.Used() + example{}.used() + PublicExample{}.Used() + PublicExample{}.used() +} From bbdc81d9f7cb7056e0e428bf9504dc0bcbd6d6ae Mon Sep 17 00:00:00 2001 From: Fata Nugraha Date: Tue, 27 Feb 2024 13:04:32 +0000 Subject: [PATCH 35/47] gopls: implement code action to split/group lines Implement a code action to easily refactor function arguments, return values, and composite literal elements into separate lines or a single line. This feature would be particularly helpful when working on large business-related features that often require long variable names. The ability to quickly split or group lines would significantly accelerate refactoring efforts to ensure code adheres to specified character limits. Fixes golang/go#65156 Change-Id: I00aaa1377735f14808424e8e99fa0b7eeab90a7a GitHub-Last-Rev: d3c5b57d81f45864e79d7d86444b5b7892b06ffc GitHub-Pull-Request: golang/tools#470 Reviewed-on: https://go-review.googlesource.com/c/tools/+/558475 Reviewed-by: Alan Donovan Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/codeaction.go | 26 ++ gopls/internal/golang/fix.go | 4 + gopls/internal/golang/lines.go | 261 ++++++++++++++++++ .../marker/testdata/codeaction/grouplines.txt | 206 ++++++++++++++ .../testdata/codeaction/removeparam.txt | 8 +- .../codeaction/removeparam_resolve.txt | 8 +- .../marker/testdata/codeaction/splitlines.txt | 223 +++++++++++++++ 7 files changed, 728 insertions(+), 8 deletions(-) create mode 100644 gopls/internal/golang/lines.go create mode 100644 gopls/internal/test/marker/testdata/codeaction/grouplines.txt create mode 100644 gopls/internal/test/marker/testdata/codeaction/splitlines.txt diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index df4ca513ce2..cab1b42f4b7 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -304,6 +304,32 @@ func getRewriteCodeActions(pkg *cache.Package, pgf *parsego.File, fh file.Handle 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 + } + 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 + } + commands = append(commands, cmd) + } + // N.B.: an inspector only pays for itself after ~5 passes, which means we're // currently not getting a good deal on this inspection. // diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go index 6f07cb869c5..4b196383175 100644 --- a/gopls/internal/golang/fix.go +++ b/gopls/internal/golang/fix.go @@ -64,6 +64,8 @@ const ( fixExtractMethod = "extract_method" fixInlineCall = "inline_call" fixInvertIfCondition = "invert_if_condition" + fixSplitLines = "split_lines" + fixJoinLines = "join_lines" ) // ApplyFix applies the specified kind of suggested fix to the given @@ -115,6 +117,8 @@ func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file fixExtractVariable: singleFile(extractVariable), fixInlineCall: inlineCall, fixInvertIfCondition: singleFile(invertIfCondition), + fixSplitLines: singleFile(splitLines), + fixJoinLines: singleFile(joinLines), } fixer, ok := fixers[fix] if !ok { diff --git a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go new file mode 100644 index 00000000000..1c4b562280d --- /dev/null +++ b/gopls/internal/golang/lines.go @@ -0,0 +1,261 @@ +// 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 golang + +// This file defines refactorings for splitting lists of elements +// (arguments, literals, etc) across multiple lines, and joining +// them into a single line. + +import ( + "bytes" + "go/ast" + "go/token" + "go/types" + "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 an enclosing curly bracket/parens into separate +// lines. +func CanSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) { + itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end) + if itemType == "" { + return "", false, nil + } + + if !canSplitJoinLines(items, comments) { + return "", false, nil + } + + for i := 1; i < len(items); i++ { + prevLine := safetoken.EndPosition(fset, items[i-1].End()).Line + curLine := safetoken.StartPosition(fset, items[i].Pos()).Line + if prevLine == curLine { + return "Split " + itemType + " into separate lines", true, nil + } + } + + return "", false, nil +} + +// CanJoinLines checks whether we can join lists of elements inside an enclosing curly bracket/parens into a single line. +func CanJoinLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) { + itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end) + if itemType == "" { + return "", false, nil + } + + if !canSplitJoinLines(items, comments) { + return "", false, nil + } + + for i := 1; i < len(items); i++ { + prevLine := safetoken.EndPosition(fset, items[i-1].End()).Line + curLine := safetoken.StartPosition(fset, items[i].Pos()).Line + if prevLine != curLine { + return "Join " + itemType + " into one line", true, nil + } + } + + return "", false, nil +} + +// canSplitJoinLines determines whether we should split/join the lines or not. +func canSplitJoinLines(items []ast.Node, comments []*ast.CommentGroup) bool { + if len(items) <= 1 { + return false + } + + for _, cg := range comments { + if !strings.HasPrefix(cg.List[0].Text, "/*") { + return false // can't split/join lists containing "//" comments + } + } + + return true +} + +// splitLines is a singleFile fixer. +func splitLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end) + if itemType == "" { + return nil, nil, nil // no fix available + } + + return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ",\n", "\n", ",\n"+indent, indent+"\t"), nil +} + +// joinLines is a singleFile fixer. +func joinLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { + itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end) + if itemType == "" { + return nil, nil, nil // no fix available + } + + return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ", ", "", "", ""), nil +} + +// processLines is the common operation for both split and join lines because this split/join operation is +// essentially a transformation of the separating whitespace. +func processLines(fset *token.FileSet, items []ast.Node, comments []*ast.CommentGroup, src []byte, braceOpen, braceClose token.Pos, sep, prefix, suffix, indent string) *analysis.SuggestedFix { + nodes := slices.Clone(items) + + // box *ast.CommentGroup to ast.Node for easier processing later. + for _, cg := range comments { + nodes = append(nodes, cg) + } + + // Sort to interleave comments and nodes. + sort.Slice(nodes, func(i, j int) bool { + return nodes[i].Pos() < nodes[j].Pos() + }) + + edits := []analysis.TextEdit{ + { + Pos: token.Pos(int(braceOpen) + len("{")), + End: nodes[0].Pos(), + NewText: []byte(prefix + indent), + }, + { + Pos: nodes[len(nodes)-1].End(), + End: braceClose, + NewText: []byte(suffix), + }, + } + + for i := 1; i < len(nodes); i++ { + pos, end := nodes[i-1].End(), nodes[i].Pos() + if pos > end { + // this will happen if we have a /*-style comment inside of a Field + // e.g. `a /*comment here */ int` + // + // we will ignore as we only care about finding the field delimiter. + continue + } + + // at this point, the `,` token in between 2 nodes here must be the field delimiter. + posOffset := safetoken.EndPosition(fset, pos).Offset + endOffset := safetoken.StartPosition(fset, end).Offset + if bytes.IndexByte(src[posOffset:endOffset], ',') == -1 { + // nodes[i] or nodes[i-1] is a comment hence no delimiter in between + // in such case, do nothing. + continue + } + + edits = append(edits, analysis.TextEdit{Pos: pos, End: end, NewText: []byte(sep + indent)}) + } + + return &analysis.SuggestedFix{TextEdits: edits} +} + +// findSplitJoinTarget returns the first curly bracket/parens that encloses the current cursor. +func findSplitJoinTarget(fset *token.FileSet, file *ast.File, src []byte, start, end token.Pos) (itemType string, items []ast.Node, comments []*ast.CommentGroup, indent string, open, close token.Pos) { + isCursorInside := func(nodePos, nodeEnd token.Pos) bool { + return nodePos < start && end < nodeEnd + } + + findTarget := func() (targetType string, target ast.Node, open, close token.Pos) { + path, _ := astutil.PathEnclosingInterval(file, start, end) + for _, node := range path { + switch node := node.(type) { + case *ast.FuncDecl: + // target struct method declarations. + // function (...) someMethod(a int, b int, c int) (d int, e, int) {} + params := node.Type.Params + if isCursorInside(params.Opening, params.Closing) { + return "parameters", params, params.Opening, params.Closing + } + + results := node.Type.Results + if results != nil && isCursorInside(results.Opening, results.Closing) { + return "return values", results, results.Opening, results.Closing + } + case *ast.FuncType: + // target function signature args and result. + // type someFunc func (a int, b int, c int) (d int, e int) + params := node.Params + if isCursorInside(params.Opening, params.Closing) { + return "parameters", params, params.Opening, params.Closing + } + + results := node.Results + if results != nil && isCursorInside(results.Opening, results.Closing) { + return "return values", results, results.Opening, results.Closing + } + case *ast.CallExpr: + // target function calls. + // someFunction(a, b, c) + if isCursorInside(node.Lparen, node.Rparen) { + return "parameters", node, node.Lparen, node.Rparen + } + case *ast.CompositeLit: + // target composite lit instantiation (structs, maps, arrays). + // A{b: 1, c: 2, d: 3} + if isCursorInside(node.Lbrace, node.Rbrace) { + return "elements", node, node.Lbrace, node.Rbrace + } + } + } + + return "", nil, 0, 0 + } + + targetType, targetNode, open, close := findTarget() + if targetType == "" { + return "", nil, nil, "", 0, 0 + } + + switch node := targetNode.(type) { + case *ast.FieldList: + for _, field := range node.List { + items = append(items, field) + } + case *ast.CallExpr: + for _, arg := range node.Args { + items = append(items, arg) + } + case *ast.CompositeLit: + for _, arg := range node.Elts { + items = append(items, arg) + } + } + + // preserve comments separately as it's not part of the targetNode AST. + for _, cg := range file.Comments { + if open <= cg.Pos() && cg.Pos() < close { + comments = append(comments, cg) + } + } + + // indent is the leading whitespace before the opening curly bracket/paren. + // + // in case where we don't have access to src yet i.e. src == nil + // it's fine to return incorrect indent because we don't need it yet. + indent = "" + if len(src) > 0 { + var pos token.Pos + switch node := targetNode.(type) { + case *ast.FieldList: + pos = node.Opening + case *ast.CallExpr: + pos = node.Lparen + case *ast.CompositeLit: + pos = node.Lbrace + } + + split := bytes.Split(src, []byte("\n")) + targetLineNumber := safetoken.StartPosition(fset, pos).Line + firstLine := string(split[targetLineNumber-1]) + trimmed := strings.TrimSpace(string(firstLine)) + indent = firstLine[:strings.Index(firstLine, trimmed)] + } + + return targetType, items, comments, indent, open, close +} diff --git a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt new file mode 100644 index 00000000000..8d1134c5d6c --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt @@ -0,0 +1,206 @@ +This test exercises the refactoring of putting arguments, return values, and composite literal elements into a +single line. + +-- go.mod -- +module unused.mod + +go 1.18 + +-- 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) { + return a, b, c, x, y +} + +-- @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) { + 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 /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, + r2, r3 int64, + r4 int, + r5 int, +) { + return a, b, c, x, y +} + +-- @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) { + return a, b, c, x, y +} + +-- 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) + +-- @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) + +-- 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, +) + +-- @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) + +-- func_call/func_call.go -- +package func_call + +import "fmt" + +func a() { + fmt.Println( + 1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, + 2, + 3, + fmt.Sprintf("hello %d", 4), + ) +} + +-- @func_call/func_call/func_call.go -- +package func_call + +import "fmt" + +func a() { + fmt.Println(1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4)) +} + +-- indent/indent.go -- +package indent + +import "fmt" + +func a() { + fmt.Println( + 1, + 2, + 3, + fmt.Sprintf( + "hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join parameters into one line")*/, + 4, + )) +} + +-- @indent/indent/indent.go -- +package indent + +import "fmt" + +func a() { + fmt.Println( + 1, + 2, + 3, + fmt.Sprintf("hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join parameters into one line")*/, 4)) +} + +-- structelts/structelts.go -- +package structelts + +type A struct{ + a int + b int +} + +func a() { + _ = A{ + a: 1, + b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/, + } +} + +-- @structelts/structelts/structelts.go -- +package structelts + +type A struct{ + a int + b int +} + +func a() { + _ = A{a: 1, b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/} +} + +-- sliceelts/sliceelts.go -- +package sliceelts + +func a() { + _ = []int{ + 1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, + 2, + } +} + +-- @sliceelts/sliceelts/sliceelts.go -- +package sliceelts + +func a() { + _ = []int{1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, 2} +} + +-- mapelts/mapelts.go -- +package mapelts + +func a() { + _ = map[string]int{ + "a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, + "b": 2, + } +} + +-- @mapelts/mapelts/mapelts.go -- +package mapelts + +func a() { + _ = map[string]int{"a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, "b": 2} +} + +-- 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) { + return x, y +} + +-- @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) { + return x, y +} + diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt index 17a058f2b82..25ec6ae1d96 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt @@ -99,7 +99,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field) +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") } func _() { @@ -108,7 +108,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field) +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") } func _() { @@ -162,7 +162,7 @@ func i() []any -- ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") } func _() { @@ -176,7 +176,7 @@ func h() (int, int) -- @ellipsis2/ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) +func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") } func _() { diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt index 1c04aa047cf..c67e8a5d039 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt @@ -110,7 +110,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field) +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") } func _() { @@ -119,7 +119,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field) +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") } func _() { @@ -173,7 +173,7 @@ func i() []any -- ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") } func _() { @@ -187,7 +187,7 @@ func h() (int, int) -- @ellipsis2/ellipsis2/ellipsis2.go -- package ellipsis2 -func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) +func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") } func _() { diff --git a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt new file mode 100644 index 00000000000..a02e39505e5 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt @@ -0,0 +1,223 @@ +This test exercises the refactoring of putting arguments, return values, and composite literal elements +into separate lines. + +-- go.mod -- +module unused.mod + +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) + return a, b, c, x, y +} + +-- @func_arg/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) + 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) + return a, b, c, x, y +} + +-- @func_ret/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) + 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) + +-- @functype_arg/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) + +-- 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) + +-- @functype_ret/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) + +-- func_call/func_call.go -- +package func_call + +import "fmt" + +func a() { + fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", func_call) +} + +-- @func_call/func_call/func_call.go -- +package func_call + +import "fmt" + +func a() { + fmt.Println( + 1, + 2, + 3, + fmt.Sprintf("hello %d", 4), + ) //@codeaction("1", "1", "refactor.rewrite", func_call) +} + +-- indent/indent.go -- +package indent + +import "fmt" + +func a() { + fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split parameters into separate lines") +} + +-- @indent/indent/indent.go -- +package indent + +import "fmt" + +func a() { + fmt.Println(1, 2, 3, fmt.Sprintf( + "hello %d", + 4, + )) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split parameters into separate lines") +} + +-- indent2/indent2.go -- +package indent2 + +import "fmt" + +func a() { + fmt. + Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split parameters into separate lines") +} + +-- @indent2/indent2/indent2.go -- +package indent2 + +import "fmt" + +func a() { + fmt. + Println( + 1, + 2, + 3, + fmt.Sprintf("hello %d", 4), + ) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split parameters into separate lines") +} + +-- structelts/structelts.go -- +package structelts + +type A struct{ + a int + b int +} + +func a() { + _ = A{a: 1, b: 2} //@codeaction("b", "b", "refactor.rewrite", structelts) +} + +-- @structelts/structelts/structelts.go -- +package structelts + +type A struct{ + a int + b int +} + +func a() { + _ = A{ + a: 1, + b: 2, + } //@codeaction("b", "b", "refactor.rewrite", structelts) +} + +-- sliceelts/sliceelts.go -- +package sliceelts + +func a() { + _ = []int{1, 2} //@codeaction("1", "1", "refactor.rewrite", sliceelts) +} + +-- @sliceelts/sliceelts/sliceelts.go -- +package sliceelts + +func a() { + _ = []int{ + 1, + 2, + } //@codeaction("1", "1", "refactor.rewrite", sliceelts) +} + +-- mapelts/mapelts.go -- +package mapelts + +func a() { + _ = map[string]int{"a": 1, "b": 2} //@codeaction("1", "1", "refactor.rewrite", mapelts) +} + +-- @mapelts/mapelts/mapelts.go -- +package mapelts + +func a() { + _ = map[string]int{ + "a": 1, + "b": 2, + } //@codeaction("1", "1", "refactor.rewrite", 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) + return x, y +} + +-- @starcomment/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) + return x, y +} + From c1f340a8c0b6c9fc336cdb1b6a58dde56d1b7e88 Mon Sep 17 00:00:00 2001 From: Patrick Pichler Date: Tue, 27 Feb 2024 11:23:51 +0000 Subject: [PATCH 36/47] gopls: add non nil if check around function result highlight The result of funcType will be nil, if a function does not return any values. This caused an SIGSEGV before. Fixes golang/go#65952 Change-Id: Ibf4ac3070744f42033504220f05b35a78c97d992 GitHub-Last-Rev: 74182b258d52620694f2d10c1d1518493dca907e GitHub-Pull-Request: golang/tools#480 Reviewed-on: https://go-review.googlesource.com/c/tools/+/567275 Reviewed-by: Mauri de Souza Meneguzzo Reviewed-by: Robert Findley Reviewed-by: Hyang-Ah Hana Kim LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/highlight.go | 70 +++++++++++++++--------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index 52483acdb9f..6cc8f6cb812 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go @@ -232,45 +232,47 @@ findEnclosingFunc: } } - // Scan fields, either adding highlights according to the highlightIndexes - // computed above, or accounting for the cursor position within the result - // list. - // (We do both at once to avoid repeating the cumbersome field traversal.) - i := 0 - findField: - for _, field := range funcType.Results.List { - for j, name := range field.Names { - if inNode(name) || highlightIndexes[i+j] { - result[posRange{name.Pos(), name.End()}] = unit{} - highlightIndexes[i+j] = true - break findField // found/highlighted the specific name - } - } - // If the cursor is in a field but not in a name (e.g. in the space, or - // the type), highlight the whole field. - // - // Note that this may not be ideal if we're at e.g. - // - // (x,‸y int, z int8) - // - // ...where it would make more sense to highlight only y. But we don't - // reach this function if not in a func, return, ident, or basiclit. - if inNode(field) || highlightIndexes[i] { - result[posRange{field.Pos(), field.End()}] = unit{} - highlightIndexes[i] = true - if inNode(field) { - for j := range field.Names { + if funcType.Results != nil { + // Scan fields, either adding highlights according to the highlightIndexes + // computed above, or accounting for the cursor position within the result + // list. + // (We do both at once to avoid repeating the cumbersome field traversal.) + i := 0 + findField: + for _, field := range funcType.Results.List { + for j, name := range field.Names { + if inNode(name) || highlightIndexes[i+j] { + result[posRange{name.Pos(), name.End()}] = unit{} highlightIndexes[i+j] = true + break findField // found/highlighted the specific name } } - break findField // found/highlighted the field - } + // If the cursor is in a field but not in a name (e.g. in the space, or + // the type), highlight the whole field. + // + // Note that this may not be ideal if we're at e.g. + // + // (x,‸y int, z int8) + // + // ...where it would make more sense to highlight only y. But we don't + // reach this function if not in a func, return, ident, or basiclit. + if inNode(field) || highlightIndexes[i] { + result[posRange{field.Pos(), field.End()}] = unit{} + highlightIndexes[i] = true + if inNode(field) { + for j := range field.Names { + highlightIndexes[i+j] = true + } + } + break findField // found/highlighted the field + } - n := len(field.Names) - if n == 0 { - n = 1 + n := len(field.Names) + if n == 0 { + n = 1 + } + i += n } - i += n } } } From 77c2a671778bc78ecb786521f31168ec26eb4ed6 Mon Sep 17 00:00:00 2001 From: sivchari Date: Fri, 23 Feb 2024 19:28:38 +0900 Subject: [PATCH 37/47] benchmark/parse: fix format The format of the parse_test.go file was not consistent. This commit fixes the format of the file. Change-Id: I5ee8d5ae77b9830f58f15d83e724875367e894bd Reviewed-on: https://go-review.googlesource.com/c/tools/+/566376 Reviewed-by: Robert Findley Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI Reviewed-by: Carlos Amedee --- benchmark/parse/parse_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/parse/parse_test.go b/benchmark/parse/parse_test.go index ca3dfa658f6..219d1dbb32d 100644 --- a/benchmark/parse/parse_test.go +++ b/benchmark/parse/parse_test.go @@ -59,11 +59,11 @@ func TestParseLine(t *testing.T) { // error handling cases { line: "BenchPress 100 19.6 ns/op", // non-benchmark - err: true, + err: true, }, { line: "BenchmarkEncrypt lots 19.6 ns/op", // non-int iterations - err: true, + err: true, }, { line: "BenchmarkBridge 100000000 19.6 smoots", // unknown unit From fc70354354e63fa88b8cba95d89e3115bdd717dd Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 27 Feb 2024 14:55:19 +0000 Subject: [PATCH 38/47] gopls/internal/test: add test for NPE in control flow highlighting Add a test for the fix in golang/go#65952: a nil pointer exception when highlighting a return value in a function returning no results. Also, merge tests related to control flow highlighting, since it is convenient to be able to run them together, and since there is nontrivial overhead to tiny tests. Updates golang/go#65952 Change-Id: Ibf8c7c6f0f4feed6dc7a283736bc038600a0bf04 Reviewed-on: https://go-review.googlesource.com/c/tools/+/567256 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley --- .../marker/testdata/highlight/controlflow.txt | 71 +++++++++++++++++++ .../marker/testdata/highlight/issue60589.txt | 30 -------- .../marker/testdata/highlight/issue65516.txt | 7 -- 3 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 gopls/internal/test/marker/testdata/highlight/controlflow.txt delete mode 100644 gopls/internal/test/marker/testdata/highlight/issue60589.txt delete mode 100644 gopls/internal/test/marker/testdata/highlight/issue65516.txt diff --git a/gopls/internal/test/marker/testdata/highlight/controlflow.txt b/gopls/internal/test/marker/testdata/highlight/controlflow.txt new file mode 100644 index 00000000000..25cc9394a47 --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/controlflow.txt @@ -0,0 +1,71 @@ +This test verifies document highlighting for control flow. + +-- go.mod -- +module mod.com + +go 1.18 + +-- p.go -- +package p + +-- issue60589.go -- +package p + +// This test verifies that control flow lighlighting correctly +// accounts for multi-name result parameters. +// In golang/go#60589, it did not. + +func _() (foo int, bar, baz string) { //@ loc(func, "func"), loc(foo, "foo"), loc(fooint, "foo int"), loc(int, "int"), loc(bar, "bar"), loc(beforebaz, " baz"), loc(baz, "baz"), loc(barbazstring, "bar, baz string"), loc(beforestring, re`() string`), loc(string, "string") + return 0, "1", "2" //@ loc(return, `return 0, "1", "2"`), loc(l0, "0"), loc(l1, `"1"`), loc(l2, `"2"`) +} + +// Assertions, expressed here to avoid clutter above. +// Note that when the cursor is over the field type, there is some +// (likely harmless) redundancy. + +//@ highlight(func, func, return) +//@ highlight(foo, foo, l0) +//@ highlight(int, fooint, int, l0) +//@ highlight(bar, bar, l1) +//@ highlight(beforebaz) +//@ highlight(baz, baz, l2) +//@ highlight(beforestring, baz, l2) +//@ highlight(string, barbazstring, string, l1, l2) +//@ highlight(l0, foo, l0) +//@ highlight(l1, bar, l1) +//@ highlight(l2, baz, l2) + +// Check that duplicate result names do not cause +// inaccurate highlighting. + +func _() (x, x int32) { //@ loc(x1, re`\((x)`), loc(x2, re`(x) int`), diag(x1, re"redeclared"), diag(x2, re"redeclared") + return 1, 2 //@ loc(one, "1"), loc(two, "2") +} + +//@ highlight(one, one, x1) +//@ highlight(two, two, x2) +//@ highlight(x1, x1, one) +//@ highlight(x2, x2, two) + +-- issue65516.go -- +package p + +// This test checks that gopls doesn't crash while highlighting +// functions with no body (golang/go#65516). + +func Foo() (int, string) //@highlight("int", "int"), highlight("func", "func") + +-- issue65952.go -- +package p + +// This test checks that gopls doesn't crash while highlighting +// return values in functions with no results. + +func _() { + return 0 //@highlight("0", "0"), diag("0", re"too many return") +} + +func _() () { + // TODO(golang/go#65966): fix the triplicate diagnostics here. + return 0 //@highlight("0", "0"), diag("0", re"too many return"), diag("0", re"too many return"), diag("0", re"too many return") +} diff --git a/gopls/internal/test/marker/testdata/highlight/issue60589.txt b/gopls/internal/test/marker/testdata/highlight/issue60589.txt deleted file mode 100644 index afa4335a9c6..00000000000 --- a/gopls/internal/test/marker/testdata/highlight/issue60589.txt +++ /dev/null @@ -1,30 +0,0 @@ -This test verifies that control flow lighlighting correctly accounts for -multi-name result parameters. In golang/go#60589, it did not. - --- go.mod -- -module mod.com - -go 1.18 - --- p.go -- -package p - -func _() (foo int, bar, baz string) { //@ loc(func, "func"), loc(foo, "foo"), loc(fooint, "foo int"), loc(int, "int"), loc(bar, "bar"), loc(beforebaz, " baz"), loc(baz, "baz"), loc(barbazstring, "bar, baz string"), loc(beforestring, re`() string`), loc(string, "string") - return 0, "1", "2" //@ loc(return, `return 0, "1", "2"`), loc(l0, "0"), loc(l1, `"1"`), loc(l2, `"2"`) -} - -// Assertions, expressed here to avoid clutter above. -// Note that when the cursor is over the field type, there is some -// (likely harmless) redundancy. - -//@ highlight(func, func, return) -//@ highlight(foo, foo, l0) -//@ highlight(int, fooint, int, l0) -//@ highlight(bar, bar, l1) -//@ highlight(beforebaz) -//@ highlight(baz, baz, l2) -//@ highlight(beforestring, baz, l2) -//@ highlight(string, barbazstring, string, l1, l2) -//@ highlight(l0, foo, l0) -//@ highlight(l1, bar, l1) -//@ highlight(l2, baz, l2) diff --git a/gopls/internal/test/marker/testdata/highlight/issue65516.txt b/gopls/internal/test/marker/testdata/highlight/issue65516.txt deleted file mode 100644 index 3fc3be27416..00000000000 --- a/gopls/internal/test/marker/testdata/highlight/issue65516.txt +++ /dev/null @@ -1,7 +0,0 @@ -This test checks that gopls doesn't crash while highlighting functions with no -body (golang/go#65516). - --- p.go -- -package p - -func Foo() (int, string) //@highlight("int", "int"), highlight("func", "func") From abe5874e805f7fe006ae6b906b3d08bbdedd23af Mon Sep 17 00:00:00 2001 From: Martin Asquino Date: Fri, 23 Feb 2024 18:08:13 +0000 Subject: [PATCH 39/47] gopls/internal/analysis: add fill switch cases code action This PR adds a code action to fill missing cases on type switches and switches on named types. Rules are defined here: https://github.com/golang/go/issues/65411#issuecomment-1922127508. Edit: I added some tests, but I'm sure there are still things to fix so sharing to get some feedback. Fixes https://github.com/golang/go/issues/65411 https://github.com/golang/tools/assets/4250565/1e67c404-e24f-478e-a3df-60a3adfaa9b1 Change-Id: Ie4ef0955d0e7ca130af8980a488b738c812aae4d GitHub-Last-Rev: a04dc69c7bb1ea23e396b8e701980cc425cdffe8 GitHub-Pull-Request: golang/tools#476 Reviewed-on: https://go-review.googlesource.com/c/tools/+/561416 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/analysis/fillswitch/doc.go | 66 ++++ .../analysis/fillswitch/fillswitch.go | 301 ++++++++++++++++++ .../analysis/fillswitch/fillswitch_test.go | 38 +++ .../analysis/fillswitch/testdata/src/a/a.go | 78 +++++ .../analysis/fillswitch/testdata/src/b/b.go | 21 ++ gopls/internal/golang/codeaction.go | 28 +- .../testdata/codeaction/fill_switch.txt | 105 ++++++ .../codeaction/fill_switch_resolve.txt | 116 +++++++ 8 files changed, 750 insertions(+), 3 deletions(-) create mode 100644 gopls/internal/analysis/fillswitch/doc.go create mode 100644 gopls/internal/analysis/fillswitch/fillswitch.go create mode 100644 gopls/internal/analysis/fillswitch/fillswitch_test.go create mode 100644 gopls/internal/analysis/fillswitch/testdata/src/a/a.go create mode 100644 gopls/internal/analysis/fillswitch/testdata/src/b/b.go create mode 100644 gopls/internal/test/marker/testdata/codeaction/fill_switch.txt create mode 100644 gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt diff --git a/gopls/internal/analysis/fillswitch/doc.go b/gopls/internal/analysis/fillswitch/doc.go new file mode 100644 index 00000000000..076c3a1323d --- /dev/null +++ b/gopls/internal/analysis/fillswitch/doc.go @@ -0,0 +1,66 @@ +// 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 fillswitch identifies switches with missing cases. +// +// It reports a diagnostic for each type switch or 'enum' switch that +// has missing cases, and suggests a fix to fill them in. +// +// The possible cases are: for a type switch, each accessible named +// type T or pointer *T that is assignable to the interface type; and +// for an 'enum' switch, each accessible named constant of the same +// type as the switch value. +// +// For an 'enum' switch, it will suggest cases for all possible values of the +// type. +// +// type Suit int8 +// const ( +// Spades Suit = iota +// Hearts +// Diamonds +// Clubs +// ) +// +// var s Suit +// switch s { +// case Spades: +// } +// +// It will report a diagnostic with a suggested fix to fill in the remaining +// cases: +// +// var s Suit +// switch s { +// case Spades: +// case Hearts: +// case Diamonds: +// case Clubs: +// default: +// panic(fmt.Sprintf("unexpected Suit: %v", s)) +// } +// +// For a type switch, it will suggest cases for all types that implement the +// interface. +// +// var stmt ast.Stmt +// switch stmt.(type) { +// case *ast.IfStmt: +// } +// +// It will report a diagnostic with a suggested fix to fill in the remaining +// cases: +// +// var stmt ast.Stmt +// switch stmt.(type) { +// case *ast.IfStmt: +// case *ast.ForStmt: +// case *ast.RangeStmt: +// case *ast.AssignStmt: +// case *ast.GoStmt: +// ... +// default: +// panic(fmt.Sprintf("unexpected ast.Stmt: %T", stmt)) +// } +package fillswitch diff --git a/gopls/internal/analysis/fillswitch/fillswitch.go b/gopls/internal/analysis/fillswitch/fillswitch.go new file mode 100644 index 00000000000..b93ade01065 --- /dev/null +++ b/gopls/internal/analysis/fillswitch/fillswitch.go @@ -0,0 +1,301 @@ +// 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 fillswitch + +import ( + "bytes" + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" +) + +// Diagnose computes diagnostics for switch statements with missing cases +// overlapping with the provided start and end position. +// +// If either start or end is invalid, the entire package is inspected. +func Diagnose(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { + var diags []analysis.Diagnostic + nodeFilter := []ast.Node{(*ast.SwitchStmt)(nil), (*ast.TypeSwitchStmt)(nil)} + inspect.Preorder(nodeFilter, func(n ast.Node) { + if start.IsValid() && n.End() < start || + end.IsValid() && n.Pos() > end { + return // non-overlapping + } + + var fix *analysis.SuggestedFix + switch n := n.(type) { + case *ast.SwitchStmt: + fix = suggestedFixSwitch(n, pkg, info) + case *ast.TypeSwitchStmt: + fix = suggestedFixTypeSwitch(n, pkg, info) + } + + if fix == nil { + return + } + + diags = append(diags, analysis.Diagnostic{ + Message: fix.Message, + Pos: n.Pos(), + End: n.Pos() + token.Pos(len("switch")), + SuggestedFixes: []analysis.SuggestedFix{*fix}, + }) + }) + + return diags +} + +func suggestedFixTypeSwitch(stmt *ast.TypeSwitchStmt, pkg *types.Package, info *types.Info) *analysis.SuggestedFix { + if hasDefaultCase(stmt.Body) { + return nil + } + + namedType := namedTypeFromTypeSwitch(stmt, info) + if namedType == nil { + return nil + } + + existingCases := caseTypes(stmt.Body, info) + // Gather accessible package-level concrete types + // that implement the switch interface type. + scope := namedType.Obj().Pkg().Scope() + var buf bytes.Buffer + for _, name := range scope.Names() { + obj := scope.Lookup(name) + if tname, ok := obj.(*types.TypeName); !ok || tname.IsAlias() { + continue // not a defined type + } + + if types.IsInterface(obj.Type()) { + continue + } + + samePkg := obj.Pkg() == pkg + if !samePkg && !obj.Exported() { + continue // inaccessible + } + + var key caseType + if types.AssignableTo(obj.Type(), namedType.Obj().Type()) { + key.named = obj.Type().(*types.Named) + } else if ptr := types.NewPointer(obj.Type()); types.AssignableTo(ptr, namedType.Obj().Type()) { + key.named = obj.Type().(*types.Named) + key.ptr = true + } + + if key.named != nil { + if existingCases[key] { + continue + } + + if buf.Len() > 0 { + buf.WriteString("\t") + } + + buf.WriteString("case ") + if key.ptr { + buf.WriteByte('*') + } + + if p := key.named.Obj().Pkg(); p != pkg { + // TODO: use the correct package name when the import is renamed + buf.WriteString(p.Name()) + buf.WriteByte('.') + } + buf.WriteString(key.named.Obj().Name()) + buf.WriteString(":\n") + } + } + + if buf.Len() == 0 { + return nil + } + + switch assign := stmt.Assign.(type) { + case *ast.AssignStmt: + addDefaultCase(&buf, namedType, assign.Lhs[0]) + case *ast.ExprStmt: + if assert, ok := assign.X.(*ast.TypeAssertExpr); ok { + addDefaultCase(&buf, namedType, assert.X) + } + } + + return &analysis.SuggestedFix{ + Message: fmt.Sprintf("Add cases for %s", namedType.Obj().Name()), + TextEdits: []analysis.TextEdit{{ + Pos: stmt.End() - token.Pos(len("}")), + End: stmt.End() - token.Pos(len("}")), + NewText: buf.Bytes(), + }}, + } +} + +func suggestedFixSwitch(stmt *ast.SwitchStmt, pkg *types.Package, info *types.Info) *analysis.SuggestedFix { + if hasDefaultCase(stmt.Body) { + return nil + } + + namedType, ok := info.TypeOf(stmt.Tag).(*types.Named) + if !ok { + return nil + } + + existingCases := caseConsts(stmt.Body, info) + // Gather accessible named constants of the same type as the switch value. + scope := namedType.Obj().Pkg().Scope() + var buf bytes.Buffer + for _, name := range scope.Names() { + obj := scope.Lookup(name) + if c, ok := obj.(*types.Const); ok && + (obj.Pkg() == pkg || obj.Exported()) && // accessible + types.Identical(obj.Type(), namedType.Obj().Type()) && + !existingCases[c] { + + if buf.Len() > 0 { + buf.WriteString("\t") + } + + buf.WriteString("case ") + if c.Pkg() != pkg { + buf.WriteString(c.Pkg().Name()) + buf.WriteByte('.') + } + buf.WriteString(c.Name()) + buf.WriteString(":\n") + } + } + + if buf.Len() == 0 { + return nil + } + + addDefaultCase(&buf, namedType, stmt.Tag) + + return &analysis.SuggestedFix{ + Message: fmt.Sprintf("Add cases for %s", namedType.Obj().Name()), + TextEdits: []analysis.TextEdit{{ + Pos: stmt.End() - token.Pos(len("}")), + End: stmt.End() - token.Pos(len("}")), + NewText: buf.Bytes(), + }}, + } +} + +func addDefaultCase(buf *bytes.Buffer, named *types.Named, expr ast.Expr) { + var dottedBuf bytes.Buffer + // writeDotted emits a dotted path a.b.c. + var writeDotted func(e ast.Expr) bool + writeDotted = func(e ast.Expr) bool { + switch e := e.(type) { + case *ast.SelectorExpr: + if !writeDotted(e.X) { + return false + } + dottedBuf.WriteByte('.') + dottedBuf.WriteString(e.Sel.Name) + return true + case *ast.Ident: + dottedBuf.WriteString(e.Name) + return true + } + return false + } + + buf.WriteString("\tdefault:\n") + typeName := fmt.Sprintf("%s.%s", named.Obj().Pkg().Name(), named.Obj().Name()) + if writeDotted(expr) { + // Switch tag expression is a dotted path. + // It is safe to re-evaluate it in the default case. + format := fmt.Sprintf("unexpected %s: %%#v", typeName) + fmt.Fprintf(buf, "\t\tpanic(fmt.Sprintf(%q, %s))\n\t", format, dottedBuf.String()) + } else { + // Emit simpler message, without re-evaluating tag expression. + fmt.Fprintf(buf, "\t\tpanic(%q)\n\t", "unexpected "+typeName) + } +} + +func namedTypeFromTypeSwitch(stmt *ast.TypeSwitchStmt, info *types.Info) *types.Named { + switch assign := stmt.Assign.(type) { + case *ast.ExprStmt: + if typ, ok := assign.X.(*ast.TypeAssertExpr); ok { + if named, ok := info.TypeOf(typ.X).(*types.Named); ok { + return named + } + } + + case *ast.AssignStmt: + if typ, ok := assign.Rhs[0].(*ast.TypeAssertExpr); ok { + if named, ok := info.TypeOf(typ.X).(*types.Named); ok { + return named + } + } + } + + return nil +} + +func hasDefaultCase(body *ast.BlockStmt) bool { + for _, clause := range body.List { + if len(clause.(*ast.CaseClause).List) == 0 { + return true + } + } + + return false +} + +func caseConsts(body *ast.BlockStmt, info *types.Info) map[*types.Const]bool { + out := map[*types.Const]bool{} + for _, stmt := range body.List { + for _, e := range stmt.(*ast.CaseClause).List { + if info.Types[e].Value == nil { + continue // not a constant + } + + if sel, ok := e.(*ast.SelectorExpr); ok { + e = sel.Sel // replace pkg.C with C + } + + if e, ok := e.(*ast.Ident); ok { + if c, ok := info.Uses[e].(*types.Const); ok { + out[c] = true + } + } + } + } + + return out +} + +type caseType struct { + named *types.Named + ptr bool +} + +func caseTypes(body *ast.BlockStmt, info *types.Info) map[caseType]bool { + out := map[caseType]bool{} + for _, stmt := range body.List { + for _, e := range stmt.(*ast.CaseClause).List { + if tv, ok := info.Types[e]; ok && tv.IsType() { + t := tv.Type + ptr := false + if p, ok := t.(*types.Pointer); ok { + t = p.Elem() + ptr = true + } + + if named, ok := t.(*types.Named); ok { + out[caseType{named, ptr}] = true + } + } + } + } + + return out +} diff --git a/gopls/internal/analysis/fillswitch/fillswitch_test.go b/gopls/internal/analysis/fillswitch/fillswitch_test.go new file mode 100644 index 00000000000..15d3ef1dd70 --- /dev/null +++ b/gopls/internal/analysis/fillswitch/fillswitch_test.go @@ -0,0 +1,38 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fillswitch_test + +import ( + "go/token" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/analysis/fillswitch" +) + +// analyzer allows us to test the fillswitch code action using the analysistest +// harness. +var analyzer = &analysis.Analyzer{ + Name: "fillswitch", + Doc: "test only", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: func(pass *analysis.Pass) (any, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + for _, d := range fillswitch.Diagnose(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { + pass.Report(d) + } + return nil, nil + }, + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillswitch", + RunDespiteErrors: true, +} + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, analyzer, "a") +} diff --git a/gopls/internal/analysis/fillswitch/testdata/src/a/a.go b/gopls/internal/analysis/fillswitch/testdata/src/a/a.go new file mode 100644 index 00000000000..06d01da5f1e --- /dev/null +++ b/gopls/internal/analysis/fillswitch/testdata/src/a/a.go @@ -0,0 +1,78 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fillswitch + +import ( + data "b" +) + +type typeA int + +const ( + typeAOne typeA = iota + typeATwo + typeAThree +) + +func doSwitch() { + var a typeA + switch a { // want `Add cases for typeA` + } + + switch a { // want `Add cases for typeA` + case typeAOne: + } + + switch a { + case typeAOne: + default: + } + + switch a { + case typeAOne: + case typeATwo: + case typeAThree: + } + + var b data.TypeB + switch b { // want `Add cases for TypeB` + case data.TypeBOne: + } +} + +type notification interface { + isNotification() +} + +type notificationOne struct{} + +func (notificationOne) isNotification() {} + +type notificationTwo struct{} + +func (notificationTwo) isNotification() {} + +func doTypeSwitch() { + var not notification + switch not.(type) { // want `Add cases for notification` + } + + switch not.(type) { // want `Add cases for notification` + case notificationOne: + } + + switch not.(type) { + case notificationOne: + case notificationTwo: + } + + switch not.(type) { + default: + } + + var t data.ExportedInterface + switch t { + } +} diff --git a/gopls/internal/analysis/fillswitch/testdata/src/b/b.go b/gopls/internal/analysis/fillswitch/testdata/src/b/b.go new file mode 100644 index 00000000000..f65f3a7e6f2 --- /dev/null +++ b/gopls/internal/analysis/fillswitch/testdata/src/b/b.go @@ -0,0 +1,21 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fillswitch + +type TypeB int + +const ( + TypeBOne TypeB = iota + TypeBTwo + TypeBThree +) + +type ExportedInterface interface { + isExportedInterface() +} + +type notExportedType struct{} + +func (notExportedType) isExportedInterface() {} diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index cab1b42f4b7..fa876ac474c 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -13,6 +13,7 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/analysis/fillstruct" + "golang.org/x/tools/gopls/internal/analysis/fillswitch" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" @@ -98,7 +99,7 @@ func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, return nil, err } if want[protocol.RefactorRewrite] { - rewrites, err := getRewriteCodeActions(pkg, pgf, fh, rng, snapshot.Options()) + rewrites, err := getRewriteCodeActions(ctx, pkg, snapshot, pgf, fh, rng, snapshot.Options()) if err != nil { return nil, err } @@ -252,8 +253,7 @@ func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Com return action } -// getRewriteCodeActions returns refactor.rewrite code actions available at the specified range. -func getRewriteCodeActions(pkg *cache.Package, pgf *parsego.File, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { +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) { // golang/go#61693: code actions were refactored to run outside of the // analysis framework, but as a result they lost their panic recovery. // @@ -354,6 +354,28 @@ func getRewriteCodeActions(pkg *cache.Package, pgf *parsego.File, fh file.Handle } } + for _, diag := range fillswitch.Diagnose(inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) { + edits, err := suggestedFixToEdits(ctx, snapshot, pkg.FileSet(), &diag.SuggestedFixes[0]) + if err != nil { + return nil, err + } + + changes := []protocol.DocumentChanges{} // must be a slice + for _, edit := range edits { + edit := edit + changes = append(changes, protocol.DocumentChanges{ + TextDocumentEdit: &edit, + }) + } + + actions = append(actions, protocol.CodeAction{ + Title: diag.Message, + Kind: protocol.RefactorRewrite, + Edit: &protocol.WorkspaceEdit{ + DocumentChanges: changes, + }, + }) + } for i := range commands { actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorRewrite, &commands[i], nil, options)) } diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt new file mode 100644 index 00000000000..2c1b19e130c --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt @@ -0,0 +1,105 @@ +This test checks the behavior of the 'fill switch' code action. +See fill_switch_resolve.txt for same test with resolve support. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/fillswitch + +go 1.18 + +-- data/data.go -- +package data + +type TypeB int + +const ( + TypeBOne TypeB = iota + TypeBTwo + TypeBThree +) + +-- a.go -- +package fillswitch + +import ( + "golang.org/lsptests/fillswitch/data" +) + +type typeA int + +const ( + typeAOne typeA = iota + typeATwo + typeAThree +) + +type notification interface { + isNotification() +} + +type notificationOne struct{} + +func (notificationOne) isNotification() {} + +type notificationTwo struct{} + +func (notificationTwo) isNotification() {} + +func doSwitch() { + var b data.TypeB + switch b { + case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) + } + + var a typeA + switch a { + case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) + } + + var n notification + switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) + } + + switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) + } + + var s struct { + a typeA + } + + switch s.a { + case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) + } +} +-- @a1/a.go -- +@@ -31 +31,4 @@ ++ case data.TypeBThree: ++ case data.TypeBTwo: ++ default: ++ panic(fmt.Sprintf("unexpected data.TypeB: %#v", b)) +-- @a2/a.go -- +@@ -36 +36,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", a)) +-- @a3/a.go -- +@@ -40 +40,4 @@ ++ case notificationOne: ++ case notificationTwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", n)) +-- @a4/a.go -- +@@ -43 +43,4 @@ ++ case notificationOne: ++ case notificationTwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", nt)) +-- @a5/a.go -- +@@ -51 +51,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", s.a)) diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt new file mode 100644 index 00000000000..504acd6043e --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt @@ -0,0 +1,116 @@ +This test checks the behavior of the 'fill switch' code action, with resolve support. +See fill_switch.txt for same test without resolve support. + +-- capabilities.json -- +{ + "textDocument": { + "codeAction": { + "dataSupport": true, + "resolveSupport": { + "properties": ["edit"] + } + } + } +} +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/fillswitch + +go 1.18 + +-- data/data.go -- +package data + +type TypeB int + +const ( + TypeBOne TypeB = iota + TypeBTwo + TypeBThree +) + +-- a.go -- +package fillswitch + +import ( + "golang.org/lsptests/fillswitch/data" +) + +type typeA int + +const ( + typeAOne typeA = iota + typeATwo + typeAThree +) + +type notification interface { + isNotification() +} + +type notificationOne struct{} + +func (notificationOne) isNotification() {} + +type notificationTwo struct{} + +func (notificationTwo) isNotification() {} + +func doSwitch() { + var b data.TypeB + switch b { + case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) + } + + var a typeA + switch a { + case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) + } + + var n notification + switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) + } + + switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) + } + + var s struct { + a typeA + } + + switch s.a { + case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) + } +} +-- @a1/a.go -- +@@ -31 +31,4 @@ ++ case data.TypeBThree: ++ case data.TypeBTwo: ++ default: ++ panic(fmt.Sprintf("unexpected data.TypeB: %#v", b)) +-- @a2/a.go -- +@@ -36 +36,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", a)) +-- @a3/a.go -- +@@ -40 +40,4 @@ ++ case notificationOne: ++ case notificationTwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", n)) +-- @a4/a.go -- +@@ -43 +43,4 @@ ++ case notificationOne: ++ case notificationTwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", nt)) +-- @a5/a.go -- +@@ -51 +51,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", s.a)) From 4d4e8028747180d2fc1cdcef0b7de569666c5984 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 26 Feb 2024 23:48:03 +0000 Subject: [PATCH 40/47] gopls/doc: address additional comments on workspace.md Address follow-up comments on CL 566936. Change-Id: I03160f4b58dc64fbde32bf4bfc0ce605573641d1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/567255 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Robert Findley --- gopls/doc/workspace.md | 123 ++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 57 deletions(-) diff --git a/gopls/doc/workspace.md b/gopls/doc/workspace.md index 59854b6c307..cb26b3dcd43 100644 --- a/gopls/doc/workspace.md +++ b/gopls/doc/workspace.md @@ -1,25 +1,38 @@ # Setting up your workspace -**In general, `gopls` should work when you open a Go file contained in your -workspace folder**. If it isn't working for you, or if you want to better -understand how gopls models your workspace, please read on. +In the language server protocol, a "workspace" consists of a folder along with +per-folder configuration. Some LSP clients such as VS Code allow configuring +workspaces explicitly, while others do so automatically by looking for special +files defining a workspace root (such as a `.git` directory or `go.mod` file). + +In order to function, gopls needs a defined scope in which language features +like references, rename, and implementation should operate. Put differently, +gopls needs to infer from the LSP workspace which `go build` invocations you +would use to build your workspace, including the working directory, +environment, and build flags. + +In the past, it could be tricky to set up your workspace so that gopls would +infer the correct build information. It required opening the correct directory +or using a `go.work` file to tell gopls about the modules you're working on, +and configuring the correct operating system and architecture in advance. +When this didn't work as expected, gopls would often fail in mysterious +ways--the dreaded "No packages found" error. + +Starting with gopls v0.15.0, workspace configuration is much simpler, and gopls +will typically work when you open a Go file anywhere in your workspace. If it +isn't working for you, or if you want to better understand how gopls models +your workspace, please read on. ## Workspace builds -`gopls` supports both Go module and GOPATH modes. However, it needs a defined -scope in which language features like references, rename, and implementation -should operate. Put differently, gopls needs to infer which `go build` -invocations you would use to build your workspace, including the working -directory, environment, and build flags. - -Starting with `gopls@v0.15.0`, gopls will try to guess the builds you are -working on based on the set of open files. When you open a file in a workspace -folder, gopls will check whether the file is contained in a module, `go.work` -workspace, or GOPATH directory, and configure the build accordingly. -Additionally, if you open a file that is constrained to a different operating -system or architecture, for example opening `foo_windows.go` when working on -Linux, gopls will create a scope with `GOOS` and `GOARCH` set to a value that -matches the file. +Starting with gopls v0.15.0, gopls will guess the builds you are working on +based on the set of open files. When you open a file in a workspace folder, +gopls checks whether the file is contained in a module, `go.work` workspace, or +GOPATH directory, and configures the build accordingly. Additionally, if you +open a file that is constrained to a different operating system or +architecture, for example opening `foo_windows.go` when working on Linux, gopls +will create a scope with `GOOS` and `GOARCH` set to a value that matches the +file. For example, suppose we had a repository with three modules: `moda`, `modb`, and `modc`, and a `go.work` file using modules `moda` and `modb`. If we open @@ -28,69 +41,65 @@ will automatically create three builds: ![Zero Config gopls](zeroconfig.png) -This allows `gopls` to _just work_ when you open a Go file, but it does come with +This allows gopls to _just work_ when you open a Go file, but it does come with several caveats: -- This causes gopls to do more work, since it is now tracking three builds +- It causes gopls to do more work, since it is now tracking three builds instead of one. However, the recent [scalability redesign](https://go.dev/blog/gopls-scalability) allows much of this work to be avoided through efficient caching. -- In some cases this may cause gopls to do more work, since gopls is now - tracking three builds instead of one. However, the recent - [scalability redesign](https://go.dev/blog/gopls-scalability) allows us - to avoid most of this work by efficient caching. -- For operations originating from a given file, including finding references - and implementations, gopls executes the operation in +- For operations invoked from a given file, such as "References" + or "Implementations", gopls executes the operation in _the default build for that file_. For example, finding references to a symbol `S` from `foo_linux.go` will return references from the Linux build, and finding references to the same symbol `S` from `foo_windows.go` will - return references from the Windows build. This is done for performance - reasons, as in the common case one build is sufficient, but may lead to - surprising results. Issues [#65757](https://go.dev/issue/65757) and + return references from the Windows build. Gopls searches the default build + for the file, but it doesn't search all the other possible builds (even + though that would be nice) because it is liable to be too expensive. + Issues [#65757](https://go.dev/issue/65757) and [#65755](https://go.dev/issue/65755) propose improvements to this behavior. - When selecting a `GOOS/GOARCH` combination to match a build-constrained file, - `gopls` will choose the first matching combination from + gopls will choose the first matching combination from [this list](https://cs.opensource.google/go/x/tools/+/master:gopls/internal/cache/port.go;l=30;drc=f872b3d6f05822d290bc7bdd29db090fd9d89f5c). In some cases, that may be surprising. - When working in a `GOOS/GOARCH` constrained file that does not match your - default toolchain, `CGO_ENABLED=0` is implicitly set. This means that `gopls` - will not work in files including `import "C"`. Issue + default toolchain, `CGO_ENABLED=0` is implicitly set, since a C toolchain for + that target is unlikely to be available. This means that gopls will not + work in files including `import "C"`. Issue [#65758](https://go.dev/issue/65758) may lead to improvements in this behavior. -- `gopls` is not able to guess build flags that include arbitrary user-defined - build constraints. For example, if you are trying to work on a file that is - constrained by the build directive `//go:build special`, gopls will not guess - that it needs to create a build with `"buildFlags": ["-tags=special"]`. Issue - [#65089](https://go.dev/issue/65089) proposes a heuristic by which gopls - could handle this automatically. +- Gopls is currently unable to guess build flags that include arbitrary + user-defined build constraints, such as a file with the build directive + `//go:build mytag`. Issue [#65089](https://go.dev/issue/65089) proposes + a heuristic by which gopls could handle this automatically. -We hope that you provide feedback on this behavior by upvoting or commenting -the issues mentioned above, or opening a [new issue](https://go.dev/issue/new) -for other improvements you'd like to see. +Please provide feedback on this behavior by upvoting or commenting the issues +mentioned above, or opening a [new issue](https://go.dev/issue/new) for other +improvements you'd like to see. ## When to use a `go.work` file for development -Starting with Go 1.18, the `go` command has native support for multi-module -workspaces, via [`go.work`](https://go.dev/ref/mod#workspaces) files. `gopls` -will recognize these files if they are present in your workspace. +Starting with Go 1.18, the `go` command has built-in support for multi-module +workspaces specified by [`go.work`](https://go.dev/ref/mod#workspaces) files. +Gopls will recognize these files if they are present in your workspace. Use a `go.work` file when: -- You want to work on multiple modules simultaneously in a single logical +- you want to work on multiple modules simultaneously in a single logical build, for example if you want changes to one module to be reflected in another. -- You want to improve `gopls'` memory usage or performance by reducing the number +- you want to improve gopls' memory usage or performance by reducing the number of builds it must track. -- You want `gopls` to know which modules you are working on in a multi-module - workspace, without opening any files. For example, if you want to use +- you want gopls to know which modules you are working on in a multi-module + workspace, without opening any files. For example, it may be convenient to use `workspace/symbol` queries before any files are open. -- You are using `gopls@v0.14.2` or earlier, and want to work on multiple +- you are using gopls v0.14.2 or earlier, and want to work on multiple modules. For example, suppose this repo is checked out into the `$WORK/tools` directory, and [`x/mod`](https://pkg.go.dev/golang.org/x/mod) is checked out into `$WORK/mod`, and you are working on a new `x/mod` API for editing `go.mod` -files that you want to simultaneously integrate into `gopls`. +files that you want to simultaneously integrate into gopls. You can work on both `golang.org/x/tools/gopls` and `golang.org/x/mod` simultaneously by creating a `go.work` file: @@ -101,16 +110,16 @@ go work init go work use tools/gopls mod ``` -...followed by opening the `$WORK` directory in your editor. +then opening the `$WORK` directory in your editor. ## When to manually configure `GOOS`, `GOARCH`, or `-tags` -As described in the first section, `gopls@v0.15.0` and later will try to +As described in the first section, gopls v0.15.0 and later will try to configure a new build scope automatically when you open a file that doesn't match the system default operating system (`GOOS`) or architecture (`GOARCH`). However, per the caveats listed in that section, this automatic behavior comes -with limitations. Customize your `gopls` environment by setting `GOOS` or +with limitations. Customize your gopls environment by setting `GOOS` or `GOARCH` in your [`"build.env"`](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#env-mapstringstring) or `-tags=...` in your" @@ -118,13 +127,13 @@ or `-tags=...` in your" when: - You want to modify the default build environment. -- `gopls` is not guessing the `GOOS/GOARCH` combination you want to use for +- Gopls is not guessing the `GOOS/GOARCH` combination you want to use for cross platform development. - You need to work on a file that is constrained by a user-defined build tags, - such as the build directive `//go:build special`. + such as the build directive `//go:build mytag`. ## GOPATH mode -When opening a directory within your `GOPATH`, the workspace scope will be just -that directory and all directories contained within it. Note that opening -a large GOPATH directory can make gopls very slow to start. +When opening a directory within a `GOPATH` directory, the workspace scope will +be just that directory and all directories contained within it. Note that +opening a large GOPATH directory can make gopls very slow to start. From 1f7dbdf01abbd56f7fb4ac2c61ea25ff673633af Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 27 Feb 2024 11:18:55 -0500 Subject: [PATCH 41/47] gopls/internal/cache: add debug assertions for bug report Updates golang/go#65960 Change-Id: I01a416a0cf9cf8e13195c0d9405008ded1a9c53a Reviewed-on: https://go-review.googlesource.com/c/tools/+/567416 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/cache/check.go | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index 7b700ea9a75..50596e130ad 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -1823,6 +1823,12 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, errs []types.Error, linkTarget // report. continue } + + // Invariant: both start and end are IsValid. + if !end.IsValid() { + panic("end is invalid") + } + posn := safetoken.StartPosition(e.Fset, start) if !posn.IsValid() { // All valid positions produced by the type checker should described by @@ -1848,10 +1854,34 @@ func typeErrorsToDiagnostics(pkg *syntaxPackage, errs []types.Error, linkTarget } continue } - if !end.IsValid() || end == start { + + // debugging #65960 + // + // At this point, we know 'start' IsValid, and + // StartPosition(start) worked (with e.Fset). + // + // If the asserted condition is true, 'start' + // is also in range for pgf.Tok, which means + // the PosRange failure must be caused by 'end'. + if pgf.Tok != e.Fset.File(start) { + bug.Reportf("internal error: inconsistent token.Files for pos") + } + + if end == start { // Expand the end position to a more meaningful span. end = analysisinternal.TypeErrorEndPos(e.Fset, pgf.Src, start) + + // debugging #65960 + if _, err := safetoken.Offset(pgf.Tok, end); err != nil { + bug.Reportf("TypeErrorEndPos returned invalid end: %v", err) + } + } else { + // debugging #65960 + if _, err := safetoken.Offset(pgf.Tok, end); err != nil { + bug.Reportf("ReadGo116ErrorData returned invalid end: %v", err) + } } + rng, err := pgf.Mapper.PosRange(pgf.Tok, start, end) if err != nil { bug.Reportf("internal error: could not compute pos to range for %v: %v", e, err) From a6c03c86fe37d3a870a08c8814c242a6a4105726 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 15 Feb 2024 12:29:41 -0500 Subject: [PATCH 42/47] x/tools: update telemetry import (new Start API) Change-Id: I78701b620029a94b47524f5d4a77c35a86df2e4e Reviewed-on: https://go-review.googlesource.com/c/tools/+/564337 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Commit-Queue: Alan Donovan Reviewed-by: Robert Findley --- cmd/deadcode/deadcode.go | 6 ++---- go.mod | 2 +- go.sum | 6 ++---- gopls/go.mod | 2 +- gopls/go.sum | 5 ++--- gopls/internal/telemetry/telemetry.go | 11 +++-------- gopls/internal/telemetry/telemetry_go118.go | 4 +--- gopls/internal/telemetry/telemetry_test.go | 14 +++++++++++++- gopls/main.go | 3 +-- 9 files changed, 26 insertions(+), 27 deletions(-) diff --git a/cmd/deadcode/deadcode.go b/cmd/deadcode/deadcode.go index 38a067e69f9..e0fce428d08 100644 --- a/cmd/deadcode/deadcode.go +++ b/cmd/deadcode/deadcode.go @@ -26,8 +26,7 @@ import ( "strings" "text/template" - "golang.org/x/telemetry/counter" - "golang.org/x/telemetry/crashmonitor" + "golang.org/x/telemetry" "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/rta" "golang.org/x/tools/go/packages" @@ -65,8 +64,7 @@ Flags: } func main() { - counter.Open() // Enable telemetry counter writing. - crashmonitor.Start() // Enable crash reporting watchdog. + telemetry.Start(telemetry.Config{ReportCrashes: true}) log.SetPrefix("deadcode: ") log.SetFlags(0) // no time prefix diff --git a/go.mod b/go.mod index 54b8bd473c5..c8e522063d4 100644 --- a/go.mod +++ b/go.mod @@ -12,5 +12,5 @@ require golang.org/x/sync v0.6.0 require ( golang.org/x/sys v0.17.0 // indirect - golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 + golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 ) diff --git a/go.sum b/go.sum index 373ed3cdd52..5e7611e6cca 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,5 @@ golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509 h1:Nr7eTQpQZ/ytesxDJpQgaf0t4sdLnnDtAbmtViTrSUo= -golang.org/x/telemetry v0.0.0-20240201224847-0a1d30dda509/go.mod h1:ZthVHHkOi8rlMEsfFr3Ie42Ym1NonbFNNRKW3ci0UrU= -golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808 h1:+Kc94D8UVEVxJnLXp/+FMfqQARZtWHfVrcRtcG8aT3g= -golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= diff --git a/gopls/go.mod b/gopls/go.mod index 6a6bec7c5f1..fdf5998d264 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -8,7 +8,7 @@ require ( github.com/jba/templatecheck v0.7.0 golang.org/x/mod v0.15.0 golang.org/x/sync v0.6.0 - golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78 + golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 golang.org/x/text v0.14.0 golang.org/x/tools v0.18.0 golang.org/x/vuln v1.0.1 diff --git a/gopls/go.sum b/gopls/go.sum index ca69d20cfc4..4a9335ea552 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -27,9 +27,8 @@ 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 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240208230135-b75ee8823808/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= -golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78 h1:vcVnuftN4J4UKLRcgetjzfU9FjjgXUUYUc3JhFplgV4= -golang.org/x/telemetry v0.0.0-20240209200032-7b892fcb8a78/go.mod h1:KG1lNk5ZFNssSZLrpVb4sMXKMpGwGXOxSG3rnu2gZQQ= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/gopls/internal/telemetry/telemetry.go b/gopls/internal/telemetry/telemetry.go index deab5d22eb1..58083992a17 100644 --- a/gopls/internal/telemetry/telemetry.go +++ b/gopls/internal/telemetry/telemetry.go @@ -16,14 +16,9 @@ import ( "golang.org/x/telemetry/upload" ) -// CounterOpen calls [counter.Open]. -func CounterOpen() { - counter.Open() -} - -// StartCrashMonitor calls [crashmonitor.Start]. -func StartCrashMonitor() { - crashmonitor.Start() +// Start starts telemetry, including the crash monitor. +func Start() { + telemetry.Start(telemetry.Config{ReportCrashes: true}) } // CrashMonitorSupported calls [crashmonitor.Supported]. diff --git a/gopls/internal/telemetry/telemetry_go118.go b/gopls/internal/telemetry/telemetry_go118.go index 12b7803f1a7..3fd1df2939c 100644 --- a/gopls/internal/telemetry/telemetry_go118.go +++ b/gopls/internal/telemetry/telemetry_go118.go @@ -12,9 +12,7 @@ package telemetry // gopls may not refer to the telemetry module directly, but must go // through this file. -func CounterOpen() {} - -func StartCrashMonitor() {} +func Start() {} func CrashMonitorSupported() bool { return false } diff --git a/gopls/internal/telemetry/telemetry_test.go b/gopls/internal/telemetry/telemetry_test.go index b52ac7093c7..1abd7ada03c 100644 --- a/gopls/internal/telemetry/telemetry_test.go +++ b/gopls/internal/telemetry/telemetry_test.go @@ -59,7 +59,7 @@ func TestTelemetry(t *testing.T) { for i, c := range sessionCounters { count, err := countertest.ReadCounter(c) if err != nil { - t.Fatalf("ReadCounter(%s): %v", c.Name(), err) + continue // counter db not open, or counter not found } initialCounts[i] = count } @@ -74,7 +74,19 @@ func TestTelemetry(t *testing.T) { goversion = strconv.Itoa(env.GoVersion()) addForwardedCounters(env, []string{"vscode/linter:a"}, []int64{1}) const desc = "got a bug" + + // This will increment a counter named something like: + // + // `gopls/bug + // golang.org/x/tools/gopls/internal/util/bug.report:+35 + // golang.org/x/tools/gopls/internal/util/bug.Report:=68 + // golang.org/x/tools/gopls/internal/telemetry_test.TestTelemetry.func2:+4 + // golang.org/x/tools/gopls/internal/test/integration.(*Runner).Run.func1:+87 + // testing.tRunner:+150 + // runtime.goexit:+0` + // bug.Report(desc) // want a stack counter with the trace starting from here. + env.Await(ShownMessage(desc)) }) diff --git a/gopls/main.go b/gopls/main.go index e3fb3861f68..31c5c5fdf1a 100644 --- a/gopls/main.go +++ b/gopls/main.go @@ -29,8 +29,7 @@ var version = "" // if set by the linker, overrides the gopls version func main() { versionpkg.VersionOverride = version - telemetry.CounterOpen() - telemetry.StartCrashMonitor() + telemetry.Start() ctx := context.Background() tool.Main(ctx, cmd.New(hooks.Options), os.Args[1:]) } From 38b0e9bfdba8d4762d50481319449a59e0a8adb6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 14 Feb 2024 14:15:22 -0500 Subject: [PATCH 43/47] x/tools: add explicit Unalias operations This change is the result of an audit of all type assertions and type switches whose operand is a types.Type. (These were enumerated with an analyzer tool.) If the operand is already the result of a call to Underlying or Unalias, there is nothing to do, but in other cases, explicit Unalias operations were added in order to preserve the existing behavior when go/types starts creating explicit Alias types. This change does not address any desired behavior changes required for the ideal handling of aliases; they will wait for a followup. In a number of places I have added comments matching "TODO.*alias". It may be prudent to split this change by top-level directory, both for ease of review, and of later bisection if needed. During the audit, there appeared to be a recurring need for the following operators: - (*types.Func).Signature (golang/go#65772); - Deref(Type): it's easy to forget to strip off the Alias constructor; - ReceiverName (CL 565075), for destructuring receiver types such as T and *T, in which up to two Aliases might be present. Updates golang/go#65294 Change-Id: I5180b9bae1c9191807026b8e0dc6f15ed4953b9a Reviewed-on: https://go-review.googlesource.com/c/tools/+/565035 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- cmd/godex/print.go | 4 +++- cmd/godex/writetype.go | 10 +++++++++- cmd/guru/describe.go | 15 ++++++++------- cmd/guru/implements.go | 15 ++++++++------- go/analysis/passes/composite/composite.go | 18 +++++++++++------- go/analysis/passes/copylock/copylock.go | 3 ++- .../passes/deepequalerrors/deepequalerrors.go | 3 +-- .../passes/httpresponse/httpresponse.go | 3 ++- .../passes/ifaceassert/parameterized.go | 5 +++++ .../passes/internal/analysisutil/util.go | 3 ++- go/analysis/passes/printf/printf.go | 7 +++++-- go/analysis/passes/printf/types.go | 5 +++-- go/analysis/passes/shift/shift.go | 3 ++- go/analysis/passes/stringintconv/string.go | 6 ++++-- .../testinggoroutine/testinggoroutine.go | 3 ++- go/analysis/passes/tests/tests.go | 2 ++ go/analysis/passes/timeformat/timeformat.go | 2 +- go/analysis/passes/unsafeptr/unsafeptr.go | 3 ++- go/analysis/passes/unusedwrite/unusedwrite.go | 15 +++++---------- go/callgraph/rta/rta.go | 4 ++-- go/callgraph/vta/graph.go | 15 ++++++++------- go/callgraph/vta/graph_test.go | 3 ++- go/callgraph/vta/utils.go | 4 +++- go/internal/gccgoimporter/parser.go | 10 ++++++---- go/ssa/builder.go | 15 +++++++-------- go/ssa/builder_test.go | 3 ++- go/ssa/const.go | 5 +++-- go/ssa/coretype.go | 3 ++- go/ssa/emit.go | 7 ++++--- go/ssa/interp/ops.go | 3 ++- go/ssa/interp/reflect.go | 9 +++++---- go/ssa/interp/value.go | 3 ++- go/ssa/methods.go | 4 ++++ go/ssa/parameterized.go | 4 ++++ go/ssa/sanity.go | 2 +- go/ssa/subst.go | 7 ++++++- go/ssa/util.go | 16 +++++++++++----- go/types/internal/play/play.go | 3 ++- go/types/objectpath/objectpath.go | 10 ++++++++-- go/types/typeutil/ui.go | 8 ++++++-- internal/analysisinternal/analysis.go | 7 ++++++- internal/facts/facts_test.go | 3 ++- internal/facts/imports.go | 4 ++++ internal/gcimporter/bexport_test.go | 3 +++ internal/gcimporter/gcimporter_test.go | 9 +++++---- internal/gcimporter/iexport.go | 9 ++++++--- internal/gcimporter/iimport.go | 11 ++++++----- internal/gcimporter/ureader_yes.go | 3 ++- internal/typeparams/common.go | 8 ++++++-- internal/typeparams/coretype.go | 4 +++- .../typeparams/genericfeatures/features.go | 3 ++- refactor/rename/check.go | 10 ++++++---- refactor/rename/spec.go | 5 +++-- 53 files changed, 221 insertions(+), 121 deletions(-) diff --git a/cmd/godex/print.go b/cmd/godex/print.go index 1bb5214edfd..da3b2f04e0b 100644 --- a/cmd/godex/print.go +++ b/cmd/godex/print.go @@ -12,6 +12,8 @@ import ( "go/types" "io" "math/big" + + "golang.org/x/tools/internal/aliases" ) // TODO(gri) use tabwriter for alignment? @@ -56,7 +58,7 @@ func (p *printer) printf(format string, args ...interface{}) { // denoted by obj is not an interface and has methods. Otherwise it returns // the zero value. func methodsFor(obj *types.TypeName) (*types.Named, []*types.Selection) { - named, _ := obj.Type().(*types.Named) + named, _ := aliases.Unalias(obj.Type()).(*types.Named) if named == nil { // A type name's type can also be the // exported basic type unsafe.Pointer. diff --git a/cmd/godex/writetype.go b/cmd/godex/writetype.go index 5cbe1b12c84..6ae365d13a3 100644 --- a/cmd/godex/writetype.go +++ b/cmd/godex/writetype.go @@ -12,7 +12,11 @@ package main -import "go/types" +import ( + "go/types" + + "golang.org/x/tools/internal/aliases" +) func (p *printer) writeType(this *types.Package, typ types.Type) { p.writeTypeInternal(this, typ, make([]types.Type, 8)) @@ -173,6 +177,10 @@ func (p *printer) writeTypeInternal(this *types.Package, typ types.Type, visited p.print(")") } + case *aliases.Alias: + // TODO(adonovan): display something aliasy. + p.writeTypeInternal(this, aliases.Unalias(t), visited) + case *types.Named: s := "" if obj := t.Obj(); obj != nil { diff --git a/cmd/guru/describe.go b/cmd/guru/describe.go index 273d5e2d73c..e85bc385feb 100644 --- a/cmd/guru/describe.go +++ b/cmd/guru/describe.go @@ -19,6 +19,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) @@ -358,7 +359,7 @@ func appendNames(names []*types.Named, typ types.Type) []*types.Named { Elem() types.Type } - switch t := typ.(type) { + switch t := aliases.Unalias(typ).(type) { case *types.Named: names = append(names, t) case *types.Map: @@ -469,7 +470,7 @@ func describeType(qpos *queryPos, path []ast.Node) (*describeTypeResult, error) description = "alias of " } else if obj.Pos() == n.Pos() { description = "definition of " // (Named type) - } else if _, ok := typ.(*types.Basic); ok { + } else if _, ok := aliases.Unalias(typ).(*types.Basic); ok { description = "reference to built-in " } else { description = "reference to " // (Named type) @@ -486,7 +487,7 @@ func describeType(qpos *queryPos, path []ast.Node) (*describeTypeResult, error) description = description + "type " + qpos.typeString(typ) // Show sizes for structs and named types (it's fairly obvious for others). - switch typ.(type) { + switch aliases.Unalias(typ).(type) { case *types.Named, *types.Struct: szs := types.StdSizes{WordSize: 8, MaxAlign: 8} // assume amd64 description = fmt.Sprintf("%s (size %d, align %d)", description, @@ -576,7 +577,7 @@ func (r *describeTypeResult) PrintPlain(printf printfFunc) { printf(r.node, "%s", r.description) // Show the underlying type for a reference to a named type. - if nt, ok := r.typ.(*types.Named); ok && r.node.Pos() != nt.Obj().Pos() { + if nt, ok := aliases.Unalias(r.typ).(*types.Named); ok && r.node.Pos() != nt.Obj().Pos() { // TODO(adonovan): improve display of complex struct/interface types. printf(nt.Obj(), "defined as %s", r.qpos.typeString(nt.Underlying())) } @@ -585,7 +586,7 @@ func (r *describeTypeResult) PrintPlain(printf printfFunc) { if len(r.methods) == 0 { // Only report null result for type kinds // capable of bearing methods. - switch r.typ.(type) { + switch aliases.Unalias(r.typ).(type) { case *types.Interface, *types.Struct, *types.Named: printf(r.node, "No methods.") } @@ -596,7 +597,7 @@ func (r *describeTypeResult) PrintPlain(printf printfFunc) { func (r *describeTypeResult) JSON(fset *token.FileSet) []byte { var namePos, nameDef string - if nt, ok := r.typ.(*types.Named); ok { + if nt, ok := aliases.Unalias(r.typ).(*types.Named); ok { namePos = fset.Position(nt.Obj().Pos()).String() nameDef = nt.Underlying().String() } @@ -727,7 +728,7 @@ func formatMember(obj types.Object, maxname int) string { } var typestr string // Abbreviate long aggregate type names. - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.Interface: if typ.NumMethods() > 1 { typestr = "interface{...}" diff --git a/cmd/guru/implements.go b/cmd/guru/implements.go index 9e4d0dba6ee..48ab19116ae 100644 --- a/cmd/guru/implements.go +++ b/cmd/guru/implements.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/cmd/guru/serial" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/refactor/importgraph" ) @@ -157,7 +158,7 @@ func implements(q *Query) error { } var pos interface{} = qpos - if nt, ok := deref(T).(*types.Named); ok { + if nt, ok := aliases.Unalias(deref(T)).(*types.Named); ok { pos = nt.Obj() } @@ -230,7 +231,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { for i, sub := range r.to { if !isInterface(sub) { if r.method == nil { - printf(deref(sub).(*types.Named).Obj(), "\t%s %s type %s", + printf(aliases.Unalias(deref(sub)).(*types.Named).Obj(), "\t%s %s type %s", relation, typeKind(sub), r.qpos.typeString(sub)) } else { meth(r.toMethod[i]) @@ -240,7 +241,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { for i, sub := range r.to { if isInterface(sub) { if r.method == nil { - printf(sub.(*types.Named).Obj(), "\t%s %s type %s", + printf(aliases.Unalias(sub).(*types.Named).Obj(), "\t%s %s type %s", relation, typeKind(sub), r.qpos.typeString(sub)) } else { meth(r.toMethod[i]) @@ -251,7 +252,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { relation = "implements" for i, super := range r.from { if r.method == nil { - printf(super.(*types.Named).Obj(), "\t%s %s", + printf(aliases.Unalias(super).(*types.Named).Obj(), "\t%s %s", relation, r.qpos.typeString(super)) } else { meth(r.fromMethod[i]) @@ -270,7 +271,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { } for i, super := range r.from { if r.method == nil { - printf(super.(*types.Named).Obj(), "\t%s %s", + printf(aliases.Unalias(super).(*types.Named).Obj(), "\t%s %s", relation, r.qpos.typeString(super)) } else { meth(r.fromMethod[i]) @@ -288,7 +289,7 @@ func (r *implementsResult) PrintPlain(printf printfFunc) { for i, psuper := range r.fromPtr { if r.method == nil { - printf(psuper.(*types.Named).Obj(), "\t%s %s", + printf(aliases.Unalias(psuper).(*types.Named).Obj(), "\t%s %s", relation, r.qpos.typeString(psuper)) } else { meth(r.fromPtrMethod[i]) @@ -332,7 +333,7 @@ func makeImplementsTypes(tt []types.Type, fset *token.FileSet) []serial.Implemen func makeImplementsType(T types.Type, fset *token.FileSet) serial.ImplementsType { var pos token.Pos - if nt, ok := deref(T).(*types.Named); ok { // implementsResult.t may be non-named + if nt, ok := aliases.Unalias(deref(T)).(*types.Named); ok { // implementsResult.t may be non-named pos = nt.Obj().Pos() } return serial.ImplementsType{ diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go index 847063bb326..6b126f897d8 100644 --- a/go/analysis/passes/composite/composite.go +++ b/go/analysis/passes/composite/composite.go @@ -15,6 +15,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/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -71,7 +72,7 @@ func run(pass *analysis.Pass) (interface{}, error) { return } var structuralTypes []types.Type - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.TypeParam: terms, err := typeparams.StructuralTerms(typ) if err != nil { @@ -84,7 +85,8 @@ func run(pass *analysis.Pass) (interface{}, error) { structuralTypes = append(structuralTypes, typ) } for _, typ := range structuralTypes { - under := deref(typ.Underlying()) + // TODO(adonovan): this operation is questionable. + under := aliases.Unalias(deref(typ.Underlying())) strct, ok := under.(*types.Struct) if !ok { // skip non-struct composite literals @@ -142,9 +144,11 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } +// Note: this is not the usual deref operator! +// It strips off all Pointer constructors (and their Aliases). func deref(typ types.Type) types.Type { for { - ptr, ok := typ.(*types.Pointer) + ptr, ok := aliases.Unalias(typ).(*types.Pointer) if !ok { break } @@ -153,18 +157,18 @@ func deref(typ types.Type) types.Type { return typ } +// isLocalType reports whether typ belongs to the same package as pass. +// TODO(adonovan): local means "internal to a function"; rename to isSamePackageType. func isLocalType(pass *analysis.Pass, typ types.Type) bool { - switch x := typ.(type) { + switch x := aliases.Unalias(typ).(type) { case *types.Struct: // struct literals are local types return true case *types.Pointer: return isLocalType(pass, x.Elem()) - case *types.Named: + case interface{ Obj() *types.TypeName }: // *Named or *TypeParam (aliases were removed already) // names in package foo are local to foo_test too return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test") - case *types.TypeParam: - return strings.TrimSuffix(x.Obj().Pkg().Path(), "_test") == strings.TrimSuffix(pass.Pkg.Path(), "_test") } return false } diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go index 6cbbc7e8140..8f39159c0f0 100644 --- a/go/analysis/passes/copylock/copylock.go +++ b/go/analysis/passes/copylock/copylock.go @@ -18,6 +18,7 @@ import ( "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" ) @@ -255,7 +256,7 @@ func lockPath(tpkg *types.Package, typ types.Type, seen map[types.Type]bool) typ } seen[typ] = true - if tpar, ok := typ.(*types.TypeParam); ok { + if tpar, ok := aliases.Unalias(typ).(*types.TypeParam); ok { terms, err := typeparams.StructuralTerms(tpar) if err != nil { return nil // invalid type diff --git a/go/analysis/passes/deepequalerrors/deepequalerrors.go b/go/analysis/passes/deepequalerrors/deepequalerrors.go index 5e17bd1ab90..95cd9a061eb 100644 --- a/go/analysis/passes/deepequalerrors/deepequalerrors.go +++ b/go/analysis/passes/deepequalerrors/deepequalerrors.go @@ -102,8 +102,7 @@ func containsError(typ types.Type) bool { return true } } - case *types.Named, - *aliases.Alias: + case *types.Named, *aliases.Alias: return check(t.Underlying()) // We list the remaining valid type kinds for completeness. diff --git a/go/analysis/passes/httpresponse/httpresponse.go b/go/analysis/passes/httpresponse/httpresponse.go index 8cb046e16da..047ae07cca1 100644 --- a/go/analysis/passes/httpresponse/httpresponse.go +++ b/go/analysis/passes/httpresponse/httpresponse.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) @@ -136,7 +137,7 @@ func isHTTPFuncOrMethodOnClient(info *types.Info, expr *ast.CallExpr) bool { if analysisutil.IsNamedType(typ, "net/http", "Client") { return true // method on http.Client. } - ptr, ok := typ.(*types.Pointer) + ptr, ok := aliases.Unalias(typ).(*types.Pointer) return ok && analysisutil.IsNamedType(ptr.Elem(), "net/http", "Client") // method on *http.Client. } diff --git a/go/analysis/passes/ifaceassert/parameterized.go b/go/analysis/passes/ifaceassert/parameterized.go index 12507f9967f..a077d440246 100644 --- a/go/analysis/passes/ifaceassert/parameterized.go +++ b/go/analysis/passes/ifaceassert/parameterized.go @@ -7,6 +7,7 @@ package ifaceassert import ( "go/types" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -94,6 +95,10 @@ func (w *tpWalker) isParameterized(typ types.Type) (res bool) { case *types.Chan: return w.isParameterized(t.Elem()) + case *aliases.Alias: + // TODO(adonovan): think about generic aliases. + return w.isParameterized(aliases.Unalias(t)) + case *types.Named: list := t.TypeArgs() for i, n := 0, list.Len(); i < n; i++ { diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go index 3f01b3b55dc..89291602a5b 100644 --- a/go/analysis/passes/internal/analysisutil/util.go +++ b/go/analysis/passes/internal/analysisutil/util.go @@ -14,6 +14,7 @@ import ( "go/types" "os" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/analysisinternal" ) @@ -115,7 +116,7 @@ func Imports(pkg *types.Package, path string) bool { // This function avoids allocating the concatenation of "pkg.Name", // which is important for the performance of syntax matching. func IsNamedType(t types.Type, pkgPath string, names ...string) bool { - n, ok := t.(*types.Named) + n, ok := aliases.Unalias(t).(*types.Named) if !ok { return false } diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 070654f0124..32350192583 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -24,6 +24,7 @@ import ( "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -959,6 +960,8 @@ func isStringer(sig *types.Signature) bool { // It is almost always a mistake to print a function value. func isFunctionValue(pass *analysis.Pass, e ast.Expr) bool { if typ := pass.TypesInfo.Types[e].Type; typ != nil { + // Don't call Underlying: a named func type with a String method is ok. + // TODO(adonovan): it would be more precise to check isStringer. _, ok := typ.(*types.Signature) return ok } @@ -1010,7 +1013,7 @@ func checkPrint(pass *analysis.Pass, call *ast.CallExpr, fn *types.Func) { // Skip checking functions with unknown type. return } - if sig, ok := typ.(*types.Signature); ok { + if sig, ok := typ.Underlying().(*types.Signature); ok { if !sig.Variadic() { // Skip checking non-variadic functions. return @@ -1020,7 +1023,7 @@ func checkPrint(pass *analysis.Pass, call *ast.CallExpr, fn *types.Func) { typ := params.At(firstArg).Type() typ = typ.(*types.Slice).Elem() - it, ok := typ.(*types.Interface) + it, ok := aliases.Unalias(typ).(*types.Interface) if !ok || !it.Empty() { // Skip variadic functions accepting non-interface{} args. return diff --git a/go/analysis/passes/printf/types.go b/go/analysis/passes/printf/types.go index ab98e569980..017c8a247ec 100644 --- a/go/analysis/passes/printf/types.go +++ b/go/analysis/passes/printf/types.go @@ -10,6 +10,7 @@ import ( "go/types" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -72,7 +73,7 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool { return true } - if typ, _ := typ.(*types.TypeParam); typ != nil { + if typ, _ := aliases.Unalias(typ).(*types.TypeParam); typ != nil { // Avoid infinite recursion through type parameters. if m.seen[typ] { return true @@ -275,7 +276,7 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool { } func isConvertibleToString(typ types.Type) bool { - if bt, ok := typ.(*types.Basic); ok && bt.Kind() == types.UntypedNil { + if bt, ok := aliases.Unalias(typ).(*types.Basic); ok && bt.Kind() == types.UntypedNil { // We explicitly don't want untyped nil, which is // convertible to both of the interfaces below, as it // would just panic anyway. diff --git a/go/analysis/passes/shift/shift.go b/go/analysis/passes/shift/shift.go index 61f16501bb3..d01eb1eebe5 100644 --- a/go/analysis/passes/shift/shift.go +++ b/go/analysis/passes/shift/shift.go @@ -21,6 +21,7 @@ import ( "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -99,7 +100,7 @@ func checkLongShift(pass *analysis.Pass, node ast.Node, x, y ast.Expr) { return } var structuralTypes []types.Type - switch t := t.(type) { + switch t := aliases.Unalias(t).(type) { case *types.TypeParam: terms, err := typeparams.StructuralTerms(t) if err != nil { diff --git a/go/analysis/passes/stringintconv/string.go b/go/analysis/passes/stringintconv/string.go index 005e2e54b7d..16a4b3e5516 100644 --- a/go/analysis/passes/stringintconv/string.go +++ b/go/analysis/passes/stringintconv/string.go @@ -60,10 +60,12 @@ func describe(typ, inType types.Type, inName string) string { } func typeName(typ types.Type) string { - if v, _ := typ.(interface{ Name() string }); v != nil { + typ = aliases.Unalias(typ) + // TODO(adonovan): don't discard alias type, return its name. + if v, _ := typ.(*types.Basic); v != nil { return v.Name() } - if v, _ := typ.(interface{ Obj() *types.TypeName }); v != nil { + if v, _ := typ.(interface{ Obj() *types.TypeName }); v != nil { // Named, TypeParam return v.Obj().Name() } return "" diff --git a/go/analysis/passes/testinggoroutine/testinggoroutine.go b/go/analysis/passes/testinggoroutine/testinggoroutine.go index dc5307a15d0..828f95bc862 100644 --- a/go/analysis/passes/testinggoroutine/testinggoroutine.go +++ b/go/analysis/passes/testinggoroutine/testinggoroutine.go @@ -17,6 +17,7 @@ import ( "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" ) //go:embed doc.go @@ -270,7 +271,7 @@ func forbiddenMethod(info *types.Info, call *ast.CallExpr) (*types.Var, *types.S func formatMethod(sel *types.Selection, fn *types.Func) string { var ptr string rtype := sel.Recv() - if p, ok := rtype.(*types.Pointer); ok { + if p, ok := aliases.Unalias(rtype).(*types.Pointer); ok { ptr = "*" rtype = p.Elem() } diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go index 6db12f3cb9a..39d0d9e429e 100644 --- a/go/analysis/passes/tests/tests.go +++ b/go/analysis/passes/tests/tests.go @@ -252,6 +252,8 @@ func validateFuzzArgs(pass *analysis.Pass, params *types.Tuple, expr ast.Expr) b } func isTestingType(typ types.Type, testingType string) bool { + // No Unalias here: I doubt "go test" recognizes + // "type A = *testing.T; func Test(A) {}" as a test. ptr, ok := typ.(*types.Pointer) if !ok { return false diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go index eb84502bd99..4a6c6b8bc6c 100644 --- a/go/analysis/passes/timeformat/timeformat.go +++ b/go/analysis/passes/timeformat/timeformat.go @@ -107,7 +107,7 @@ func badFormatAt(info *types.Info, e ast.Expr) int { return -1 } - t, ok := tv.Type.(*types.Basic) + t, ok := tv.Type.(*types.Basic) // sic, no unalias if !ok || t.Info()&types.IsString == 0 { return -1 } diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go index 32e71ef979d..14e4a6c1e4b 100644 --- a/go/analysis/passes/unsafeptr/unsafeptr.go +++ b/go/analysis/passes/unsafeptr/unsafeptr.go @@ -17,6 +17,7 @@ import ( "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" ) //go:embed doc.go @@ -88,7 +89,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // by the time we get to the conversion at the end. // For now approximate by saying that *Header is okay // but Header is not. - pt, ok := info.Types[x.X].Type.(*types.Pointer) + pt, ok := aliases.Unalias(info.Types[x.X].Type).(*types.Pointer) if ok && isReflectHeader(pt.Elem()) { return true } diff --git a/go/analysis/passes/unusedwrite/unusedwrite.go b/go/analysis/passes/unusedwrite/unusedwrite.go index cc484620dcc..a01cbb8f83a 100644 --- a/go/analysis/passes/unusedwrite/unusedwrite.go +++ b/go/analysis/passes/unusedwrite/unusedwrite.go @@ -125,10 +125,7 @@ func isDeadStore(store *ssa.Store, obj ssa.Value, addr ssa.Instruction) bool { // isStructOrArray returns whether the underlying type is struct or array. func isStructOrArray(tp types.Type) bool { - if named, ok := tp.(*types.Named); ok { - tp = named.Underlying() - } - switch tp.(type) { + switch tp.Underlying().(type) { case *types.Array: return true case *types.Struct: @@ -146,7 +143,7 @@ func hasStructOrArrayType(v ssa.Value) bool { // func (t T) f() { ...} // the receiver object is of type *T: // t0 = local T (t) *T - if tp, ok := alloc.Type().(*types.Pointer); ok { + if tp, ok := aliases.Unalias(alloc.Type()).(*types.Pointer); ok { return isStructOrArray(tp.Elem()) } return false @@ -162,14 +159,12 @@ func hasStructOrArrayType(v ssa.Value) bool { func getFieldName(tp types.Type, index int) string { // TODO(adonovan): use // stp, ok := typeparams.Deref(tp).Underlying().(*types.Struct); ok { - // when Deref is defined. + // when Deref is defined. But see CL 565456 for a better fix. + if pt, ok := aliases.Unalias(tp).(*types.Pointer); ok { tp = pt.Elem() } - if named, ok := aliases.Unalias(tp).(*types.Named); ok { - tp = named.Underlying() - } - if stp, ok := tp.(*types.Struct); ok { + if stp, ok := tp.Underlying().(*types.Struct); ok { return stp.Field(index).Name() } return fmt.Sprintf("%d", index) diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go index 72b383dabe7..3c8dc41cd38 100644 --- a/go/callgraph/rta/rta.go +++ b/go/callgraph/rta/rta.go @@ -375,7 +375,7 @@ func (r *rta) interfaces(C types.Type) []*types.Interface { // and update the 'implements' relation. r.interfaceTypes.Iterate(func(I types.Type, v interface{}) { iinfo := v.(*interfaceTypeInfo) - if I := I.(*types.Interface); implements(cinfo, iinfo) { + if I := aliases.Unalias(I).(*types.Interface); implements(cinfo, iinfo) { iinfo.implementations = append(iinfo.implementations, C) cinfo.implements = append(cinfo.implements, I) } @@ -457,7 +457,7 @@ func (r *rta) addRuntimeType(T types.Type, skip bool) { // Each package maintains its own set of types it has visited. var n *types.Named - switch T := T.(type) { + switch T := aliases.Unalias(T).(type) { case *types.Named: n = T case *types.Pointer: diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 4b5a65b3336..0abeab01bb1 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -12,6 +12,7 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -410,7 +411,7 @@ func (b *builder) tassert(a *ssa.TypeAssert) { // The case where a is register so there // is a flow from a.X to a[0]. Here, a[0] is represented as an // indexedLocal: an entry into local tuple register a at index 0. - tup := a.Type().Underlying().(*types.Tuple) + tup := a.Type().(*types.Tuple) t := tup.At(0).Type() local := indexedLocal{val: a, typ: t, index: 0} @@ -421,7 +422,7 @@ func (b *builder) tassert(a *ssa.TypeAssert) { // and t1 where the source is indexed local representing a value // from tuple register t2 at index i and the target is t1. func (b *builder) extract(e *ssa.Extract) { - tup := e.Tuple.Type().Underlying().(*types.Tuple) + tup := e.Tuple.Type().(*types.Tuple) t := tup.At(e.Index).Type() local := indexedLocal{val: e.Tuple, typ: t, index: e.Index} @@ -527,7 +528,7 @@ func (b *builder) next(n *ssa.Next) { if n.IsString { return } - tup := n.Type().Underlying().(*types.Tuple) + tup := n.Type().(*types.Tuple) kt := tup.At(1).Type() vt := tup.At(2).Type() @@ -657,7 +658,7 @@ func addReturnFlows(b *builder, r *ssa.Return, site ssa.Value) { return } - tup := site.Type().Underlying().(*types.Tuple) + tup := site.Type().(*types.Tuple) for i, r := range results { local := indexedLocal{val: site, typ: tup.At(i).Type(), index: i} b.addInFlowEdge(b.nodeFromVal(r), local) @@ -671,7 +672,7 @@ func (b *builder) multiconvert(c *ssa.MultiConvert) { // This is a adaptation of x/exp/typeparams.NormalTerms which x/tools cannot depend on. var terms []*types.Term var err error - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.TypeParam: terms, err = typeparams.StructuralTerms(typ) case *types.Union: @@ -692,7 +693,7 @@ func (b *builder) multiconvert(c *ssa.MultiConvert) { } // isValuePreserving returns true if a conversion from ut_src to // ut_dst is value-preserving, i.e. just a change of type. - // Precondition: neither argument is a named type. + // Precondition: neither argument is a named or alias type. isValuePreserving := func(ut_src, ut_dst types.Type) bool { // Identical underlying types? if types.IdenticalIgnoreTags(ut_dst, ut_src) { @@ -740,7 +741,7 @@ func (b *builder) addInFlowEdge(s, d node) { // Creates const, pointer, global, func, and local nodes based on register instructions. func (b *builder) nodeFromVal(val ssa.Value) node { - if p, ok := val.Type().(*types.Pointer); ok && !types.IsInterface(p.Elem()) && !isFunction(p.Elem()) { + if p, ok := aliases.Unalias(val.Type()).(*types.Pointer); ok && !types.IsInterface(p.Elem()) && !isFunction(p.Elem()) { // Nested pointer to interfaces are modeled as a special // nestedPtrInterface node. if i := interfaceUnderPtr(p.Elem()); i != nil { diff --git a/go/callgraph/vta/graph_test.go b/go/callgraph/vta/graph_test.go index da574d71b53..060f67f7ae6 100644 --- a/go/callgraph/vta/graph_test.go +++ b/go/callgraph/vta/graph_test.go @@ -15,6 +15,7 @@ import ( "golang.org/x/tools/go/callgraph/cha" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/aliases" ) func TestNodeInterface(t *testing.T) { @@ -35,7 +36,7 @@ func TestNodeInterface(t *testing.T) { reg := firstRegInstr(main) // t0 := *gl X := pkg.Type("X").Type() gl := pkg.Var("gl") - glPtrType, ok := gl.Type().(*types.Pointer) + glPtrType, ok := aliases.Unalias(gl.Type()).(*types.Pointer) if !ok { t.Fatalf("could not cast gl variable to pointer type") } diff --git a/go/callgraph/vta/utils.go b/go/callgraph/vta/utils.go index 3471aae3a10..ed248d73e0b 100644 --- a/go/callgraph/vta/utils.go +++ b/go/callgraph/vta/utils.go @@ -9,6 +9,7 @@ import ( "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -24,7 +25,7 @@ func isReferenceNode(n node) bool { return true } - if _, ok := n.Type().(*types.Pointer); ok { + if _, ok := aliases.Unalias(n.Type()).(*types.Pointer); ok { return true } @@ -166,6 +167,7 @@ func siteCallees(c ssa.CallInstruction, callgraph *callgraph.Graph) []*ssa.Funct } func canHaveMethods(t types.Type) bool { + t = aliases.Unalias(t) if _, ok := t.(*types.Named); ok { return true } diff --git a/go/internal/gccgoimporter/parser.go b/go/internal/gccgoimporter/parser.go index 72bbe793c1e..dc40d217a88 100644 --- a/go/internal/gccgoimporter/parser.go +++ b/go/internal/gccgoimporter/parser.go @@ -20,6 +20,8 @@ import ( "strings" "text/scanner" "unicode/utf8" + + "golang.org/x/tools/internal/aliases" ) type parser struct { @@ -241,7 +243,7 @@ func (p *parser) parseName() string { } func deref(typ types.Type) types.Type { - if p, _ := typ.(*types.Pointer); p != nil { + if p, _ := aliases.Unalias(typ).(*types.Pointer); p != nil { typ = p.Elem() } return typ @@ -260,7 +262,7 @@ func (p *parser) parseField(pkg *types.Package) (field *types.Var, tag string) { if aname, ok := p.aliases[n]; ok { name = aname } else { - switch typ := deref(typ).(type) { + switch typ := aliases.Unalias(deref(typ)).(type) { case *types.Basic: name = typ.Name() case *types.Named: @@ -579,7 +581,7 @@ func (p *parser) parseNamedType(nlist []interface{}) types.Type { t := obj.Type() p.update(t, nlist) - nt, ok := t.(*types.Named) + nt, ok := aliases.Unalias(t).(*types.Named) if !ok { // This can happen for unsafe.Pointer, which is a TypeName holding a Basic type. pt := p.parseType(pkg) @@ -1334,7 +1336,7 @@ func (p *parser) parsePackage() *types.Package { } p.fixups = nil for _, typ := range p.typeList { - if it, ok := typ.(*types.Interface); ok { + if it, ok := aliases.Unalias(typ).(*types.Interface); ok { it.Complete() } } diff --git a/go/ssa/builder.go b/go/ssa/builder.go index ecbc3e4f566..95884060317 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -81,16 +81,15 @@ import ( "os" "sync" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/versions" ) -type opaqueType struct { - types.Type - name string -} +type opaqueType struct{ name string } -func (t *opaqueType) String() string { return t.name } +func (t *opaqueType) String() string { return t.name } +func (t *opaqueType) Underlying() types.Type { return t } var ( varOk = newVar("ok", tBool) @@ -103,7 +102,7 @@ var ( tInvalid = types.Typ[types.Invalid] tString = types.Typ[types.String] tUntypedNil = types.Typ[types.UntypedNil] - tRangeIter = &opaqueType{nil, "iter"} // the type of all "range" iterators + tRangeIter = &opaqueType{"iter"} // the type of all "range" iterators tEface = types.NewInterfaceType(nil, nil).Complete() // SSA Value constants. @@ -802,7 +801,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { if types.IsInterface(rt) { // If v may be an interface type I (after instantiating), // we must emit a check that v is non-nil. - if recv, ok := sel.recv.(*types.TypeParam); ok { + if recv, ok := aliases.Unalias(sel.recv).(*types.TypeParam); ok { // Emit a nil check if any possible instantiation of the // type parameter is an interface type. if typeSetOf(recv).Len() > 0 { @@ -1253,7 +1252,7 @@ func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero case *types.Array, *types.Slice: var at *types.Array var array Value - switch t := t.(type) { + switch t := aliases.Unalias(t).(type) { case *types.Slice: at = types.NewArray(t.Elem(), b.arrayLen(fn, e.Elts)) array = emitNew(fn, at, e.Lbrace, "slicelit") diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index a15ab97aca9..680358cc889 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -25,6 +25,7 @@ import ( "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" ) @@ -1091,7 +1092,7 @@ func TestIssue58491Rec(t *testing.T) { // Find the local type result instantiated with int. var found bool for _, rt := range p.Prog.RuntimeTypes() { - if n, ok := rt.(*types.Named); ok { + if n, ok := aliases.Unalias(rt).(*types.Named); ok { if u, ok := n.Underlying().(*types.Struct); ok { found = true if got, want := n.String(), "p.result"; got != want { diff --git a/go/ssa/const.go b/go/ssa/const.go index 2a6ac5882a0..e0d79f5ef72 100644 --- a/go/ssa/const.go +++ b/go/ssa/const.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -47,7 +48,7 @@ func soleTypeKind(typ types.Type) types.BasicInfo { state := types.IsBoolean | types.IsInteger | types.IsString underIs(typeSetOf(typ), func(t types.Type) bool { var c types.BasicInfo - if t, ok := t.(*types.Basic); ok { + if t, ok := aliases.Unalias(t).(*types.Basic); ok { c = t.Info() } if c&types.IsNumeric != 0 { // int/float/complex @@ -113,7 +114,7 @@ func zeroString(t types.Type, from *types.Package) string { } case *types.Pointer, *types.Slice, *types.Interface, *types.Chan, *types.Map, *types.Signature: return "nil" - case *types.Named: + case *types.Named, *aliases.Alias: return zeroString(t.Underlying(), from) case *types.Array, *types.Struct: return relType(t, from) + "{}" diff --git a/go/ssa/coretype.go b/go/ssa/coretype.go index 88136b43842..3a512830b1f 100644 --- a/go/ssa/coretype.go +++ b/go/ssa/coretype.go @@ -7,6 +7,7 @@ package ssa import ( "go/types" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -49,7 +50,7 @@ func typeSetOf(typ types.Type) termList { // This is a adaptation of x/exp/typeparams.NormalTerms which x/tools cannot depend on. var terms []*types.Term var err error - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.TypeParam: terms, err = typeparams.StructuralTerms(typ) case *types.Union: diff --git a/go/ssa/emit.go b/go/ssa/emit.go index 2a26c9492dc..549c9114d43 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -12,6 +12,7 @@ import ( "go/token" "go/types" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -184,7 +185,7 @@ func emitCompare(f *Function, op token.Token, x, y Value, pos token.Pos) Value { // isValuePreserving returns true if a conversion from ut_src to // ut_dst is value-preserving, i.e. just a change of type. -// Precondition: neither argument is a named type. +// Precondition: neither argument is a named or alias type. func isValuePreserving(ut_src, ut_dst types.Type) bool { // Identical underlying types? if types.IdenticalIgnoreTags(ut_dst, ut_src) { @@ -283,11 +284,11 @@ func emitConv(f *Function, val Value, typ types.Type) Value { } // Conversion from slice to array or slice to array pointer? - if slice, ok := s.(*types.Slice); ok { + if slice, ok := aliases.Unalias(s).(*types.Slice); ok { var arr *types.Array var ptr bool // Conversion from slice to array pointer? - switch d := d.(type) { + switch d := aliases.Unalias(d).(type) { case *types.Array: arr = d case *types.Pointer: diff --git a/go/ssa/interp/ops.go b/go/ssa/interp/ops.go index 65d6452b783..62b635c20ac 100644 --- a/go/ssa/interp/ops.go +++ b/go/ssa/interp/ops.go @@ -17,6 +17,7 @@ import ( "unsafe" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/aliases" ) // If the target program panics, the interpreter panics with this type. @@ -172,7 +173,7 @@ func asUnsigned(x value) (value, bool) { // zero returns a new "zero" value of the specified type. func zero(t types.Type) value { - switch t := t.(type) { + switch t := aliases.Unalias(t).(type) { case *types.Basic: if t.Kind() == types.UntypedNil { panic("untyped nil has no zero value") diff --git a/go/ssa/interp/reflect.go b/go/ssa/interp/reflect.go index 9f2f9e1e457..7df3ea27d8b 100644 --- a/go/ssa/interp/reflect.go +++ b/go/ssa/interp/reflect.go @@ -18,6 +18,7 @@ import ( "unsafe" "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/aliases" ) type opaqueType struct { @@ -119,7 +120,7 @@ func ext۰reflect۰rtype۰NumField(fr *frame, args []value) value { func ext۰reflect۰rtype۰NumIn(fr *frame, args []value) value { // Signature: func (t reflect.rtype) int - return args[0].(rtype).t.(*types.Signature).Params().Len() + return args[0].(rtype).t.Underlying().(*types.Signature).Params().Len() } func ext۰reflect۰rtype۰NumMethod(fr *frame, args []value) value { @@ -129,13 +130,13 @@ func ext۰reflect۰rtype۰NumMethod(fr *frame, args []value) value { func ext۰reflect۰rtype۰NumOut(fr *frame, args []value) value { // Signature: func (t reflect.rtype) int - return args[0].(rtype).t.(*types.Signature).Results().Len() + return args[0].(rtype).t.Underlying().(*types.Signature).Results().Len() } func ext۰reflect۰rtype۰Out(fr *frame, args []value) value { // Signature: func (t reflect.rtype, i int) int i := args[1].(int) - return makeReflectType(rtype{args[0].(rtype).t.(*types.Signature).Results().At(i).Type()}) + return makeReflectType(rtype{args[0].(rtype).t.Underlying().(*types.Signature).Results().At(i).Type()}) } func ext۰reflect۰rtype۰Size(fr *frame, args []value) value { @@ -178,7 +179,7 @@ func ext۰reflect۰Zero(fr *frame, args []value) value { } func reflectKind(t types.Type) reflect.Kind { - switch t := t.(type) { + switch t := aliases.Unalias(t).(type) { case *types.Named: return reflectKind(t.Underlying()) case *types.Basic: diff --git a/go/ssa/interp/value.go b/go/ssa/interp/value.go index 94018b550fc..d35da990ed1 100644 --- a/go/ssa/interp/value.go +++ b/go/ssa/interp/value.go @@ -45,6 +45,7 @@ import ( "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" ) type value interface{} @@ -117,7 +118,7 @@ func usesBuiltinMap(t types.Type) bool { switch t := t.(type) { case *types.Basic, *types.Chan, *types.Pointer: return true - case *types.Named: + case *types.Named, *aliases.Alias: return usesBuiltinMap(t.Underlying()) case *types.Interface, *types.Array, *types.Struct: return false diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 4797b39286c..5f46a18484c 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -11,6 +11,7 @@ import ( "go/types" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -209,6 +210,9 @@ func forEachReachable(msets *typeutil.MethodSetCache, T types.Type, f func(types } switch T := T.(type) { + case *aliases.Alias: + visit(aliases.Unalias(T), false) + case *types.Basic: // nop diff --git a/go/ssa/parameterized.go b/go/ssa/parameterized.go index 84db49d392f..74c541107ef 100644 --- a/go/ssa/parameterized.go +++ b/go/ssa/parameterized.go @@ -8,6 +8,7 @@ import ( "go/types" "sync" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -48,6 +49,9 @@ func (w *tpWalker) isParameterizedLocked(typ types.Type) (res bool) { case nil, *types.Basic: // TODO(gri) should nil be handled here? break + case *aliases.Alias: + return w.isParameterizedLocked(aliases.Unalias(t)) + case *types.Array: return w.isParameterizedLocked(t.Elem()) diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 22a3c6bc3dc..13bd39fe862 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -349,7 +349,7 @@ func (s *sanity) checkBlock(b *BasicBlock, index int) { // Check that "untyped" types only appear on constant operands. if _, ok := (*op).(*Const); !ok { - if basic, ok := (*op).Type().(*types.Basic); ok { + if basic, ok := (*op).Type().Underlying().(*types.Basic); ok { if basic.Info()&types.IsUntyped != 0 { s.errorf("operand #%d of %s is untyped: %s", i, instr, basic) } diff --git a/go/ssa/subst.go b/go/ssa/subst.go index a9a6d41e813..9f2f2f30008 100644 --- a/go/ssa/subst.go +++ b/go/ssa/subst.go @@ -6,6 +6,8 @@ package ssa import ( "go/types" + + "golang.org/x/tools/internal/aliases" ) // Type substituter for a fixed set of replacement types. @@ -80,6 +82,9 @@ func (subst *subster) typ(t types.Type) (res types.Type) { // fall through if result r will be identical to t, types.Identical(r, t). switch t := t.(type) { + case *aliases.Alias: + return subst.typ(aliases.Unalias(t)) + case *types.TypeParam: r := subst.replacements[t] assert(r != nil, "type param without replacement encountered") @@ -466,7 +471,7 @@ func reaches(t types.Type, c map[types.Type]bool) (res bool) { return true } } - case *types.Named: + case *types.Named, *aliases.Alias: return reaches(t.Underlying(), c) default: panic("unreachable") diff --git a/go/ssa/util.go b/go/ssa/util.go index 915b4e274c1..4d65259ed9c 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -17,6 +17,7 @@ import ( "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" "golang.org/x/tools/internal/typesinternal" ) @@ -51,16 +52,19 @@ func isNonTypeParamInterface(t types.Type) bool { // isBasic reports whether t is a basic type. func isBasic(t types.Type) bool { - _, ok := t.(*types.Basic) + _, ok := aliases.Unalias(t).(*types.Basic) return ok } // isString reports whether t is exactly a string type. +// t is assumed to be an Underlying type (not Named or Alias). func isString(t types.Type) bool { - return isBasic(t) && t.(*types.Basic).Info()&types.IsString != 0 + basic, ok := t.(*types.Basic) + return ok && basic.Info()&types.IsString != 0 } // isByteSlice reports whether t is of the form []~bytes. +// t is assumed to be an Underlying type (not Named or Alias). func isByteSlice(t types.Type) bool { if b, ok := t.(*types.Slice); ok { e, _ := b.Elem().Underlying().(*types.Basic) @@ -70,6 +74,7 @@ func isByteSlice(t types.Type) bool { } // isRuneSlice reports whether t is of the form []~runes. +// t is assumed to be an Underlying type (not Named or Alias). func isRuneSlice(t types.Type) bool { if b, ok := t.(*types.Slice); ok { e, _ := b.Elem().Underlying().(*types.Basic) @@ -131,8 +136,9 @@ func fieldOf(typ types.Type, index int) *types.Var { return nil } -// isUntyped returns true for types that are untyped. +// isUntyped reports whether typ is the type of an untyped constant. func isUntyped(typ types.Type) bool { + // No Underlying/Unalias: untyped constant types cannot be Named or Alias. b, ok := typ.(*types.Basic) return ok && b.Info()&types.IsUntyped != 0 } @@ -342,10 +348,10 @@ func (m *typeListMap) hash(ts []types.Type) uint32 { // instantiateMethod instantiates m with targs and returns a canonical representative for this method. func (canon *canonizer) instantiateMethod(m *types.Func, targs []types.Type, ctxt *types.Context) *types.Func { recv := recvType(m) - if p, ok := recv.(*types.Pointer); ok { + if p, ok := aliases.Unalias(recv).(*types.Pointer); ok { recv = p.Elem() } - named := recv.(*types.Named) + named := aliases.Unalias(recv).(*types.Named) inst, err := types.Instantiate(ctxt, named.Origin(), targs, false) if err != nil { panic(err) diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go index 8692d51f0b7..382d8ab3645 100644 --- a/go/types/internal/play/play.go +++ b/go/types/internal/play/play.go @@ -32,6 +32,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" ) @@ -285,7 +286,7 @@ func formatObj(out *strings.Builder, fset *token.FileSet, ref string, obj types. if obj.IsAlias() { kind = "type alias" } - if named, ok := obj.Type().(*types.Named); ok { + if named, ok := aliases.Unalias(obj.Type()).(*types.Named); ok { origin = named.Obj() } } diff --git a/go/types/objectpath/objectpath.go b/go/types/objectpath/objectpath.go index 62d9585a691..6a57ce3b136 100644 --- a/go/types/objectpath/objectpath.go +++ b/go/types/objectpath/objectpath.go @@ -29,10 +29,13 @@ import ( "strconv" "strings" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" ) +// TODO(adonovan): think about generic aliases. + // A Path is an opaque name that identifies a types.Object // relative to its package. Conceptually, the name consists of a // sequence of destructuring operations applied to the package scope @@ -224,7 +227,7 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { // Reject obviously non-viable cases. switch obj := obj.(type) { case *types.TypeName: - if _, ok := obj.Type().(*types.TypeParam); !ok { + if _, ok := aliases.Unalias(obj.Type()).(*types.TypeParam); !ok { // With the exception of type parameters, only package-level type names // have a path. return "", fmt.Errorf("no path for %v", obj) @@ -311,7 +314,7 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { } // Inspect declared methods of defined types. - if T, ok := o.Type().(*types.Named); ok { + if T, ok := aliases.Unalias(o.Type()).(*types.Named); ok { path = append(path, opType) // The method index here is always with respect // to the underlying go/types data structures, @@ -440,6 +443,8 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) { // nil, it will be allocated as necessary. func find(obj types.Object, T types.Type, path []byte, seen map[*types.TypeName]bool) []byte { switch T := T.(type) { + case *aliases.Alias: + return find(obj, aliases.Unalias(T), path, seen) case *types.Basic, *types.Named: // Named types belonging to pkg were handled already, // so T must belong to another package. No path. @@ -612,6 +617,7 @@ func Object(pkg *types.Package, p Path) (types.Object, error) { // Inv: t != nil, obj == nil + t = aliases.Unalias(t) switch code { case opElem: hasElem, ok := t.(hasElem) // Pointer, Slice, Array, Chan, Map diff --git a/go/types/typeutil/ui.go b/go/types/typeutil/ui.go index fa55b0a1e65..a0c1a60ac02 100644 --- a/go/types/typeutil/ui.go +++ b/go/types/typeutil/ui.go @@ -6,7 +6,11 @@ package typeutil // This file defines utilities for user interfaces that display types. -import "go/types" +import ( + "go/types" + + "golang.org/x/tools/internal/aliases" +) // IntuitiveMethodSet returns the intuitive method set of a type T, // which is the set of methods you can call on an addressable value of @@ -24,7 +28,7 @@ import "go/types" // The order of the result is as for types.MethodSet(T). func IntuitiveMethodSet(T types.Type, msets *MethodSetCache) []*types.Selection { isPointerToConcrete := func(T types.Type) bool { - ptr, ok := T.(*types.Pointer) + ptr, ok := aliases.Unalias(T).(*types.Pointer) return ok && !types.IsInterface(ptr.Elem()) } diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index b24a0fba9e7..c3022a28625 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -13,6 +13,8 @@ import ( "go/token" "go/types" "strconv" + + "golang.org/x/tools/internal/aliases" ) func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos { @@ -28,7 +30,10 @@ func TypeErrorEndPos(fset *token.FileSet, src []byte, start token.Pos) token.Pos } func ZeroValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { - under := typ + // TODO(adonovan): think about generics, and also generic aliases. + under := aliases.Unalias(typ) + // Don't call Underlying unconditionally: although it removed + // Named and Alias, it also removes TypeParam. if n, ok := typ.(*types.Named); ok { under = n.Underlying() } diff --git a/internal/facts/facts_test.go b/internal/facts/facts_test.go index 56eb599cfd9..daebea2ff59 100644 --- a/internal/facts/facts_test.go +++ b/internal/facts/facts_test.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/packages" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/facts" "golang.org/x/tools/internal/testenv" ) @@ -360,7 +361,7 @@ func find(p *types.Package, expr string) types.Object { if err != nil { return nil } - if n, ok := tv.Type.(*types.Named); ok { + if n, ok := aliases.Unalias(tv.Type).(*types.Named); ok { return n.Obj() } return nil diff --git a/internal/facts/imports.go b/internal/facts/imports.go index 1fe63ca6b51..9f706cd954f 100644 --- a/internal/facts/imports.go +++ b/internal/facts/imports.go @@ -6,6 +6,8 @@ package facts import ( "go/types" + + "golang.org/x/tools/internal/aliases" ) // importMap computes the import map for a package by traversing the @@ -45,6 +47,8 @@ func importMap(imports []*types.Package) map[string]*types.Package { addType = func(T types.Type) { switch T := T.(type) { + case *aliases.Alias: + addType(aliases.Unalias(T)) case *types.Basic: // nop case *types.Named: diff --git a/internal/gcimporter/bexport_test.go b/internal/gcimporter/bexport_test.go index 72fa8a2a31e..1a2c8e8dd0a 100644 --- a/internal/gcimporter/bexport_test.go +++ b/internal/gcimporter/bexport_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/gcimporter" ) @@ -30,6 +31,8 @@ func fileLine(fset *token.FileSet, obj types.Object) string { } func equalType(x, y types.Type) error { + x = aliases.Unalias(x) + y = aliases.Unalias(y) if reflect.TypeOf(x) != reflect.TypeOf(y) { return fmt.Errorf("unequal kinds: %T vs %T", x, y) } diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 84f99400bcf..81d36bd5ff6 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -28,6 +28,7 @@ import ( "testing" "time" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/goroot" "golang.org/x/tools/internal/testenv" ) @@ -438,7 +439,7 @@ func TestImportedTypes(t *testing.T) { t.Errorf("%s: got %q; want %q", test.name, got, test.want) } - if named, _ := obj.Type().(*types.Named); named != nil { + if named, _ := aliases.Unalias(obj.Type()).(*types.Named); named != nil { verifyInterfaceMethodRecvs(t, named, 0) } } @@ -521,7 +522,7 @@ func verifyInterfaceMethodRecvs(t *testing.T, named *types.Named, level int) { // check embedded interfaces (if they are named, too) for i := 0; i < iface.NumEmbeddeds(); i++ { // embedding of interfaces cannot have cycles; recursion will terminate - if etype, _ := iface.EmbeddedType(i).(*types.Named); etype != nil { + if etype, _ := aliases.Unalias(iface.EmbeddedType(i)).(*types.Named); etype != nil { verifyInterfaceMethodRecvs(t, etype, level+1) } } @@ -541,7 +542,7 @@ func TestIssue5815(t *testing.T) { t.Errorf("no pkg for %s", obj) } if tname, _ := obj.(*types.TypeName); tname != nil { - named := tname.Type().(*types.Named) + named := aliases.Unalias(tname.Type()).(*types.Named) for i := 0; i < named.NumMethods(); i++ { m := named.Method(i) if m.Pkg() == nil { @@ -641,7 +642,7 @@ func TestIssue13898(t *testing.T) { // look for go/types.Object type obj := lookupObj(t, goTypesPkg.Scope(), "Object") - typ, ok := obj.Type().(*types.Named) + typ, ok := aliases.Unalias(obj.Type()).(*types.Named) if !ok { t.Fatalf("go/types.Object type is %v; wanted named type", typ) } diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 2ee8c70164f..638fc1d3b86 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go @@ -23,6 +23,7 @@ import ( "strings" "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/tokeninternal" ) @@ -506,13 +507,13 @@ func (p *iexporter) doDecl(obj types.Object) { case *types.TypeName: t := obj.Type() - if tparam, ok := t.(*types.TypeParam); ok { + if tparam, ok := aliases.Unalias(t).(*types.TypeParam); ok { w.tag('P') w.pos(obj.Pos()) constraint := tparam.Constraint() if p.version >= iexportVersionGo1_18 { implicit := false - if iface, _ := constraint.(*types.Interface); iface != nil { + if iface, _ := aliases.Unalias(constraint).(*types.Interface); iface != nil { implicit = iface.IsImplicit() } w.bool(implicit) @@ -738,6 +739,8 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { }() } switch t := t.(type) { + // TODO(adonovan): support types.Alias. + case *types.Named: if targs := t.TypeArgs(); targs.Len() > 0 { w.startType(instanceType) @@ -843,7 +846,7 @@ func (w *exportWriter) doTyp(t types.Type, pkg *types.Package) { for i := 0; i < n; i++ { ft := t.EmbeddedType(i) tPkg := pkg - if named, _ := ft.(*types.Named); named != nil { + if named, _ := aliases.Unalias(ft).(*types.Named); named != nil { w.pos(named.Obj().Pos()) } else { w.pos(token.NoPos) diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index 778caf6078e..4d50eb8e587 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go @@ -22,6 +22,7 @@ import ( "strings" "golang.org/x/tools/go/types/objectpath" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) @@ -523,7 +524,7 @@ func canReuse(def *types.Named, rhs types.Type) bool { if def == nil { return true } - iface, _ := rhs.(*types.Interface) + iface, _ := aliases.Unalias(rhs).(*types.Interface) if iface == nil { return true } @@ -594,7 +595,7 @@ func (r *importReader) obj(name string) { if targs.Len() > 0 { rparams = make([]*types.TypeParam, targs.Len()) for i := range rparams { - rparams[i] = targs.At(i).(*types.TypeParam) + rparams[i] = aliases.Unalias(targs.At(i)).(*types.TypeParam) } } msig := r.signature(recv, rparams, nil) @@ -624,7 +625,7 @@ func (r *importReader) obj(name string) { } constraint := r.typ() if implicit { - iface, _ := constraint.(*types.Interface) + iface, _ := aliases.Unalias(constraint).(*types.Interface) if iface == nil { errorf("non-interface constraint marked implicit") } @@ -831,7 +832,7 @@ func (r *importReader) typ() types.Type { } func isInterface(t types.Type) bool { - _, ok := t.(*types.Interface) + _, ok := aliases.Unalias(t).(*types.Interface) return ok } @@ -1030,7 +1031,7 @@ func (r *importReader) tparamList() []*types.TypeParam { for i := range xs { // Note: the standard library importer is tolerant of nil types here, // though would panic in SetTypeParams. - xs[i] = r.typ().(*types.TypeParam) + xs[i] = aliases.Unalias(r.typ()).(*types.TypeParam) } return xs } diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index b977435f626..bcf52d931df 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -16,6 +16,7 @@ import ( "sort" "strings" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/pkgbits" ) @@ -553,7 +554,7 @@ func (pr *pkgReader) objIdx(idx pkgbits.Index) (*types.Package, string) { // If the underlying type is an interface, we need to // duplicate its methods so we can replace the receiver // parameter's type (#49906). - if iface, ok := underlying.(*types.Interface); ok && iface.NumExplicitMethods() != 0 { + if iface, ok := aliases.Unalias(underlying).(*types.Interface); ok && iface.NumExplicitMethods() != 0 { methods := make([]*types.Func, iface.NumExplicitMethods()) for i := range methods { fn := iface.ExplicitMethod(i) diff --git a/internal/typeparams/common.go b/internal/typeparams/common.go index e5e3ed313cd..6679d79f9cf 100644 --- a/internal/typeparams/common.go +++ b/internal/typeparams/common.go @@ -28,6 +28,7 @@ import ( "go/token" "go/types" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/typesinternal" ) @@ -74,9 +75,9 @@ func PackIndexExpr(x ast.Expr, lbrack token.Pos, indices []ast.Expr, rbrack toke } } -// IsTypeParam reports whether t is a type parameter. +// IsTypeParam reports whether t is a type parameter (or an alias of one). func IsTypeParam(t types.Type) bool { - _, ok := t.(*types.TypeParam) + _, ok := aliases.Unalias(t).(*types.TypeParam) return ok } @@ -155,6 +156,9 @@ func OriginMethod(fn *types.Func) *types.Func { // In this case, GenericAssignableTo reports that instantiations of Container // are assignable to the corresponding instantiation of Interface. func GenericAssignableTo(ctxt *types.Context, V, T types.Type) bool { + V = aliases.Unalias(V) + T = aliases.Unalias(T) + // If V and T are not both named, or do not have matching non-empty type // parameter lists, fall back on types.AssignableTo. diff --git a/internal/typeparams/coretype.go b/internal/typeparams/coretype.go index 048e2161493..e66e9d0f48c 100644 --- a/internal/typeparams/coretype.go +++ b/internal/typeparams/coretype.go @@ -7,6 +7,8 @@ package typeparams import ( "fmt" "go/types" + + "golang.org/x/tools/internal/aliases" ) // CoreType returns the core type of T or nil if T does not have a core type. @@ -110,7 +112,7 @@ func CoreType(T types.Type) types.Type { // _NormalTerms makes no guarantees about the order of terms, except that it // is deterministic. func _NormalTerms(typ types.Type) ([]*types.Term, error) { - switch typ := typ.(type) { + switch typ := aliases.Unalias(typ).(type) { case *types.TypeParam: return StructuralTerms(typ) case *types.Union: diff --git a/internal/typeparams/genericfeatures/features.go b/internal/typeparams/genericfeatures/features.go index e307e677758..e7d0e0e6112 100644 --- a/internal/typeparams/genericfeatures/features.go +++ b/internal/typeparams/genericfeatures/features.go @@ -12,6 +12,7 @@ import ( "strings" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/aliases" ) // Features is a set of flags reporting which features of generic Go code a @@ -92,7 +93,7 @@ func ForPackage(inspect *inspector.Inspector, info *types.Info) Features { }) for _, inst := range info.Instances { - switch inst.Type.(type) { + switch aliases.Unalias(inst.Type).(type) { case *types.Named: direct |= TypeInstantiation case *types.Signature: diff --git a/refactor/rename/check.go b/refactor/rename/check.go index 9f29b98a0a4..ac2c5d4206f 100644 --- a/refactor/rename/check.go +++ b/refactor/rename/check.go @@ -13,6 +13,7 @@ import ( "go/types" "golang.org/x/tools/go/loader" + "golang.org/x/tools/internal/aliases" "golang.org/x/tools/refactor/satisfy" ) @@ -467,9 +468,10 @@ func (r *renamer) checkStructField(from *types.Var) { // type T int // this and // var s struct {T} // this must change too. if from.Anonymous() { - if named, ok := from.Type().(*types.Named); ok { + // TODO(adonovan): think carefully about aliases. + if named, ok := aliases.Unalias(from.Type()).(*types.Named); ok { r.check(named.Obj()) - } else if named, ok := deref(from.Type()).(*types.Named); ok { + } else if named, ok := aliases.Unalias(deref(from.Type())).(*types.Named); ok { r.check(named.Obj()) } } @@ -777,7 +779,7 @@ func (r *renamer) checkMethod(from *types.Func) { var iface string I := recv(imeth).Type() - if named, ok := I.(*types.Named); ok { + if named, ok := aliases.Unalias(I).(*types.Named); ok { pos = named.Obj().Pos() iface = "interface " + named.Obj().Name() } else { @@ -851,7 +853,7 @@ func someUse(info *loader.PackageInfo, obj types.Object) *ast.Ident { func isInterface(T types.Type) bool { return types.IsInterface(T) } func deref(typ types.Type) types.Type { - if p, _ := typ.(*types.Pointer); p != nil { + if p, _ := aliases.Unalias(typ).(*types.Pointer); p != nil { return p.Elem() } return typ diff --git a/refactor/rename/spec.go b/refactor/rename/spec.go index 22a268a7942..69198cb0d96 100644 --- a/refactor/rename/spec.go +++ b/refactor/rename/spec.go @@ -25,6 +25,7 @@ import ( "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/loader" + "golang.org/x/tools/internal/aliases" ) // A spec specifies an entity to rename. @@ -465,9 +466,9 @@ func findObjects(info *loader.PackageInfo, spec *spec) ([]types.Object, error) { if spec.searchFor == "" { // If it is an embedded field, return the type of the field. if v, ok := obj.(*types.Var); ok && v.Anonymous() { - switch t := v.Type().(type) { + switch t := aliases.Unalias(v.Type()).(type) { case *types.Pointer: - return []types.Object{t.Elem().(*types.Named).Obj()}, nil + return []types.Object{aliases.Unalias(t.Elem()).(*types.Named).Obj()}, nil case *types.Named: return []types.Object{t.Obj()}, nil } From 7f348c7a4c28c9162056530f6be369bb179e05a5 Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 27 Feb 2024 16:42:54 -0800 Subject: [PATCH 44/47] internal/versions: updates the meaning of FileVersions. For Go >=1.22, FileVersions returns an unknown [invalid] Future version when the file versions would be invalid. This better matches go/types. For Go <=1.21, FileVersions returns either the runtime.Version() or the Future version. Adds AtLeast and Before for doing comparisons with Future. Updates golang/go#65612 Fixes golang/go#66007 Change-Id: I93ff1681b0f9117765614a20d82642749597307c Reviewed-on: https://go-review.googlesource.com/c/tools/+/567635 Reviewed-by: Robert Findley TryBot-Result: Gopher Robot Run-TryBot: Tim King LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/loopclosure/loopclosure.go | 5 +- .../passes/loopclosure/loopclosure_test.go | 3 + .../passes/loopclosure/testdata/src/a/a.go | 3 +- .../testdata/src/subtests/subtest.go | 3 +- .../testdata/src/typeparams/typeparams.go | 11 +- go/ssa/builder.go | 5 +- go/ssa/create.go | 2 +- internal/versions/features.go | 43 +++++++ internal/versions/toolchain.go | 14 +++ internal/versions/toolchain_go119.go | 14 +++ internal/versions/toolchain_go120.go | 14 +++ internal/versions/toolchain_go121.go | 14 +++ internal/versions/types_go121.go | 18 ++- internal/versions/types_go122.go | 25 +++- internal/versions/types_test.go | 4 +- internal/versions/versions.go | 4 +- internal/versions/versions_test.go | 117 ++++++++++++++++++ 17 files changed, 274 insertions(+), 25 deletions(-) create mode 100644 internal/versions/features.go create mode 100644 internal/versions/toolchain.go create mode 100644 internal/versions/toolchain_go119.go create mode 100644 internal/versions/toolchain_go120.go create mode 100644 internal/versions/toolchain_go121.go diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 7b78939c86e..fe05eda44e4 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -55,9 +55,8 @@ func run(pass *analysis.Pass) (interface{}, error) { switch n := n.(type) { case *ast.File: // Only traverse the file if its goversion is strictly before go1.22. - goversion := versions.Lang(versions.FileVersions(pass.TypesInfo, n)) - // goversion is empty for older go versions (or the version is invalid). - return goversion == "" || versions.Compare(goversion, "go1.22") < 0 + goversion := versions.FileVersion(pass.TypesInfo, n) + return versions.Before(goversion, versions.Go1_22) case *ast.RangeStmt: body = n.Body addVar(n.Key) diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index 386f53289ce..c8aab4834d3 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go @@ -17,6 +17,9 @@ import ( ) func Test(t *testing.T) { + // legacy loopclosure test expectations are incorrect > 1.21. + testenv.SkipAfterGo1Point(t, 21) + testdata := analysistest.TestData() analysistest.Run(t, testdata, loopclosure.Analyzer, "a", "golang.org/...", "subtests", "typeparams") diff --git a/go/analysis/passes/loopclosure/testdata/src/a/a.go b/go/analysis/passes/loopclosure/testdata/src/a/a.go index 7a7f05f663f..eb4d2a6cc7a 100644 --- a/go/analysis/passes/loopclosure/testdata/src/a/a.go +++ b/go/analysis/passes/loopclosure/testdata/src/a/a.go @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file contains tests for the loopclosure checker. +// This file contains legacy tests for the loopclosure checker. +// Legacy expectations are incorrect after go1.22. package testdata diff --git a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go index 50283ec6152..faf98387c5d 100644 --- a/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go +++ b/go/analysis/passes/loopclosure/testdata/src/subtests/subtest.go @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file contains tests that the loopclosure analyzer detects leaked +// This file contains legacy tests that the loopclosure analyzer detects leaked // references via parallel subtests. +// Legacy expectations are incorrect after go1.22. package subtests diff --git a/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go b/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go index 55e129c0ab0..ef5b143f6da 100644 --- a/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// This file contains tests for the loopclosure checker. +// This file contains legacy tests for the loopclosure checker for GoVersion 0 - if afterGo122 { + if versions.AtLeast(fn.goversion, versions.Go1_22) { b.forStmtGo122(fn, s, label) return } @@ -2243,7 +2242,7 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { } } - afterGo122 := versions.Compare(fn.goversion, "go1.21") > 0 + afterGo122 := versions.AtLeast(fn.goversion, versions.Go1_22) if s.Tok == token.DEFINE && !afterGo122 { // pre-go1.22: If iteration variables are defined (:=), this // occurs once outside the loop. diff --git a/go/ssa/create.go b/go/ssa/create.go index 4545c178d78..f8f584a1a56 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -245,7 +245,7 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * if len(files) > 0 { // Go source package. for _, file := range files { - goversion := versions.Lang(versions.FileVersions(p.info, file)) + goversion := versions.Lang(versions.FileVersion(p.info, file)) for _, decl := range file.Decls { membersFromDecl(p, decl, goversion) } diff --git a/internal/versions/features.go b/internal/versions/features.go new file mode 100644 index 00000000000..b53f1786161 --- /dev/null +++ b/internal/versions/features.go @@ -0,0 +1,43 @@ +// 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 versions + +// This file contains predicates for working with file versions to +// decide when a tool should consider a language feature enabled. + +// GoVersions that features in x/tools can be gated to. +const ( + Go1_18 = "go1.18" + Go1_19 = "go1.19" + Go1_20 = "go1.20" + Go1_21 = "go1.21" + Go1_22 = "go1.22" +) + +// Future is an invalid unknown Go version sometime in the future. +// Do not use directly with Compare. +const Future = "" + +// AtLeast reports whether the file version v comes after a Go release. +// +// Use this predicate to enable a behavior once a certain Go release +// has happened (and stays enabled in the future). +func AtLeast(v, release string) bool { + if v == Future { + return true // an unknown future version is always after y. + } + return Compare(Lang(v), Lang(release)) >= 0 +} + +// Before reports whether the file version v is strictly before a Go release. +// +// Use this predicate to disable a behavior once a certain Go release +// has happened (and stays enabled in the future). +func Before(v, release string) bool { + if v == Future { + return false // an unknown future version happens after y. + } + return Compare(Lang(v), Lang(release)) < 0 +} diff --git a/internal/versions/toolchain.go b/internal/versions/toolchain.go new file mode 100644 index 00000000000..377bf7a53b4 --- /dev/null +++ b/internal/versions/toolchain.go @@ -0,0 +1,14 @@ +// 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 versions + +// toolchain is maximum version (<1.22) that the go toolchain used +// to build the current tool is known to support. +// +// When a tool is built with >=1.22, the value of toolchain is unused. +// +// x/tools does not support building with go <1.18. So we take this +// as the minimum possible maximum. +var toolchain string = Go1_18 diff --git a/internal/versions/toolchain_go119.go b/internal/versions/toolchain_go119.go new file mode 100644 index 00000000000..f65beed9d83 --- /dev/null +++ b/internal/versions/toolchain_go119.go @@ -0,0 +1,14 @@ +// 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.19 +// +build go1.19 + +package versions + +func init() { + if Compare(toolchain, Go1_19) < 0 { + toolchain = Go1_19 + } +} diff --git a/internal/versions/toolchain_go120.go b/internal/versions/toolchain_go120.go new file mode 100644 index 00000000000..1a9efa126cd --- /dev/null +++ b/internal/versions/toolchain_go120.go @@ -0,0 +1,14 @@ +// 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.20 +// +build go1.20 + +package versions + +func init() { + if Compare(toolchain, Go1_20) < 0 { + toolchain = Go1_20 + } +} diff --git a/internal/versions/toolchain_go121.go b/internal/versions/toolchain_go121.go new file mode 100644 index 00000000000..b7ef216dfec --- /dev/null +++ b/internal/versions/toolchain_go121.go @@ -0,0 +1,14 @@ +// 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.21 +// +build go1.21 + +package versions + +func init() { + if Compare(toolchain, Go1_21) < 0 { + toolchain = Go1_21 + } +} diff --git a/internal/versions/types_go121.go b/internal/versions/types_go121.go index a7b79207aee..b4345d3349e 100644 --- a/internal/versions/types_go121.go +++ b/internal/versions/types_go121.go @@ -12,9 +12,19 @@ import ( "go/types" ) -// FileVersions always reports the a file's Go version as the -// zero version at this Go version. -func FileVersions(info *types.Info, file *ast.File) string { return "" } +// FileVersion returns a language version (<=1.21) derived from runtime.Version() +// or an unknown future version. +func FileVersion(info *types.Info, file *ast.File) string { + // In x/tools built with Go <= 1.21, we do not have Info.FileVersions + // available. We use a go version derived from the toolchain used to + // compile the tool by default. + // This will be <= go1.21. We take this as the maximum version that + // this tool can support. + // + // There are no features currently in x/tools that need to tell fine grained + // differences for versions <1.22. + return toolchain +} -// InitFileVersions is a noop at this Go version. +// InitFileVersions is a noop when compiled with this Go version. func InitFileVersions(*types.Info) {} diff --git a/internal/versions/types_go122.go b/internal/versions/types_go122.go index 7b9ba89a822..e8180632a52 100644 --- a/internal/versions/types_go122.go +++ b/internal/versions/types_go122.go @@ -12,10 +12,27 @@ import ( "go/types" ) -// FileVersions maps a file to the file's semantic Go version. -// The reported version is the zero version if a version cannot be determined. -func FileVersions(info *types.Info, file *ast.File) string { - return info.FileVersions[file] +// FileVersions returns a file's Go version. +// The reported version is an unknown Future version if a +// version cannot be determined. +func FileVersion(info *types.Info, file *ast.File) string { + // In tools built with Go >= 1.22, the Go version of a file + // follow a cascades of sources: + // 1) types.Info.FileVersion, which follows the cascade: + // 1.a) file version (ast.File.GoVersion), + // 1.b) the package version (types.Config.GoVersion), or + // 2) is some unknown Future version. + // + // File versions require a valid package version to be provided to types + // in Config.GoVersion. Config.GoVersion is either from the package's module + // or the toolchain (go run). This value should be provided by go/packages + // or unitchecker.Config.GoVersion. + if v := info.FileVersions[file]; IsValid(v) { + return v + } + // Note: we could instead return runtime.Version() [if valid]. + // This would act as a max version on what a tool can support. + return Future } // InitFileVersions initializes info to record Go versions for Go files. diff --git a/internal/versions/types_test.go b/internal/versions/types_test.go index 0ffdd468dcf..59f6d18b45f 100644 --- a/internal/versions/types_test.go +++ b/internal/versions/types_test.go @@ -52,11 +52,11 @@ func Test(t *testing.T) { if got, want := versions.GoVersion(pkg), item.pversion; versions.Compare(got, want) != 0 { t.Errorf("GoVersion()=%q. expected %q", got, want) } - if got := versions.FileVersions(info, nil); got != "" { + if got := versions.FileVersion(info, nil); got != "" { t.Errorf(`FileVersions(nil)=%q. expected ""`, got) } for i, test := range item.tests { - if got, want := versions.FileVersions(info, files[i]), test.want; got != want { + if got, want := versions.FileVersion(info, files[i]), test.want; got != want { t.Errorf("FileVersions(%s)=%q. expected %q", test.fname, got, want) } } diff --git a/internal/versions/versions.go b/internal/versions/versions.go index f8982841b7b..8d1f7453dbf 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -4,7 +4,9 @@ package versions -import "strings" +import ( + "strings" +) // Note: If we use build tags to use go/versions when go >=1.22, // we run into go.dev/issue/53737. Under some operations users would see an diff --git a/internal/versions/versions_test.go b/internal/versions/versions_test.go index 997de2a8a61..dbc1c555d22 100644 --- a/internal/versions/versions_test.go +++ b/internal/versions/versions_test.go @@ -5,8 +5,13 @@ package versions_test import ( + "go/ast" + "go/parser" + "go/token" + "go/types" "testing" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/versions" ) @@ -137,3 +142,115 @@ func TestLang(t *testing.T) { } } + +func TestKnown(t *testing.T) { + for _, v := range [...]string{ + versions.Go1_18, + versions.Go1_19, + versions.Go1_20, + versions.Go1_21, + versions.Go1_22, + } { + if !versions.IsValid(v) { + t.Errorf("Expected known version %q to be valid.", v) + } + if v != versions.Lang(v) { + t.Errorf("Expected known version %q == Lang(%q).", v, versions.Lang(v)) + } + } +} + +func TestAtLeast(t *testing.T) { + for _, item := range [...]struct { + v, release string + want bool + }{ + {versions.Future, versions.Go1_22, true}, + {versions.Go1_22, versions.Go1_22, true}, + {"go1.21", versions.Go1_22, false}, + {"invalid", versions.Go1_22, false}, + } { + if got := versions.AtLeast(item.v, item.release); got != item.want { + t.Errorf("AtLeast(%q, %q)=%v. wanted %v", item.v, item.release, got, item.want) + } + } +} + +func TestBefore(t *testing.T) { + for _, item := range [...]struct { + v, release string + want bool + }{ + {versions.Future, versions.Go1_22, false}, + {versions.Go1_22, versions.Go1_22, false}, + {"go1.21", versions.Go1_22, true}, + {"invalid", versions.Go1_22, true}, // invalid < Go1_22 + } { + if got := versions.Before(item.v, item.release); got != item.want { + t.Errorf("Before(%q, %q)=%v. wanted %v", item.v, item.release, got, item.want) + } + } +} + +func TestFileVersions122(t *testing.T) { + testenv.NeedsGo1Point(t, 22) + + const source = ` + package P + ` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "hello.go", source, 0) + if err != nil { + t.Fatal(err) + } + + for _, conf := range []types.Config{ + {GoVersion: versions.Go1_22}, + {}, // GoVersion is unset. + } { + info := &types.Info{} + versions.InitFileVersions(info) + + _, err = conf.Check("P", fset, []*ast.File{f}, info) + if err != nil { + t.Fatal(err) + } + + v := versions.FileVersion(info, f) + if !versions.AtLeast(v, versions.Go1_22) { + t.Errorf("versions.AtLeast(%q, %q) expected to hold", v, versions.Go1_22) + } + + if versions.Before(v, versions.Go1_22) { + t.Errorf("versions.AtLeast(%q, %q) expected to be false", v, versions.Go1_22) + } + + if conf.GoVersion == "" && v != versions.Future { + t.Error("Expected the FileVersion to be the Future when conf.GoVersion is unset") + } + } +} + +func TestFileVersions121(t *testing.T) { + testenv.SkipAfterGo1Point(t, 21) + + // If <1.22, info and file are ignored. + v := versions.FileVersion(nil, nil) + oneof := map[string]bool{ + versions.Go1_18: true, + versions.Go1_19: true, + versions.Go1_20: true, + versions.Go1_21: true, + } + if !oneof[v] { + t.Errorf("FileVersion(...)=%q expected to be a known go version <1.22", v) + } + + if versions.AtLeast(v, versions.Go1_22) { + t.Errorf("versions.AtLeast(%q, %q) expected to be false", v, versions.Go1_22) + } + + if !versions.Before(v, versions.Go1_22) { + t.Errorf("versions.Before(%q, %q) expected to hold", v, versions.Go1_22) + } +} From 283fce21c13672b609664a88e2575aaae16a7ddb Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 27 Feb 2024 12:32:21 -0500 Subject: [PATCH 45/47] x/tools: drop go1.18 support Updates golang/go#64407 Change-Id: I247a7ff7f07613674f8e31e4cb9c5a68762d2203 Reviewed-on: https://go-review.googlesource.com/c/tools/+/567418 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- cmd/bisect/main_test.go | 3 +- copyright/copyright.go | 3 - copyright/copyright_test.go | 3 - go.mod | 2 +- go/analysis/passes/asmdecl/arches_go118.go | 12 - go/analysis/passes/asmdecl/arches_go119.go | 14 - go/analysis/passes/asmdecl/asmdecl.go | 3 +- .../testdata/src/typeparams/typeparams.go | 2 - .../src/typeparams/typeparams.go.golden | 2 - .../testdata/src/typeparams/typeparams.go | 2 - .../composite/testdata/src/a/a_fuzz_test.go | 3 - .../testdata/src/a/a_fuzz_test.go.golden | 3 - .../testdata/src/typeparams/typeparams.go | 2 - .../passes/loopclosure/loopclosure_test.go | 2 - .../testdata/src/typeparams/typeparams.go | 2 +- .../testdata/src/versions/go18.txtar | 6 +- .../testdata/src/versions/go22.txtar | 3 + .../testdata/src/typeparams/typeparams.go | 4 +- .../testdata/src/typeparams/typeparams.go | 8 +- .../passes/nilness/nilness_go117_test.go | 20 - go/analysis/passes/nilness/nilness_test.go | 5 + go/analysis/passes/printf/printf_test.go | 3 - .../testdata/src/typeparams/diagnostics.go | 3 - .../testdata/src/typeparams/wrappers.go | 3 - .../passes/stdmethods/testdata/src/a/b.go | 3 - go/analysis/passes/testinggoroutine/util.go | 2 +- .../passes/tests/testdata/src/a/go118_test.go | 3 - .../testdata/src/typeparams/typeparams.go | 2 - .../src/typeparams/userdefs/userdefs.go | 4 +- go/analysis/unitchecker/separate_test.go | 2 - go/analysis/unitchecker/unitchecker_test.go | 2 - go/callgraph/rta/rta.go | 3 +- go/callgraph/vta/vta_go117_test.go | 33 -- go/callgraph/vta/vta_test.go | 17 + go/packages/packages_test.go | 3 - go/ssa/builder_test.go | 4 - go/ssa/interp/interp_test.go | 4 - go/types/internal/play/play.go | 2 - go/types/objectpath/objectpath_go118_test.go | 3 - godoc/godoc.go | 9 +- godoc/tohtml_go119.go | 17 - godoc/tohtml_other.go | 17 - gopls/api-diff/api_diff.go | 3 - gopls/go.mod | 2 +- .../analysis/deprecated/deprecated_test.go | 2 - .../testdata/src/typeparams/typeparams.go | 3 - .../src/typeparams/typeparams.go.golden | 3 - gopls/internal/cache/port_test.go | 6 +- gopls/internal/cmd/serve.go | 5 +- gopls/internal/golang/comment.go | 395 ++---------------- gopls/internal/golang/comment_go118_test.go | 371 ---------------- gopls/internal/golang/comment_go119.go | 54 --- gopls/internal/golang/origin.go | 20 +- gopls/internal/golang/origin_119.go | 33 -- gopls/internal/golang/rename.go | 23 +- gopls/internal/protocol/generate/generate.go | 3 - gopls/internal/protocol/generate/main.go | 3 - gopls/internal/protocol/generate/main_test.go | 3 - gopls/internal/protocol/generate/output.go | 3 - gopls/internal/protocol/generate/tables.go | 3 - gopls/internal/protocol/generate/typenames.go | 3 - gopls/internal/protocol/generate/types.go | 3 - gopls/internal/protocol/protocol.go | 4 +- gopls/internal/server/command.go | 10 +- gopls/internal/server/general.go | 47 ++- gopls/internal/server/prompt.go | 2 +- gopls/internal/telemetry/telemetry.go | 107 ----- gopls/internal/telemetry/telemetry_go118.go | 42 -- .../completion/completion18_test.go | 3 - .../integration/misc/configuration_test.go | 15 +- .../test/integration/misc/vuln_test.go | 9 +- gopls/internal/test/marker/marker_test.go | 15 +- .../test/marker/testdata/definition/embed.txt | 5 - .../marker/testdata/definition/import.txt | 5 - .../test/marker/testdata/definition/misc.txt | 5 - .../testdata/diagnostics/issue60544.txt | 4 - .../testdata/diagnostics/rundespiteerrors.txt | 3 - .../test/marker/testdata/hover/basiclit.txt | 5 - .../test/marker/testdata/hover/const.txt | 5 - .../test/marker/testdata/hover/godef.txt | 3 + .../test/marker/testdata/hover/goprivate.txt | 5 - .../test/marker/testdata/hover/hover.txt | 5 - .../test/marker/testdata/hover/linkable.txt | 5 - .../testdata/hover/linkable_generics.txt | 5 - .../test/marker/testdata/hover/linkname.txt | 5 - .../test/marker/testdata/hover/std.txt | 5 - .../test/marker/testdata/symbol/generic.txt | 3 - gopls/internal/util/bug/bug.go | 4 +- gopls/internal/util/frob/frob.go | 43 +- gopls/internal/util/lru/lru_fuzz_test.go | 3 - gopls/internal/util/slices/slices.go | 10 +- gopls/internal/vulncheck/scan/command.go | 3 - gopls/internal/vulncheck/semver/semver.go | 3 - .../internal/vulncheck/semver/semver_test.go | 3 - gopls/internal/vulncheck/vulntest/db.go | 3 - gopls/internal/vulncheck/vulntest/db_test.go | 3 - gopls/internal/vulncheck/vulntest/report.go | 3 - .../vulncheck/vulntest/report_test.go | 3 - gopls/internal/vulncheck/vulntest/stdlib.go | 3 - .../vulncheck/vulntest/stdlib_test.go | 3 - gopls/main.go | 4 +- internal/compat/appendf.go | 13 - internal/compat/appendf_118.go | 13 - internal/compat/doc.go | 7 - internal/gcimporter/gcimporter_test.go | 22 +- internal/gcimporter/iexport_go118_test.go | 5 +- internal/gcimporter/support_go117.go | 16 - internal/gcimporter/support_go118.go | 3 - internal/gcimporter/unified_no.go | 4 +- internal/gcimporter/unified_yes.go | 4 +- internal/gcimporter/ureader_no.go | 19 - internal/gcimporter/ureader_yes.go | 3 - internal/imports/mod_test.go | 8 - internal/robustio/robustio_posix.go | 2 - internal/tokeninternal/tokeninternal.go | 28 +- internal/typeparams/common.go | 18 +- internal/typeparams/common_test.go | 5 - internal/typesinternal/types_118.go | 3 - 118 files changed, 186 insertions(+), 1584 deletions(-) delete mode 100644 go/analysis/passes/asmdecl/arches_go118.go delete mode 100644 go/analysis/passes/asmdecl/arches_go119.go delete mode 100644 go/analysis/passes/nilness/nilness_go117_test.go delete mode 100644 go/callgraph/vta/vta_go117_test.go delete mode 100644 godoc/tohtml_go119.go delete mode 100644 godoc/tohtml_other.go delete mode 100644 gopls/internal/golang/comment_go118_test.go delete mode 100644 gopls/internal/golang/comment_go119.go delete mode 100644 gopls/internal/golang/origin_119.go delete mode 100644 gopls/internal/telemetry/telemetry.go delete mode 100644 gopls/internal/telemetry/telemetry_go118.go delete mode 100644 internal/compat/appendf.go delete mode 100644 internal/compat/appendf_118.go delete mode 100644 internal/compat/doc.go delete mode 100644 internal/gcimporter/support_go117.go delete mode 100644 internal/gcimporter/ureader_no.go diff --git a/cmd/bisect/main_test.go b/cmd/bisect/main_test.go index 7c10ff0fb4b..bff1bf23c0c 100644 --- a/cmd/bisect/main_test.go +++ b/cmd/bisect/main_test.go @@ -17,7 +17,6 @@ import ( "testing" "golang.org/x/tools/internal/bisect" - "golang.org/x/tools/internal/compat" "golang.org/x/tools/internal/diffp" "golang.org/x/tools/txtar" ) @@ -82,7 +81,7 @@ func Test(t *testing.T) { have[color] = true } if m.ShouldReport(uint64(i)) { - out = compat.Appendf(out, "%s %s\n", color, bisect.Marker(uint64(i))) + out = fmt.Appendf(out, "%s %s\n", color, bisect.Marker(uint64(i))) } } err = nil diff --git a/copyright/copyright.go b/copyright/copyright.go index b13b56e85f1..556c6e4f69a 100644 --- a/copyright/copyright.go +++ b/copyright/copyright.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.18 -// +build go1.18 - // Package copyright checks that files have the correct copyright notices. package copyright diff --git a/copyright/copyright_test.go b/copyright/copyright_test.go index 7f7892524f5..947fb10c1d1 100644 --- a/copyright/copyright_test.go +++ b/copyright/copyright_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.18 -// +build go1.18 - package copyright import ( diff --git a/go.mod b/go.mod index c8e522063d4..9458c1500cc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module golang.org/x/tools -go 1.18 +go 1.19 require ( github.com/yuin/goldmark v1.4.13 diff --git a/go/analysis/passes/asmdecl/arches_go118.go b/go/analysis/passes/asmdecl/arches_go118.go deleted file mode 100644 index d8211afdc8d..00000000000 --- a/go/analysis/passes/asmdecl/arches_go118.go +++ /dev/null @@ -1,12 +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 !go1.19 -// +build !go1.19 - -package asmdecl - -func additionalArches() []*asmArch { - return nil -} diff --git a/go/analysis/passes/asmdecl/arches_go119.go b/go/analysis/passes/asmdecl/arches_go119.go deleted file mode 100644 index 3018383e7f2..00000000000 --- a/go/analysis/passes/asmdecl/arches_go119.go +++ /dev/null @@ -1,14 +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 go1.19 -// +build go1.19 - -package asmdecl - -var asmArchLoong64 = asmArch{name: "loong64", bigEndian: false, stack: "R3", lr: true} - -func additionalArches() []*asmArch { - return []*asmArch{&asmArchLoong64} -} diff --git a/go/analysis/passes/asmdecl/asmdecl.go b/go/analysis/passes/asmdecl/asmdecl.go index e24dac9865a..f2ca95aa9eb 100644 --- a/go/analysis/passes/asmdecl/asmdecl.go +++ b/go/analysis/passes/asmdecl/asmdecl.go @@ -96,6 +96,7 @@ var ( asmArchRISCV64 = asmArch{name: "riscv64", bigEndian: false, stack: "SP", lr: true, retRegs: []string{"X10", "F10"}} asmArchS390X = asmArch{name: "s390x", bigEndian: true, stack: "R15", lr: true} asmArchWasm = asmArch{name: "wasm", bigEndian: false, stack: "SP", lr: false} + asmArchLoong64 = asmArch{name: "loong64", bigEndian: false, stack: "R3", lr: true} arches = []*asmArch{ &asmArch386, @@ -111,11 +112,11 @@ var ( &asmArchRISCV64, &asmArchS390X, &asmArchWasm, + &asmArchLoong64, } ) func init() { - arches = append(arches, additionalArches()...) for _, arch := range arches { arch.sizes = types.SizesFor("gc", arch.name) if arch.sizes == nil { diff --git a/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go b/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go index 345db277933..fc80410e78a 100644 --- a/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go @@ -4,8 +4,6 @@ // This file contains tests for the useless-assignment checker. -//go:build go1.18 - package testdata import "math/rand" diff --git a/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go.golden b/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go.golden index d9384ed5aab..8c8c4b61f5c 100644 --- a/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go.golden +++ b/go/analysis/passes/assign/testdata/src/typeparams/typeparams.go.golden @@ -4,8 +4,6 @@ // This file contains tests for the useless-assignment checker. -//go:build go1.18 - package testdata import "math/rand" diff --git a/go/analysis/passes/bools/testdata/src/typeparams/typeparams.go b/go/analysis/passes/bools/testdata/src/typeparams/typeparams.go index 718462593a1..3afb56a5d0c 100644 --- a/go/analysis/passes/bools/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/bools/testdata/src/typeparams/typeparams.go @@ -4,8 +4,6 @@ // This file contains tests for the bool checker. -//go:build go1.18 - package typeparams type T[P interface{ ~int }] struct { diff --git a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go index 20b652e88dd..00cbd70051e 100644 --- a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go +++ b/go/analysis/passes/composite/testdata/src/a/a_fuzz_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.18 -// +build go1.18 - package a import "testing" diff --git a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden index 20b652e88dd..00cbd70051e 100644 --- a/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden +++ b/go/analysis/passes/composite/testdata/src/a/a_fuzz_test.go.golden @@ -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.18 -// +build go1.18 - package a import "testing" diff --git a/go/analysis/passes/httpresponse/testdata/src/typeparams/typeparams.go b/go/analysis/passes/httpresponse/testdata/src/typeparams/typeparams.go index 65dd58c7f8a..b2515c950ba 100644 --- a/go/analysis/passes/httpresponse/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/httpresponse/testdata/src/typeparams/typeparams.go @@ -4,8 +4,6 @@ // This file contains tests for the httpresponse checker. -//go:build go1.18 - package typeparams import ( diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index c8aab4834d3..683f91e7b73 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go @@ -33,8 +33,6 @@ func TestVersions22(t *testing.T) { } func TestVersions18(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - testfile := filepath.Join(analysistest.TestData(), "src", "versions", "go18.txtar") runTxtarFile(t, testfile, loopclosure.Analyzer, "golang.org/fake/versions") } diff --git a/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go b/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go index ef5b143f6da..85976873b9a 100644 --- a/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/loopclosure/testdata/src/typeparams/typeparams.go @@ -5,7 +5,7 @@ // This file contains legacy tests for the loopclosure checker for GoVersion 0 { return tool.CommandLineErrorf("server does not take arguments, got %v", args) diff --git a/gopls/internal/golang/comment.go b/gopls/internal/golang/comment.go index ece123801ff..95f0df98293 100644 --- a/gopls/internal/golang/comment.go +++ b/gopls/internal/golang/comment.go @@ -1,19 +1,12 @@ -// Copyright 2019 The Go Authors. All rights reserved. +// 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 !go1.19 -// +build !go1.19 - package golang import ( - "bytes" - "io" - "regexp" - "strings" - "unicode" - "unicode/utf8" + "fmt" + "go/doc/comment" "golang.org/x/tools/gopls/internal/settings" ) @@ -23,364 +16,26 @@ import ( // so it is known not to have leading, trailing blank lines // nor to have trailing spaces at the end of lines. // The comment markers have already been removed. -// -// Each line is converted into a markdown line and empty lines are just converted to -// newlines. Heading are prefixed with `### ` to make it a markdown heading. -// -// A span of indented lines retains a 4 space prefix block, with the common indent -// prefix removed unless empty, in which case it will be converted to a newline. -// -// URLs in the comment text are converted into links. -func CommentToMarkdown(text string, _ *settings.Options) string { - buf := &bytes.Buffer{} - commentToMarkdown(buf, text) - return buf.String() -} - -var ( - mdNewline = []byte("\n") - mdHeader = []byte("### ") - mdIndent = []byte(" ") - mdLinkStart = []byte("[") - mdLinkDiv = []byte("](") - mdLinkEnd = []byte(")") -) - -func commentToMarkdown(w io.Writer, text string) { - blocks := blocks(text) - for i, b := range blocks { - switch b.op { - case opPara: - for _, line := range b.lines { - emphasize(w, line, true) - } - case opHead: - // The header block can consist of only one line. - // However, check the number of lines, just in case. - if len(b.lines) == 0 { - // Skip this block. - continue - } - header := b.lines[0] - - w.Write(mdHeader) - commentEscape(w, header, true) - // Header doesn't end with \n unlike the lines of other blocks. - w.Write(mdNewline) - case opPre: - for _, line := range b.lines { - if isBlank(line) { - w.Write(mdNewline) - continue - } - w.Write(mdIndent) - w.Write([]byte(line)) - } - } - - if i < len(blocks)-1 { - w.Write(mdNewline) - } - } -} - -const ( - ulquo = "“" - urquo = "”" -) - -var ( - markdownEscape = regexp.MustCompile(`([\\\x60*{}[\]()#+\-.!_>~|"$%&'\/:;<=?@^])`) - - unicodeQuoteReplacer = strings.NewReplacer("``", ulquo, "''", urquo) -) - -// commentEscape escapes comment text for markdown. If nice is set, -// also turn double ` and ' into “ and ”. -func commentEscape(w io.Writer, text string, nice bool) { - if nice { - text = convertQuotes(text) - } - text = escapeRegex(text) - w.Write([]byte(text)) -} - -func convertQuotes(text string) string { - return unicodeQuoteReplacer.Replace(text) -} - -func escapeRegex(text string) string { - return markdownEscape.ReplaceAllString(text, `\$1`) -} - -func emphasize(w io.Writer, line string, nice bool) { - for { - m := matchRx.FindStringSubmatchIndex(line) - if m == nil { - break - } - // m >= 6 (two parenthesized sub-regexps in matchRx, 1st one is urlRx) - - // write text before match - commentEscape(w, line[0:m[0]], nice) - - // adjust match for URLs - match := line[m[0]:m[1]] - if strings.Contains(match, "://") { - m0, m1 := m[0], m[1] - for _, s := range []string{"()", "{}", "[]"} { - open, close := s[:1], s[1:] // E.g., "(" and ")" - // require opening parentheses before closing parentheses (#22285) - if i := strings.Index(match, close); i >= 0 && i < strings.Index(match, open) { - m1 = m0 + i - match = line[m0:m1] - } - // require balanced pairs of parentheses (#5043) - for i := 0; strings.Count(match, open) != strings.Count(match, close) && i < 10; i++ { - m1 = strings.LastIndexAny(line[:m1], s) - match = line[m0:m1] - } - } - if m1 != m[1] { - // redo matching with shortened line for correct indices - m = matchRx.FindStringSubmatchIndex(line[:m[0]+len(match)]) - } - } - - // Following code has been modified from go/doc since words is always - // nil. All html formatting has also been transformed into markdown formatting - - // analyze match - url := "" - if m[2] >= 0 { - url = match - } - - // write match - if len(url) > 0 { - w.Write(mdLinkStart) - } - - commentEscape(w, match, nice) - - if len(url) > 0 { - w.Write(mdLinkDiv) - w.Write([]byte(urlReplacer.Replace(url))) - w.Write(mdLinkEnd) - } - - // advance - line = line[m[1]:] - } - commentEscape(w, line, nice) -} - -// Everything from here on is a copy of go/doc/comment.go - -const ( - // Regexp for Go identifiers - identRx = `[\pL_][\pL_0-9]*` - - // Regexp for URLs - // Match parens, and check later for balance - see #5043, #22285 - // Match .,:;?! within path, but not at end - see #18139, #16565 - // This excludes some rare yet valid urls ending in common punctuation - // in order to allow sentences ending in URLs. - - // protocol (required) e.g. http - protoPart = `(https?|ftp|file|gopher|mailto|nntp)` - // host (required) e.g. www.example.com or [::1]:8080 - hostPart = `([a-zA-Z0-9_@\-.\[\]:]+)` - // path+query+fragment (optional) e.g. /path/index.html?q=foo#bar - pathPart = `([.,:;?!]*[a-zA-Z0-9$'()*+&#=@~_/\-\[\]%])*` - - urlRx = protoPart + `://` + hostPart + pathPart -) - -var ( - matchRx = regexp.MustCompile(`(` + urlRx + `)|(` + identRx + `)`) - urlReplacer = strings.NewReplacer(`(`, `\(`, `)`, `\)`) -) - -func indentLen(s string) int { - i := 0 - for i < len(s) && (s[i] == ' ' || s[i] == '\t') { - i++ - } - return i -} - -func isBlank(s string) bool { - return len(s) == 0 || (len(s) == 1 && s[0] == '\n') -} - -func commonPrefix(a, b string) string { - i := 0 - for i < len(a) && i < len(b) && a[i] == b[i] { - i++ - } - return a[0:i] -} - -func unindent(block []string) { - if len(block) == 0 { - return - } - - // compute maximum common white prefix - prefix := block[0][0:indentLen(block[0])] - for _, line := range block { - if !isBlank(line) { - prefix = commonPrefix(prefix, line) - } - } - n := len(prefix) - - // remove - for i, line := range block { - if !isBlank(line) { - block[i] = line[n:] - } - } -} - -// heading returns the trimmed line if it passes as a section heading; -// otherwise it returns the empty string. -func heading(line string) string { - line = strings.TrimSpace(line) - if len(line) == 0 { - return "" - } - - // a heading must start with an uppercase letter - r, _ := utf8.DecodeRuneInString(line) - if !unicode.IsLetter(r) || !unicode.IsUpper(r) { - return "" - } - - // it must end in a letter or digit: - r, _ = utf8.DecodeLastRuneInString(line) - if !unicode.IsLetter(r) && !unicode.IsDigit(r) { - return "" - } - - // exclude lines with illegal characters. we allow "()," - if strings.ContainsAny(line, ";:!?+*/=[]{}_^°&§~%#@<\">\\") { - return "" - } - - // allow "'" for possessive "'s" only - for b := line; ; { - i := strings.IndexRune(b, '\'') - if i < 0 { - break - } - if i+1 >= len(b) || b[i+1] != 's' || (i+2 < len(b) && b[i+2] != ' ') { - return "" // not followed by "s " - } - b = b[i+2:] - } - - // allow "." when followed by non-space - for b := line; ; { - i := strings.IndexRune(b, '.') - if i < 0 { - break - } - if i+1 >= len(b) || b[i+1] == ' ' { - return "" // not followed by non-space - } - b = b[i+1:] - } - - return line -} - -type op int - -const ( - opPara op = iota - opHead - opPre -) - -type block struct { - op op - lines []string -} - -func blocks(text string) []block { - var ( - out []block - para []string - - lastWasBlank = false - lastWasHeading = false - ) - - close := func() { - if para != nil { - out = append(out, block{opPara, para}) - para = nil - } - } - - lines := strings.SplitAfter(text, "\n") - unindent(lines) - for i := 0; i < len(lines); { - line := lines[i] - if isBlank(line) { - // close paragraph - close() - i++ - lastWasBlank = true - continue - } - if indentLen(line) > 0 { - // close paragraph - close() - - // count indented or blank lines - j := i + 1 - for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) { - j++ - } - // but not trailing blank lines - for j > i && isBlank(lines[j-1]) { - j-- - } - pre := lines[i:j] - i = j - - unindent(pre) - - // put those lines in a pre block - out = append(out, block{opPre, pre}) - lastWasHeading = false - continue - } - - if lastWasBlank && !lastWasHeading && i+2 < len(lines) && - isBlank(lines[i+1]) && !isBlank(lines[i+2]) && indentLen(lines[i+2]) == 0 { - // current line is non-blank, surrounded by blank lines - // and the next non-blank line is not indented: this - // might be a heading. - if head := heading(line); head != "" { - close() - out = append(out, block{opHead, []string{head}}) - i += 2 - lastWasHeading = true - continue - } - } - - // open paragraph - lastWasBlank = false - lastWasHeading = false - para = append(para, lines[i]) - i++ - } - close() - - return out +func CommentToMarkdown(text string, options *settings.Options) string { + var p comment.Parser + doc := p.Parse(text) + var pr comment.Printer + // The default produces {#Hdr-...} tags for headings. + // vscode displays thems, which is undesirable. + // The godoc for comment.Printer says the tags + // avoid a security problem. + pr.HeadingID = func(*comment.Heading) string { return "" } + pr.DocLinkURL = func(link *comment.DocLink) string { + msg := fmt.Sprintf("https://%s/%s", options.LinkTarget, link.ImportPath) + if link.Name != "" { + msg += "#" + if link.Recv != "" { + msg += link.Recv + "." + } + msg += link.Name + } + return msg + } + easy := pr.Markdown(doc) + return string(easy) } diff --git a/gopls/internal/golang/comment_go118_test.go b/gopls/internal/golang/comment_go118_test.go deleted file mode 100644 index c38898a28e6..00000000000 --- a/gopls/internal/golang/comment_go118_test.go +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !go1.19 -// +build !go1.19 - -package golang - -import ( - "bytes" - "reflect" - "strings" - "testing" -) - -// This file is a copy of go/doc/comment_test.go with the exception for -// the test cases for TestEmphasize and TestCommentEscape - -var headingTests = []struct { - line string - ok bool -}{ - {"Section", true}, - {"A typical usage", true}, - {"ΔΛΞ is Greek", true}, - {"Foo 42", true}, - {"", false}, - {"section", false}, - {"A typical usage:", false}, - {"This code:", false}, - {"δ is Greek", false}, - {"Foo §", false}, - {"Fermat's Last Sentence", true}, - {"Fermat's", true}, - {"'sX", false}, - {"Ted 'Too' Bar", false}, - {"Use n+m", false}, - {"Scanning:", false}, - {"N:M", false}, -} - -func TestIsHeading(t *testing.T) { - for _, tt := range headingTests { - if h := heading(tt.line); (len(h) > 0) != tt.ok { - t.Errorf("isHeading(%q) = %v, want %v", tt.line, h, tt.ok) - } - } -} - -var blocksTests = []struct { - in string - out []block - text string -}{ - { - in: `Para 1. -Para 1 line 2. - -Para 2. - -Section - -Para 3. - - pre - pre1 - -Para 4. - - pre - pre1 - - pre2 - -Para 5. - - - pre - - - pre1 - pre2 - -Para 6. - pre - pre2 -`, - out: []block{ - {opPara, []string{"Para 1.\n", "Para 1 line 2.\n"}}, - {opPara, []string{"Para 2.\n"}}, - {opHead, []string{"Section"}}, - {opPara, []string{"Para 3.\n"}}, - {opPre, []string{"pre\n", "pre1\n"}}, - {opPara, []string{"Para 4.\n"}}, - {opPre, []string{"pre\n", "pre1\n", "\n", "pre2\n"}}, - {opPara, []string{"Para 5.\n"}}, - {opPre, []string{"pre\n", "\n", "\n", "pre1\n", "pre2\n"}}, - {opPara, []string{"Para 6.\n"}}, - {opPre, []string{"pre\n", "pre2\n"}}, - }, - text: `. Para 1. Para 1 line 2. - -. Para 2. - - -. Section - -. Para 3. - -$ pre -$ pre1 - -. Para 4. - -$ pre -$ pre1 - -$ pre2 - -. Para 5. - -$ pre - - -$ pre1 -$ pre2 - -. Para 6. - -$ pre -$ pre2 -`, - }, - { - in: "Para.\n\tshould not be ``escaped''", - out: []block{ - {opPara, []string{"Para.\n"}}, - {opPre, []string{"should not be ``escaped''"}}, - }, - text: ". Para.\n\n$ should not be ``escaped''", - }, - { - in: "// A very long line of 46 char for line wrapping.", - out: []block{ - {opPara, []string{"// A very long line of 46 char for line wrapping."}}, - }, - text: `. // A very long line of 46 char for line -. // wrapping. -`, - }, - { - in: `/* A very long line of 46 char for line wrapping. -A very long line of 46 char for line wrapping. */`, - out: []block{ - {opPara, []string{"/* A very long line of 46 char for line wrapping.\n", "A very long line of 46 char for line wrapping. */"}}, - }, - text: `. /* A very long line of 46 char for line -. wrapping. A very long line of 46 char -. for line wrapping. */ -`, - }, -} - -func TestBlocks(t *testing.T) { - for i, tt := range blocksTests { - b := blocks(tt.in) - if !reflect.DeepEqual(b, tt.out) { - t.Errorf("#%d: mismatch\nhave: %v\nwant: %v", i, b, tt.out) - } - } -} - -// This has been modified from go/doc to use markdown links instead of html ones -// and use markdown escaping instead oh html -var emphasizeTests = []struct { - in, out string -}{ - {"", ""}, - {"http://[::1]:8080/foo.txt", `[http\:\/\/\[\:\:1\]\:8080\/foo\.txt](http://[::1]:8080/foo.txt)`}, - {"before (https://www.google.com) after", `before \([https\:\/\/www\.google\.com](https://www.google.com)\) after`}, - {"before https://www.google.com:30/x/y/z:b::c. After", `before [https\:\/\/www\.google\.com\:30\/x\/y\/z\:b\:\:c](https://www.google.com:30/x/y/z:b::c)\. After`}, - {"http://www.google.com/path/:;!-/?query=%34b#093124", `[http\:\/\/www\.google\.com\/path\/\:\;\!\-\/\?query\=\%34b\#093124](http://www.google.com/path/:;!-/?query=%34b#093124)`}, - {"http://www.google.com/path/:;!-/?query=%34bar#093124", `[http\:\/\/www\.google\.com\/path\/\:\;\!\-\/\?query\=\%34bar\#093124](http://www.google.com/path/:;!-/?query=%34bar#093124)`}, - {"http://www.google.com/index.html! After", `[http\:\/\/www\.google\.com\/index\.html](http://www.google.com/index.html)\! After`}, - {"http://www.google.com/", `[http\:\/\/www\.google\.com\/](http://www.google.com/)`}, - {"https://www.google.com/", `[https\:\/\/www\.google\.com\/](https://www.google.com/)`}, - {"http://www.google.com/path.", `[http\:\/\/www\.google\.com\/path](http://www.google.com/path)\.`}, - {"http://en.wikipedia.org/wiki/Camellia_(cipher)", `[http\:\/\/en\.wikipedia\.org\/wiki\/Camellia\_\(cipher\)](http://en.wikipedia.org/wiki/Camellia_\(cipher\))`}, - {"(http://www.google.com/)", `\([http\:\/\/www\.google\.com\/](http://www.google.com/)\)`}, - {"http://gmail.com)", `[http\:\/\/gmail\.com](http://gmail.com)\)`}, - {"((http://gmail.com))", `\(\([http\:\/\/gmail\.com](http://gmail.com)\)\)`}, - {"http://gmail.com ((http://gmail.com)) ()", `[http\:\/\/gmail\.com](http://gmail.com) \(\([http\:\/\/gmail\.com](http://gmail.com)\)\) \(\)`}, - {"Foo bar http://example.com/ quux!", `Foo bar [http\:\/\/example\.com\/](http://example.com/) quux\!`}, - {"Hello http://example.com/%2f/ /world.", `Hello [http\:\/\/example\.com\/\%2f\/](http://example.com/%2f/) \/world\.`}, - {"Lorem http: ipsum //host/path", `Lorem http\: ipsum \/\/host\/path`}, - {"javascript://is/not/linked", `javascript\:\/\/is\/not\/linked`}, - {"http://foo", `[http\:\/\/foo](http://foo)`}, - {"art by [[https://www.example.com/person/][Person Name]]", `art by \[\[[https\:\/\/www\.example\.com\/person\/](https://www.example.com/person/)\]\[Person Name\]\]`}, - {"please visit (http://golang.org/)", `please visit \([http\:\/\/golang\.org\/](http://golang.org/)\)`}, - {"please visit http://golang.org/hello())", `please visit [http\:\/\/golang\.org\/hello\(\)](http://golang.org/hello\(\))\)`}, - {"http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD", `[http\:\/\/git\.qemu\.org\/\?p\=qemu\.git\;a\=blob\;f\=qapi\-schema\.json\;hb\=HEAD](http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD)`}, - {"https://foo.bar/bal/x(])", `[https\:\/\/foo\.bar\/bal\/x\(](https://foo.bar/bal/x\()\]\)`}, - {"foo [ http://bar(])", `foo \[ [http\:\/\/bar\(](http://bar\()\]\)`}, -} - -func TestEmphasize(t *testing.T) { - for i, tt := range emphasizeTests { - var buf bytes.Buffer - emphasize(&buf, tt.in, true) - out := buf.String() - if out != tt.out { - t.Errorf("#%d: mismatch\nhave: %v\nwant: %v", i, out, tt.out) - } - } -} - -func TestCommentEscape(t *testing.T) { - //ldquo -> ulquo and rdquo -> urquo - commentTests := []struct { - in, out string - }{ - {"typically invoked as ``go tool asm'',", "typically invoked as " + ulquo + "go tool asm" + urquo + ","}, - {"For more detail, run ``go help test'' and ``go help testflag''", "For more detail, run " + ulquo + "go help test" + urquo + " and " + ulquo + "go help testflag" + urquo}} - for i, tt := range commentTests { - var buf strings.Builder - commentEscape(&buf, tt.in, true) - out := buf.String() - if out != tt.out { - t.Errorf("#%d: mismatch\nhave: %q\nwant: %q", i, out, tt.out) - } - } -} - -func TestCommentToMarkdown(t *testing.T) { - tests := []struct { - in, out string - }{ - { - in: "F declaration.\n", - out: "F declaration\\.\n", - }, - { - in: ` -F declaration. Lorem ipsum dolor sit amet. -Etiam mattis eros at orci mollis molestie. -`, - out: ` -F declaration\. Lorem ipsum dolor sit amet\. -Etiam mattis eros at orci mollis molestie\. -`, - }, - { - in: ` -F declaration. - -Lorem ipsum dolor sit amet. -Sed id dui turpis. - - - - -Aenean tempus velit non auctor eleifend. -Aenean efficitur a sem id ultricies. - - -Phasellus efficitur mauris et viverra bibendum. -`, - out: ` -F declaration\. - -Lorem ipsum dolor sit amet\. -Sed id dui turpis\. - -Aenean tempus velit non auctor eleifend\. -Aenean efficitur a sem id ultricies\. - -Phasellus efficitur mauris et viverra bibendum\. -`, - }, - { - in: ` -F declaration. - -Aenean tempus velit non auctor eleifend. - -Section - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. - - func foo() {} - - - func bar() {} - -Fusce lorem lacus. - - func foo() {} - - func bar() {} - -Maecenas in lobortis lectus. - - func foo() {} - - func bar() {} - -Phasellus efficitur mauris et viverra bibendum. -`, - out: ` -F declaration\. - -Aenean tempus velit non auctor eleifend\. - -### Section - -Lorem ipsum dolor sit amet, consectetur adipiscing elit\. - - func foo() {} - - - func bar() {} - -Fusce lorem lacus\. - - func foo() {} - - func bar() {} - -Maecenas in lobortis lectus\. - - func foo() {} - - func bar() {} - -Phasellus efficitur mauris et viverra bibendum\. -`, - }, - { - in: ` -F declaration. - - func foo() { - fmt.Println("foo") - } - func bar() { - fmt.Println("bar") - } -`, - out: ` -F declaration\. - - func foo() { - fmt.Println("foo") - } - func bar() { - fmt.Println("bar") - } -`, - }, - } - for i, tt := range tests { - // Comments start with new lines for better readability. So, we should trim them. - tt.in = strings.TrimPrefix(tt.in, "\n") - tt.out = strings.TrimPrefix(tt.out, "\n") - - if out := CommentToMarkdown(tt.in, nil); out != tt.out { - t.Errorf("#%d: mismatch\nhave: %q\nwant: %q", i, out, tt.out) - } - } -} diff --git a/gopls/internal/golang/comment_go119.go b/gopls/internal/golang/comment_go119.go deleted file mode 100644 index eec338d54fa..00000000000 --- a/gopls/internal/golang/comment_go119.go +++ /dev/null @@ -1,54 +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 go1.19 -// +build go1.19 - -package golang - -// Starting with go1.19, the formatting of comments has changed, and there -// is a new package (go/doc/comment) for processing them. -// As long as gopls has to compile under earlier versions, tests -// have to pass with both the old and new code, which produce -// slightly different results. - -// When gopls no longer needs to compile with go1.18, the old comment.go should -// be replaced by this file, the golden test files should be updated. -// (and checkSameMarkdown() could be replaced by a simple comparison.) - -import ( - "fmt" - "go/doc/comment" - - "golang.org/x/tools/gopls/internal/settings" -) - -// CommentToMarkdown converts comment text to formatted markdown. -// The comment was prepared by DocReader, -// so it is known not to have leading, trailing blank lines -// nor to have trailing spaces at the end of lines. -// The comment markers have already been removed. -func CommentToMarkdown(text string, options *settings.Options) string { - var p comment.Parser - doc := p.Parse(text) - var pr comment.Printer - // The default produces {#Hdr-...} tags for headings. - // vscode displays thems, which is undesirable. - // The godoc for comment.Printer says the tags - // avoid a security problem. - pr.HeadingID = func(*comment.Heading) string { return "" } - pr.DocLinkURL = func(link *comment.DocLink) string { - msg := fmt.Sprintf("https://%s/%s", options.LinkTarget, link.ImportPath) - if link.Name != "" { - msg += "#" - if link.Recv != "" { - msg += link.Recv + "." - } - msg += link.Name - } - return msg - } - easy := pr.Markdown(doc) - return string(easy) -} diff --git a/gopls/internal/golang/origin.go b/gopls/internal/golang/origin.go index c5e84db0ceb..aa77a9b3aa4 100644 --- a/gopls/internal/golang/origin.go +++ b/gopls/internal/golang/origin.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.19 -// +build !go1.19 - package golang import "go/types" @@ -13,14 +10,21 @@ import "go/types" // with the same origin as the provided obj (which may be a synthetic object // created during instantiation). func containsOrigin(objSet map[types.Object]bool, obj types.Object) bool { - if obj == nil { - return objSet[obj] - } - // In Go 1.18, we can't use the types.Var.Origin and types.Func.Origin methods. + objOrigin := origin(obj) for target := range objSet { - if target.Pkg() == obj.Pkg() && target.Pos() == obj.Pos() && target.Name() == obj.Name() { + if origin(target) == objOrigin { return true } } return false } + +func origin(obj types.Object) types.Object { + switch obj := obj.(type) { + case *types.Var: + return obj.Origin() + case *types.Func: + return obj.Origin() + } + return obj +} diff --git a/gopls/internal/golang/origin_119.go b/gopls/internal/golang/origin_119.go deleted file mode 100644 index 16f6ca7c065..00000000000 --- a/gopls/internal/golang/origin_119.go +++ /dev/null @@ -1,33 +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. - -//go:build go1.19 -// +build go1.19 - -package golang - -import "go/types" - -// containsOrigin reports whether the provided object set contains an object -// with the same origin as the provided obj (which may be a synthetic object -// created during instantiation). -func containsOrigin(objSet map[types.Object]bool, obj types.Object) bool { - objOrigin := origin(obj) - for target := range objSet { - if origin(target) == objOrigin { - return true - } - } - return false -} - -func origin(obj types.Object) types.Object { - switch obj := obj.(type) { - case *types.Var: - return obj.Origin() - case *types.Func: - return obj.Origin() - } - return obj -} diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index 81114fc6a09..8c40079b2f3 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -68,7 +68,6 @@ import ( "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/typeparams" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/refactor/satisfy" ) @@ -347,8 +346,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle var declObjPath objectpath.Path if obj.Exported() { // objectpath.For requires the origin of a generic function or type, not an - // instantiation (a bug?). Unfortunately we can't call Func.Origin as this - // is not available in go/types@go1.18. So we take a scenic route. + // instantiation (a bug?). // // Note that unlike Funcs, TypeNames are always canonical (they are "left" // of the type parameters, unlike methods). @@ -360,7 +358,7 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle goto skipObjectPath } case *types.Func: - obj = funcOrigin(obj.(*types.Func)) + obj = obj.(*types.Func).Origin() case *types.Var: // TODO(adonovan): do vars need the origin treatment too? (issue #58462) @@ -451,23 +449,6 @@ func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle return renameExported(pkgs, declPkgPath, declObjPath, newName) } -// funcOrigin is a go1.18-portable implementation of (*types.Func).Origin. -func funcOrigin(fn *types.Func) *types.Func { - // Method? - if fn.Type().(*types.Signature).Recv() != nil { - return typeparams.OriginMethod(fn) - } - - // Package-level function? - // (Assume the origin has the same position.) - gen := fn.Pkg().Scope().Lookup(fn.Name()) - if gen != nil && gen.Pos() == fn.Pos() { - return gen.(*types.Func) - } - - return fn -} - // typeCheckReverseDependencies returns the type-checked packages for // the reverse dependencies of all packages variants containing // file declURI. The packages are in some topological order. diff --git a/gopls/internal/protocol/generate/generate.go b/gopls/internal/protocol/generate/generate.go index 0496b7d060c..7418918f51f 100644 --- a/gopls/internal/protocol/generate/generate.go +++ b/gopls/internal/protocol/generate/generate.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.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/generate/main.go b/gopls/internal/protocol/generate/main.go index f70c5810d6c..bdc2728e2a9 100644 --- a/gopls/internal/protocol/generate/main.go +++ b/gopls/internal/protocol/generate/main.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.19 -// +build go1.19 - // The generate command generates Go declarations from VSCode's // description of the Language Server Protocol. // diff --git a/gopls/internal/protocol/generate/main_test.go b/gopls/internal/protocol/generate/main_test.go index 5f336690687..73c22048a80 100644 --- a/gopls/internal/protocol/generate/main_test.go +++ b/gopls/internal/protocol/generate/main_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.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/generate/output.go b/gopls/internal/protocol/generate/output.go index fc64677ff8e..47608626b82 100644 --- a/gopls/internal/protocol/generate/output.go +++ b/gopls/internal/protocol/generate/output.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.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/generate/tables.go b/gopls/internal/protocol/generate/tables.go index ac428b58479..a9207bfc9a3 100644 --- a/gopls/internal/protocol/generate/tables.go +++ b/gopls/internal/protocol/generate/tables.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.19 -// +build go1.19 - package main import "log" diff --git a/gopls/internal/protocol/generate/typenames.go b/gopls/internal/protocol/generate/typenames.go index 8bacdd2a1cf..83f25a010a0 100644 --- a/gopls/internal/protocol/generate/typenames.go +++ b/gopls/internal/protocol/generate/typenames.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.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/generate/types.go b/gopls/internal/protocol/generate/types.go index 0d01ae43cb1..0537748eb5b 100644 --- a/gopls/internal/protocol/generate/types.go +++ b/gopls/internal/protocol/generate/types.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.19 -// +build go1.19 - package main import ( diff --git a/gopls/internal/protocol/protocol.go b/gopls/internal/protocol/protocol.go index 09bfaca2e86..7cc5589aa0b 100644 --- a/gopls/internal/protocol/protocol.go +++ b/gopls/internal/protocol/protocol.go @@ -11,7 +11,7 @@ import ( "fmt" "io" - "golang.org/x/tools/gopls/internal/telemetry" + "golang.org/x/telemetry/crashmonitor" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" @@ -302,7 +302,7 @@ func recoverHandlerPanic(method string) { // Report panics in the handler goroutine, // unless we have enabled the monitor, // which reports all crashes. - if !telemetry.CrashMonitorSupported() { + if !crashmonitor.Supported() { defer func() { if x := recover(); x != nil { bug.Reportf("panic in %s request", method) diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 19ea884f45d..3f5d53c53a2 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -21,6 +21,7 @@ import ( "sync" "golang.org/x/mod/modfile" + "golang.org/x/telemetry/counter" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" @@ -32,7 +33,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/telemetry" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/gopls/internal/vulncheck/scan" @@ -80,7 +80,13 @@ func (*commandHandler) AddTelemetryCounters(_ context.Context, args command.AddT return fmt.Errorf("Names and Values must have the same length") } // invalid counter update requests will be silently dropped. (no audience) - telemetry.AddForwardedCounters(args.Names, args.Values) + for i, n := range args.Names { + v := args.Values[i] + if n == "" || v < 0 { + continue + } + counter.Add("fwd/"+n, v) + } return nil } diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index cdaee1b973b..9ead4705cea 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go @@ -20,12 +20,12 @@ import ( "strings" "sync" + "golang.org/x/telemetry/counter" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" "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/telemetry" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/goversion" "golang.org/x/tools/gopls/internal/util/maps" @@ -41,7 +41,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitializ if params != nil && params.ClientInfo != nil { clientName = params.ClientInfo.Name } - telemetry.RecordClientInfo(clientName) + recordClientInfo(clientName) s.stateMu.Lock() if s.state >= serverInitializing { @@ -249,7 +249,9 @@ func (s *server) checkViewGoVersions() { if oldestVersion == -1 || viewVersion < oldestVersion { oldestVersion, fromBuild = viewVersion, false } - telemetry.RecordViewGoVersion(viewVersion) + if viewVersion >= 0 { + counter.Inc(fmt.Sprintf("gopls/goversion:1.%d", viewVersion)) + } } if msg, isError := goversion.Message(oldestVersion, fromBuild); msg != "" { @@ -637,3 +639,42 @@ func (s *server) Exit(ctx context.Context) error { // close naturally if needed after the connection is closed. return nil } + +// recordClientInfo records gopls client info. +func recordClientInfo(clientName string) { + key := "gopls/client:other" + switch clientName { + case "Visual Studio Code": + key = "gopls/client:vscode" + case "Visual Studio Code - Insiders": + key = "gopls/client:vscode-insiders" + case "VSCodium": + key = "gopls/client:vscodium" + case "code-server": + // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19 + key = "gopls/client:code-server" + case "Eglot": + // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html + key = "gopls/client:eglot" + case "govim": + // https://github.com/govim/govim/pull/1189 + key = "gopls/client:govim" + case "Neovim": + // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361 + key = "gopls/client:neovim" + case "coc.nvim": + // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994 + key = "gopls/client:coc.nvim" + case "Sublime Text LSP": + // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493 + key = "gopls/client:sublimetext" + default: + // Accumulate at least a local counter for an unknown + // client name, but also fall through to count it as + // ":other" for collection. + if clientName != "" { + counter.New(fmt.Sprintf("gopls/client-other:%s", clientName)).Inc() + } + } + counter.Inc(key) +} diff --git a/gopls/internal/server/prompt.go b/gopls/internal/server/prompt.go index 72c5113dc4e..7dc16b9a324 100644 --- a/gopls/internal/server/prompt.go +++ b/gopls/internal/server/prompt.go @@ -11,8 +11,8 @@ import ( "path/filepath" "time" + "golang.org/x/telemetry" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/internal/event" ) diff --git a/gopls/internal/telemetry/telemetry.go b/gopls/internal/telemetry/telemetry.go deleted file mode 100644 index 58083992a17..00000000000 --- a/gopls/internal/telemetry/telemetry.go +++ /dev/null @@ -1,107 +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. - -//go:build go1.19 -// +build go1.19 - -package telemetry - -import ( - "fmt" - - "golang.org/x/telemetry" - "golang.org/x/telemetry/counter" - "golang.org/x/telemetry/crashmonitor" - "golang.org/x/telemetry/upload" -) - -// Start starts telemetry, including the crash monitor. -func Start() { - telemetry.Start(telemetry.Config{ReportCrashes: true}) -} - -// CrashMonitorSupported calls [crashmonitor.Supported]. -func CrashMonitorSupported() bool { - return crashmonitor.Supported() -} - -// NewStackCounter calls [counter.NewStack]. -func NewStackCounter(name string, depth int) *counter.StackCounter { - return counter.NewStack(name, depth) -} - -// Mode calls x/telemetry.Mode. -func Mode() string { - return telemetry.Mode() -} - -// SetMode calls x/telemetry.SetMode. -func SetMode(mode string) error { - return telemetry.SetMode(mode) -} - -// Upload starts a goroutine for telemetry upload. -func Upload() { - go upload.Run(nil) -} - -// RecordClientInfo records gopls client info. -func RecordClientInfo(clientName string) { - key := "gopls/client:other" - switch clientName { - case "Visual Studio Code": - key = "gopls/client:vscode" - case "Visual Studio Code - Insiders": - key = "gopls/client:vscode-insiders" - case "VSCodium": - key = "gopls/client:vscodium" - case "code-server": - // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19 - key = "gopls/client:code-server" - case "Eglot": - // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html - key = "gopls/client:eglot" - case "govim": - // https://github.com/govim/govim/pull/1189 - key = "gopls/client:govim" - case "Neovim": - // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361 - key = "gopls/client:neovim" - case "coc.nvim": - // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994 - key = "gopls/client:coc.nvim" - case "Sublime Text LSP": - // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493 - key = "gopls/client:sublimetext" - default: - // Accumulate at least a local counter for an unknown - // client name, but also fall through to count it as - // ":other" for collection. - if clientName != "" { - counter.New(fmt.Sprintf("gopls/client-other:%s", clientName)).Inc() - } - } - counter.Inc(key) -} - -// RecordViewGoVersion records the Go minor version number (1.x) used for a view. -func RecordViewGoVersion(x int) { - if x < 0 { - return - } - name := fmt.Sprintf("gopls/goversion:1.%d", x) - counter.Inc(name) -} - -// AddForwardedCounters adds the given counters on behalf of clients. -// Names and values must have the same length. -func AddForwardedCounters(names []string, values []int64) { - for i, n := range names { - v := values[i] - if n == "" || v < 0 { - continue // Should we report an error? Who is the audience? - } - counter.Add("fwd/"+n, v) - } -} diff --git a/gopls/internal/telemetry/telemetry_go118.go b/gopls/internal/telemetry/telemetry_go118.go deleted file mode 100644 index 3fd1df2939c..00000000000 --- a/gopls/internal/telemetry/telemetry_go118.go +++ /dev/null @@ -1,42 +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. - -//go:build !go1.19 -// +build !go1.19 - -package telemetry - -// This file defines dummy implementations of telemetry operations to -// permit building with go1.18. Until we drop support for go1.18, -// gopls may not refer to the telemetry module directly, but must go -// through this file. - -func Start() {} - -func CrashMonitorSupported() bool { return false } - -func NewStackCounter(string, int) dummyCounter { return dummyCounter{} } - -type dummyCounter struct{} - -func (dummyCounter) Inc() {} - -func Mode() string { - return "local" -} - -func SetMode(mode string) error { - return nil -} - -func Upload() { -} - -func RecordClientInfo(string) {} - -func RecordViewGoVersion(x int) { -} - -func AddForwardedCounters(names []string, values []int64) { -} diff --git a/gopls/internal/test/integration/completion/completion18_test.go b/gopls/internal/test/integration/completion/completion18_test.go index 0ca83778664..a35061d693b 100644 --- a/gopls/internal/test/integration/completion/completion18_test.go +++ b/gopls/internal/test/integration/completion/completion18_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.18 -// +build go1.18 - package completion import ( diff --git a/gopls/internal/test/integration/misc/configuration_test.go b/gopls/internal/test/integration/misc/configuration_test.go index c6ed18041ae..39980f353df 100644 --- a/gopls/internal/test/integration/misc/configuration_test.go +++ b/gopls/internal/test/integration/misc/configuration_test.go @@ -15,7 +15,7 @@ import ( // Test that enabling and disabling produces the expected results of showing // and hiding staticcheck analysis results. func TestChangeConfiguration(t *testing.T) { - // Staticcheck only supports Go versions >= 1.19. + // Staticcheck only supports Go versions >= 1.20. // Note: keep this in sync with TestStaticcheckWarning. Below this version we // should get an error when setting staticcheck configuration. testenv.NeedsGo1Point(t, 20) @@ -164,19 +164,6 @@ var FooErr = errors.New("foo") }) } -func TestGofumptWarning(t *testing.T) { - testenv.SkipAfterGo1Point(t, 17) - - WithOptions( - Settings{"gofumpt": true}, - ).Run(t, "", func(t *testing.T, env *Env) { - env.OnceMet( - InitialWorkspaceLoad, - ShownMessage("gofumpt is not supported"), - ) - }) -} - func TestDeprecatedSettings(t *testing.T) { WithOptions( Settings{ diff --git a/gopls/internal/test/integration/misc/vuln_test.go b/gopls/internal/test/integration/misc/vuln_test.go index f74c1c38d77..b2bd520759b 100644 --- a/gopls/internal/test/integration/misc/vuln_test.go +++ b/gopls/internal/test/integration/misc/vuln_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.18 -// +build go1.18 - package misc import ( @@ -167,7 +164,7 @@ func TestRunGovulncheckStd(t *testing.T) { -- go.mod -- module mod.com -go 1.18 +go 1.19 -- main.go -- package main @@ -192,9 +189,9 @@ func main() { // Let the analyzer read vulnerabilities data from the testdata/vulndb. "GOVULNDB": db.URI(), // When fetchinging stdlib package vulnerability info, - // behave as if our go version is go1.18 for this testing. + // behave as if our go version is go1.19 for this testing. // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). - cache.GoVersionForVulnTest: "go1.18", + cache.GoVersionForVulnTest: "go1.19", "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. }, Settings{ diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index 1652eec7282..c32b81ac6bc 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -230,19 +230,14 @@ func Test(t *testing.T) { if err := os.WriteFile(filename, formatted, 0644); err != nil { t.Error(err) } - } else { - // On go 1.19 and later, verify that the testdata has not changed. - // - // On earlier Go versions, the golden test data varies due to different - // markdown escaping. + } else if !t.Failed() { + // Verify that the testdata has not changed. // // Only check this if the test hasn't already failed, otherwise we'd // report duplicate mismatches of golden data. - if testenv.Go1Point() >= 19 && !t.Failed() { - // Otherwise, verify that formatted content matches. - if diff := compare.NamedText("formatted", "on-disk", string(formatted), string(test.content)); diff != "" { - t.Errorf("formatted test does not match on-disk content:\n%s", diff) - } + // Otherwise, verify that formatted content matches. + if diff := compare.NamedText("formatted", "on-disk", string(formatted), string(test.content)); diff != "" { + t.Errorf("formatted test does not match on-disk content:\n%s", diff) } } }) diff --git a/gopls/internal/test/marker/testdata/definition/embed.txt b/gopls/internal/test/marker/testdata/definition/embed.txt index 9842c37d047..bd503869f8b 100644 --- a/gopls/internal/test/marker/testdata/definition/embed.txt +++ b/gopls/internal/test/marker/testdata/definition/embed.txt @@ -1,10 +1,5 @@ This test checks definition and hover operations over embedded fields and methods. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/definition/import.txt b/gopls/internal/test/marker/testdata/definition/import.txt index c0e5d76c00f..2ae95a8c29b 100644 --- a/gopls/internal/test/marker/testdata/definition/import.txt +++ b/gopls/internal/test/marker/testdata/definition/import.txt @@ -1,10 +1,5 @@ This test checks definition and hover over imports. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/definition/misc.txt b/gopls/internal/test/marker/testdata/definition/misc.txt index c7147a625be..82c15b5d2ba 100644 --- a/gopls/internal/test/marker/testdata/definition/misc.txt +++ b/gopls/internal/test/marker/testdata/definition/misc.txt @@ -1,10 +1,5 @@ This test exercises miscellaneous definition and hover requests. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt b/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt index b644d453164..6b8d6ce0ad2 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt @@ -1,10 +1,6 @@ This test exercises a crash due to treatment of "comparable" in methodset calculation (golang/go#60544). --min_go is 1.19 as the error message changed at this Go version. --- flags -- --min_go=go1.19 - -- main.go -- package main diff --git a/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt b/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt index a60cdeebeeb..b14f4dfabd0 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt @@ -1,9 +1,6 @@ This test verifies that analyzers without RunDespiteErrors are not executed on a package containing type errors (see issue #54762). -We require go1.18 because the range of the `1 + ""` go/types error -changed then, and the new @diag marker is quite particular. - -- go.mod -- module example.com go 1.12 diff --git a/gopls/internal/test/marker/testdata/hover/basiclit.txt b/gopls/internal/test/marker/testdata/hover/basiclit.txt index 9269c289840..9c26b2a2f07 100644 --- a/gopls/internal/test/marker/testdata/hover/basiclit.txt +++ b/gopls/internal/test/marker/testdata/hover/basiclit.txt @@ -1,10 +1,5 @@ This test checks gopls behavior when hovering over basic literals. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- basiclit.go -- package basiclit diff --git a/gopls/internal/test/marker/testdata/hover/const.txt b/gopls/internal/test/marker/testdata/hover/const.txt index c5f783c1f5d..179ff155357 100644 --- a/gopls/internal/test/marker/testdata/hover/const.txt +++ b/gopls/internal/test/marker/testdata/hover/const.txt @@ -1,10 +1,5 @@ This test checks hovering over constants. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/hover/godef.txt b/gopls/internal/test/marker/testdata/hover/godef.txt index 2d82ab0debe..9b2e7ec2ce3 100644 --- a/gopls/internal/test/marker/testdata/hover/godef.txt +++ b/gopls/internal/test/marker/testdata/hover/godef.txt @@ -3,6 +3,9 @@ It tests various hover and definition requests. Requires go1.19+ for the new go/doc/comment package. +TODO(adonovan): figure out why this test also fails +without -min_go=go1.20. Or just wait... + -- flags -- -min_go=go1.19 diff --git a/gopls/internal/test/marker/testdata/hover/goprivate.txt b/gopls/internal/test/marker/testdata/hover/goprivate.txt index f5b71795392..7269ba19634 100644 --- a/gopls/internal/test/marker/testdata/hover/goprivate.txt +++ b/gopls/internal/test/marker/testdata/hover/goprivate.txt @@ -1,10 +1,5 @@ This test checks that links in hover obey GOPRIVATE. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- env -- GOPRIVATE=mod.com -- go.mod -- diff --git a/gopls/internal/test/marker/testdata/hover/hover.txt b/gopls/internal/test/marker/testdata/hover/hover.txt index 32a4c195663..b5e4a88434b 100644 --- a/gopls/internal/test/marker/testdata/hover/hover.txt +++ b/gopls/internal/test/marker/testdata/hover/hover.txt @@ -1,10 +1,5 @@ This test demonstrates some features of the new marker test runner. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- a.go -- package a diff --git a/gopls/internal/test/marker/testdata/hover/linkable.txt b/gopls/internal/test/marker/testdata/hover/linkable.txt index b77b9e8a4d9..7fdb2470cdf 100644 --- a/gopls/internal/test/marker/testdata/hover/linkable.txt +++ b/gopls/internal/test/marker/testdata/hover/linkable.txt @@ -4,11 +4,6 @@ identifiers. We should only produce links that work, meaning the object is reachable via the package's public API. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/hover/linkable_generics.txt b/gopls/internal/test/marker/testdata/hover/linkable_generics.txt index 1ea009e0318..0b7ade7965e 100644 --- a/gopls/internal/test/marker/testdata/hover/linkable_generics.txt +++ b/gopls/internal/test/marker/testdata/hover/linkable_generics.txt @@ -1,10 +1,5 @@ This file contains tests for documentation links to generic code in hover. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/hover/linkname.txt b/gopls/internal/test/marker/testdata/hover/linkname.txt index 829097ce307..8bb2eeb33cd 100644 --- a/gopls/internal/test/marker/testdata/hover/linkname.txt +++ b/gopls/internal/test/marker/testdata/hover/linkname.txt @@ -1,10 +1,5 @@ This test check hover on the 2nd argument in go:linkname directives. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- go.mod -- module mod.com diff --git a/gopls/internal/test/marker/testdata/hover/std.txt b/gopls/internal/test/marker/testdata/hover/std.txt index af8f89fe65c..c0db135f6b1 100644 --- a/gopls/internal/test/marker/testdata/hover/std.txt +++ b/gopls/internal/test/marker/testdata/hover/std.txt @@ -7,11 +7,6 @@ synopsis does not. In the future we may need to limit this test to the latest Go version to avoid documentation churn. -Requires go1.19+ for the new go/doc/comment package. - --- flags -- --min_go=go1.19 - -- settings.json -- { "hoverKind": "SynopsisDocumentation" diff --git a/gopls/internal/test/marker/testdata/symbol/generic.txt b/gopls/internal/test/marker/testdata/symbol/generic.txt index 84cea99c1f7..1254851ad14 100644 --- a/gopls/internal/test/marker/testdata/symbol/generic.txt +++ b/gopls/internal/test/marker/testdata/symbol/generic.txt @@ -3,9 +3,6 @@ Basic tests of textDocument/documentSymbols with generics. -- symbol.go -- //@symbol(want) -//go:build go1.18 -// +build go1.18 - package main type T[P any] struct { diff --git a/gopls/internal/util/bug/bug.go b/gopls/internal/util/bug/bug.go index e04b753e315..dcd242d4856 100644 --- a/gopls/internal/util/bug/bug.go +++ b/gopls/internal/util/bug/bug.go @@ -19,7 +19,7 @@ import ( "sync" "time" - "golang.org/x/tools/gopls/internal/telemetry" + "golang.org/x/telemetry/counter" ) // PanicOnBugs controls whether to panic when bugs are reported. @@ -69,7 +69,7 @@ func Report(description string) { } // BugReportCount is a telemetry counter that tracks # of bug reports. -var BugReportCount = telemetry.NewStackCounter("gopls/bug", 16) +var BugReportCount = counter.NewStack("gopls/bug", 16) func report(description string) { _, file, line, ok := runtime.Caller(2) // all exported reporting functions call report directly diff --git a/gopls/internal/util/frob/frob.go b/gopls/internal/util/frob/frob.go index 0f3ede26773..cd385a9d692 100644 --- a/gopls/internal/util/frob/frob.go +++ b/gopls/internal/util/frob/frob.go @@ -93,7 +93,7 @@ func frobFor(t reflect.Type) *frob { case reflect.Array, reflect.Slice, - reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer + reflect.Pointer: fr.addElem(fr.t.Elem()) case reflect.Map: @@ -214,7 +214,7 @@ func (fr *frob) encode(out *writer, v reflect.Value) { } } - case reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer + case reflect.Pointer: if v.IsNil() { out.uint8(0) } else { @@ -341,7 +341,7 @@ func (fr *frob) decode(in *reader, addr reflect.Value) { } } - case reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer + case reflect.Pointer: isNil := in.uint8() == 0 if !isNil { ptr := reflect.New(fr.elems[0].t) @@ -402,38 +402,7 @@ func (r *reader) bytes(n int) []byte { type writer struct{ data []byte } func (w *writer) uint8(v uint8) { w.data = append(w.data, v) } -func (w *writer) uint16(v uint16) { w.data = appendUint16(w.data, v) } -func (w *writer) uint32(v uint32) { w.data = appendUint32(w.data, v) } -func (w *writer) uint64(v uint64) { w.data = appendUint64(w.data, v) } +func (w *writer) uint16(v uint16) { w.data = le.AppendUint16(w.data, v) } +func (w *writer) uint32(v uint32) { w.data = le.AppendUint32(w.data, v) } +func (w *writer) uint64(v uint64) { w.data = le.AppendUint64(w.data, v) } func (w *writer) bytes(v []byte) { w.data = append(w.data, v...) } - -// TODO(adonovan): delete these as in go1.19 they are methods on LittleEndian: - -func appendUint16(b []byte, v uint16) []byte { - return append(b, - byte(v), - byte(v>>8), - ) -} - -func appendUint32(b []byte, v uint32) []byte { - return append(b, - byte(v), - byte(v>>8), - byte(v>>16), - byte(v>>24), - ) -} - -func appendUint64(b []byte, v uint64) []byte { - return append(b, - byte(v), - byte(v>>8), - byte(v>>16), - byte(v>>24), - byte(v>>32), - byte(v>>40), - byte(v>>48), - byte(v>>56), - ) -} diff --git a/gopls/internal/util/lru/lru_fuzz_test.go b/gopls/internal/util/lru/lru_fuzz_test.go index 1733511f35e..b82776b25ba 100644 --- a/gopls/internal/util/lru/lru_fuzz_test.go +++ b/gopls/internal/util/lru/lru_fuzz_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.18 -// +build go1.18 - package lru_test import ( diff --git a/gopls/internal/util/slices/slices.go b/gopls/internal/util/slices/slices.go index 4c4fa4d0a79..938ae6c34c0 100644 --- a/gopls/internal/util/slices/slices.go +++ b/gopls/internal/util/slices/slices.go @@ -6,14 +6,14 @@ 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.19 slices.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.19 slices.Contains. +// 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 { @@ -25,7 +25,7 @@ func Contains[S ~[]E, E comparable](slice S, x E) bool { // IndexFunc returns the first index i satisfying f(s[i]), // or -1 if none do. -// TODO(adonovan): use go1.19 slices.IndexFunc. +// 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]) { @@ -37,7 +37,7 @@ func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { // ContainsFunc reports whether at least one // element e of s satisfies f(e). -// TODO(adonovan): use go1.19 slices.ContainsFunc. +// 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 } @@ -63,7 +63,7 @@ func Concat[S ~[]E, E any](slices ...S) S { // 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.19 slices.Grow. +// 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") diff --git a/gopls/internal/vulncheck/scan/command.go b/gopls/internal/vulncheck/scan/command.go index 1f53d8b55f3..4ef005010c9 100644 --- a/gopls/internal/vulncheck/scan/command.go +++ b/gopls/internal/vulncheck/scan/command.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.18 -// +build go1.18 - package scan import ( diff --git a/gopls/internal/vulncheck/semver/semver.go b/gopls/internal/vulncheck/semver/semver.go index 67c4fe8a39e..ade710d0573 100644 --- a/gopls/internal/vulncheck/semver/semver.go +++ b/gopls/internal/vulncheck/semver/semver.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.18 -// +build go1.18 - // Package semver provides shared utilities for manipulating // Go semantic versions. package semver diff --git a/gopls/internal/vulncheck/semver/semver_test.go b/gopls/internal/vulncheck/semver/semver_test.go index 6daead6855b..8a462287fa4 100644 --- a/gopls/internal/vulncheck/semver/semver_test.go +++ b/gopls/internal/vulncheck/semver/semver_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.18 -// +build go1.18 - package semver import ( diff --git a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go index 659d2f1fd10..e661b83bc71 100644 --- a/gopls/internal/vulncheck/vulntest/db.go +++ b/gopls/internal/vulncheck/vulntest/db.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.18 -// +build go1.18 - // Package vulntest provides helpers for vulncheck functionality testing. package vulntest diff --git a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulncheck/vulntest/db_test.go index 22281249502..3c3407105ac 100644 --- a/gopls/internal/vulncheck/vulntest/db_test.go +++ b/gopls/internal/vulncheck/vulntest/db_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.18 -// +build go1.18 - package vulntest import ( diff --git a/gopls/internal/vulncheck/vulntest/report.go b/gopls/internal/vulncheck/vulntest/report.go index cbfd0aeb8ff..b67986cf8c2 100644 --- a/gopls/internal/vulncheck/vulntest/report.go +++ b/gopls/internal/vulncheck/vulntest/report.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.18 -// +build go1.18 - package vulntest import ( diff --git a/gopls/internal/vulncheck/vulntest/report_test.go b/gopls/internal/vulncheck/vulntest/report_test.go index 31f62aba838..b88633c2f1c 100644 --- a/gopls/internal/vulncheck/vulntest/report_test.go +++ b/gopls/internal/vulncheck/vulntest/report_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.18 -// +build go1.18 - package vulntest import ( diff --git a/gopls/internal/vulncheck/vulntest/stdlib.go b/gopls/internal/vulncheck/vulntest/stdlib.go index 9bf4d4ef0d4..57194f71688 100644 --- a/gopls/internal/vulncheck/vulntest/stdlib.go +++ b/gopls/internal/vulncheck/vulntest/stdlib.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.18 -// +build go1.18 - package vulntest import ( diff --git a/gopls/internal/vulncheck/vulntest/stdlib_test.go b/gopls/internal/vulncheck/vulntest/stdlib_test.go index 8f893f3ec42..7b212976350 100644 --- a/gopls/internal/vulncheck/vulntest/stdlib_test.go +++ b/gopls/internal/vulncheck/vulntest/stdlib_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.18 -// +build go1.18 - package vulntest import "testing" diff --git a/gopls/main.go b/gopls/main.go index 31c5c5fdf1a..9217b278b1a 100644 --- a/gopls/main.go +++ b/gopls/main.go @@ -17,9 +17,9 @@ import ( "context" "os" + "golang.org/x/telemetry" "golang.org/x/tools/gopls/internal/cmd" "golang.org/x/tools/gopls/internal/hooks" - "golang.org/x/tools/gopls/internal/telemetry" versionpkg "golang.org/x/tools/gopls/internal/version" "golang.org/x/tools/internal/tool" ) @@ -29,7 +29,7 @@ var version = "" // if set by the linker, overrides the gopls version func main() { versionpkg.VersionOverride = version - telemetry.Start() + telemetry.Start(telemetry.Config{ReportCrashes: true}) ctx := context.Background() tool.Main(ctx, cmd.New(hooks.Options), os.Args[1:]) } diff --git a/internal/compat/appendf.go b/internal/compat/appendf.go deleted file mode 100644 index 069d5171704..00000000000 --- a/internal/compat/appendf.go +++ /dev/null @@ -1,13 +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. - -//go:build go1.19 - -package compat - -import "fmt" - -func Appendf(b []byte, format string, a ...interface{}) []byte { - return fmt.Appendf(b, format, a...) -} diff --git a/internal/compat/appendf_118.go b/internal/compat/appendf_118.go deleted file mode 100644 index 29af353cdaf..00000000000 --- a/internal/compat/appendf_118.go +++ /dev/null @@ -1,13 +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. - -//go:build !go1.19 - -package compat - -import "fmt" - -func Appendf(b []byte, format string, a ...interface{}) []byte { - return append(b, fmt.Sprintf(format, a...)...) -} diff --git a/internal/compat/doc.go b/internal/compat/doc.go deleted file mode 100644 index 59c667a37a2..00000000000 --- a/internal/compat/doc.go +++ /dev/null @@ -1,7 +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. - -// The compat package implements API shims for backward compatibility at older -// Go versions. -package compat diff --git a/internal/gcimporter/gcimporter_test.go b/internal/gcimporter/gcimporter_test.go index 81d36bd5ff6..95cc36c4d96 100644 --- a/internal/gcimporter/gcimporter_test.go +++ b/internal/gcimporter/gcimporter_test.go @@ -11,7 +11,6 @@ import ( "bytes" "fmt" "go/ast" - "go/build" "go/constant" goimporter "go/importer" goparser "go/parser" @@ -165,8 +164,7 @@ func TestImportTypeparamTests(t *testing.T) { t.Skipf("in short mode, skipping test that requires export data for all of std") } - testenv.NeedsGo1Point(t, 18) // requires generics - testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache + testenv.NeedsGoBuild(t) // to find stdlib export data in the build cache // This package only handles gc export data. if runtime.Compiler != "gc" { @@ -406,18 +404,6 @@ var importedObjectTests = []struct { {"go/types.Type", "type Type interface{String() string; Underlying() Type}"}, } -// TODO(rsc): Delete this init func after x/tools no longer needs to test successfully with Go 1.17. -func init() { - if build.Default.ReleaseTags[len(build.Default.ReleaseTags)-1] <= "go1.17" { - for i := range importedObjectTests { - if importedObjectTests[i].name == "context.Context" { - // Expand any to interface{}. - importedObjectTests[i].want = "type Context interface{Deadline() (deadline time.Time, ok bool); Done() <-chan struct{}; Err() error; Value(key interface{}) interface{}}" - } - } - } -} - func TestImportedTypes(t *testing.T) { // This package only handles gc export data. needsCompiler(t, "gc") @@ -739,8 +725,6 @@ func TestIssue25301(t *testing.T) { } func TestIssue51836(t *testing.T) { - testenv.NeedsGo1Point(t, 18) // requires generics - // This package only handles gc export data. needsCompiler(t, "gc") @@ -770,8 +754,6 @@ func TestIssue51836(t *testing.T) { } func TestIssue61561(t *testing.T) { - testenv.NeedsGo1Point(t, 18) // requires generics - const src = `package p type I[P any] interface { @@ -836,8 +818,6 @@ type K = StillBad[string] } func TestIssue57015(t *testing.T) { - testenv.NeedsGo1Point(t, 18) // requires generics - // This package only handles gc export data. needsCompiler(t, "gc") diff --git a/internal/gcimporter/iexport_go118_test.go b/internal/gcimporter/iexport_go118_test.go index 134c231f8c1..c748fb36165 100644 --- a/internal/gcimporter/iexport_go118_test.go +++ b/internal/gcimporter/iexport_go118_test.go @@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 -// +build go1.18 - package gcimporter_test +// This file defines test of generics features introduce in go1.18. + import ( "bytes" "fmt" diff --git a/internal/gcimporter/support_go117.go b/internal/gcimporter/support_go117.go deleted file mode 100644 index d892273efb6..00000000000 --- a/internal/gcimporter/support_go117.go +++ /dev/null @@ -1,16 +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.18 -// +build !go1.18 - -package gcimporter - -import "go/types" - -const iexportVersion = iexportVersionGo1_11 - -func additionalPredeclared() []types.Type { - return nil -} diff --git a/internal/gcimporter/support_go118.go b/internal/gcimporter/support_go118.go index edbe6ea7041..0cd3b91b65a 100644 --- a/internal/gcimporter/support_go118.go +++ b/internal/gcimporter/support_go118.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.18 -// +build go1.18 - package gcimporter import "go/types" diff --git a/internal/gcimporter/unified_no.go b/internal/gcimporter/unified_no.go index 286bf445483..38b624cadab 100644 --- a/internal/gcimporter/unified_no.go +++ b/internal/gcimporter/unified_no.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !(go1.18 && goexperiment.unified) -// +build !go1.18 !goexperiment.unified +//go:build !goexperiment.unified +// +build !goexperiment.unified package gcimporter diff --git a/internal/gcimporter/unified_yes.go b/internal/gcimporter/unified_yes.go index b5d69ffbe68..b5118d0b3a5 100644 --- a/internal/gcimporter/unified_yes.go +++ b/internal/gcimporter/unified_yes.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.18 && goexperiment.unified -// +build go1.18,goexperiment.unified +//go:build goexperiment.unified +// +build goexperiment.unified package gcimporter diff --git a/internal/gcimporter/ureader_no.go b/internal/gcimporter/ureader_no.go deleted file mode 100644 index 8eb20729c2a..00000000000 --- a/internal/gcimporter/ureader_no.go +++ /dev/null @@ -1,19 +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 !go1.18 -// +build !go1.18 - -package gcimporter - -import ( - "fmt" - "go/token" - "go/types" -) - -func UImportData(fset *token.FileSet, imports map[string]*types.Package, data []byte, path string) (_ int, pkg *types.Package, err error) { - err = fmt.Errorf("go/tools compiled with a Go version earlier than 1.18 cannot read unified IR export data") - return -} diff --git a/internal/gcimporter/ureader_yes.go b/internal/gcimporter/ureader_yes.go index bcf52d931df..f4edc46ab74 100644 --- a/internal/gcimporter/ureader_yes.go +++ b/internal/gcimporter/ureader_yes.go @@ -4,9 +4,6 @@ // Derived from go/internal/gcimporter/ureader.go -//go:build go1.18 -// +build go1.18 - package gcimporter import ( diff --git a/internal/imports/mod_test.go b/internal/imports/mod_test.go index c624463895d..e65104cbf2e 100644 --- a/internal/imports/mod_test.go +++ b/internal/imports/mod_test.go @@ -559,8 +559,6 @@ package v // Tests that go.work files are respected. func TestModWorkspace(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - mt := setup(t, nil, ` -- go.work -- go 1.18 @@ -595,8 +593,6 @@ package b // respected and that a wildcard replace in go.work overrides a versioned replace // in go.mod. func TestModWorkspaceReplace(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - mt := setup(t, nil, ` -- go.work -- use m @@ -654,8 +650,6 @@ func G() { // Tests a case where conflicting replaces are overridden by a replace // in the go.work file. func TestModWorkspaceReplaceOverride(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - mt := setup(t, nil, `-- go.work -- use m use n @@ -719,8 +713,6 @@ func G() { // workspaces with module pruning. This is based on the // cmd/go mod_prune_all script test. func TestModWorkspacePrune(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - mt := setup(t, nil, ` -- go.work -- go 1.18 diff --git a/internal/robustio/robustio_posix.go b/internal/robustio/robustio_posix.go index 8aa13d02786..cf74865d0b5 100644 --- a/internal/robustio/robustio_posix.go +++ b/internal/robustio/robustio_posix.go @@ -5,8 +5,6 @@ //go:build !windows && !plan9 // +build !windows,!plan9 -// TODO(adonovan): use 'unix' tag when go1.19 can be assumed. - package robustio import ( diff --git a/internal/tokeninternal/tokeninternal.go b/internal/tokeninternal/tokeninternal.go index 7e638ec24fc..ff9437a36cd 100644 --- a/internal/tokeninternal/tokeninternal.go +++ b/internal/tokeninternal/tokeninternal.go @@ -34,30 +34,16 @@ func GetLines(file *token.File) []int { lines []int _ []struct{} } - type tokenFile118 struct { - _ *token.FileSet // deleted in go1.19 - tokenFile119 - } - - type uP = unsafe.Pointer - switch unsafe.Sizeof(*file) { - case unsafe.Sizeof(tokenFile118{}): - var ptr *tokenFile118 - *(*uP)(uP(&ptr)) = uP(file) - ptr.mu.Lock() - defer ptr.mu.Unlock() - return ptr.lines - case unsafe.Sizeof(tokenFile119{}): - var ptr *tokenFile119 - *(*uP)(uP(&ptr)) = uP(file) - ptr.mu.Lock() - defer ptr.mu.Unlock() - return ptr.lines - - default: + if unsafe.Sizeof(*file) != unsafe.Sizeof(tokenFile119{}) { panic("unexpected token.File size") } + var ptr *tokenFile119 + type uP = unsafe.Pointer + *(*uP)(uP(&ptr)) = uP(file) + ptr.mu.Lock() + defer ptr.mu.Unlock() + return ptr.lines } // AddExistingFiles adds the specified files to the FileSet if they diff --git a/internal/typeparams/common.go b/internal/typeparams/common.go index 6679d79f9cf..8c3a42dc311 100644 --- a/internal/typeparams/common.go +++ b/internal/typeparams/common.go @@ -2,20 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package typeparams contains common utilities for writing tools that interact -// with generic Go code, as introduced with Go 1.18. -// -// Many of the types and functions in this package are proxies for the new APIs -// introduced in the standard library with Go 1.18. For example, the -// typeparams.Union type is an alias for go/types.Union, and the ForTypeSpec -// function returns the value of the go/ast.TypeSpec.TypeParams field. At Go -// versions older than 1.18 these helpers are implemented as stubs, allowing -// users of this package to write code that handles generic constructs inline, -// even if the Go version being used to compile does not support generics. -// -// Additionally, this package contains common utilities for working with the -// new generic constructs, to supplement the standard library APIs. Notably, -// the StructuralTerms API computes a minimal representation of the structural +// Package typeparams contains common utilities for writing tools that +// interact with generic Go code, as introduced with Go 1.18. It +// supplements the standard library APIs. Notably, the StructuralTerms +// API computes a minimal representation of the structural // restrictions on a type parameter. // // An external version of these APIs is available in the diff --git a/internal/typeparams/common_test.go b/internal/typeparams/common_test.go index b6f355cc5d7..b095b65e808 100644 --- a/internal/typeparams/common_test.go +++ b/internal/typeparams/common_test.go @@ -11,7 +11,6 @@ import ( "go/types" "testing" - "golang.org/x/tools/internal/testenv" . "golang.org/x/tools/internal/typeparams" ) @@ -41,7 +40,6 @@ func TestGetIndexExprData(t *testing.T) { } func TestOriginMethodRecursive(t *testing.T) { - testenv.NeedsGo1Point(t, 18) src := `package p type N[A any] int @@ -113,7 +111,6 @@ func (r *N[C]) n() { } } func TestOriginMethodUses(t *testing.T) { - testenv.NeedsGo1Point(t, 18) tests := []string{ `type T interface { m() }; func _(t T) { t.m() }`, @@ -209,8 +206,6 @@ func TestOriginMethod60628(t *testing.T) { } func TestGenericAssignableTo(t *testing.T) { - testenv.NeedsGo1Point(t, 18) - tests := []struct { src string want bool diff --git a/internal/typesinternal/types_118.go b/internal/typesinternal/types_118.go index a42b072a67d..ef7ea290c0b 100644 --- a/internal/typesinternal/types_118.go +++ b/internal/typesinternal/types_118.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.18 -// +build go1.18 - package typesinternal import ( From 5bf7d005299769e9d476813f09efd2a4296d5fe9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 29 Feb 2024 08:13:55 -0500 Subject: [PATCH 46/47] cmd/callgraph: add 'posn' template helper This change adds a helper function to the template environment to make it easier to compute the token.Position of an ssa Function. Also, improve the documentation. Fixes golang/go#65980 Change-Id: I16d4cc87bb6f96985684da5ce027be296f22a601 Reviewed-on: https://go-review.googlesource.com/c/tools/+/567838 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- cmd/callgraph/main.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/cmd/callgraph/main.go b/cmd/callgraph/main.go index 33f7dfa8098..7853826b8fc 100644 --- a/cmd/callgraph/main.go +++ b/cmd/callgraph/main.go @@ -109,9 +109,20 @@ Flags: Caller and Callee are *ssa.Function values, which print as "(*sync/atomic.Mutex).Lock", but other attributes may be - derived from them, e.g. Caller.Pkg.Pkg.Path yields the - import path of the enclosing package. Consult the go/ssa - API documentation for details. + derived from them. For example: + + - {{.Caller.Pkg.Pkg.Path}} yields the import path of the + enclosing package; and + + - {{(.Caller.Prog.Fset.Position .Caller.Pos).Filename}} + yields the name of the file that declares the caller. + + - The 'posn' template function returns the token.Position + of an ssa.Function, so the previous example can be + reduced to {{(posn .Caller).Filename}}. + + Consult the documentation for go/token, text/template, and + golang.org/x/tools/go/ssa for more detail. Examples: @@ -238,7 +249,12 @@ func doCallgraph(dir, gopath, algo, format string, tests bool, args []string) er format = ` {{printf "%q" .Caller}} -> {{printf "%q" .Callee}}` } - tmpl, err := template.New("-format").Parse(format) + funcMap := template.FuncMap{ + "posn": func(f *ssa.Function) token.Position { + return f.Prog.Fset.Position(f.Pos()) + }, + } + tmpl, err := template.New("-format").Funcs(funcMap).Parse(format) if err != nil { return fmt.Errorf("invalid -format template: %v", err) } From 7656c4c657688cae30795365d2a5f30d6f18be7f Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Mon, 4 Mar 2024 20:11:25 +0000 Subject: [PATCH 47/47] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I5525544e003e009ee5436c992a14d05b9822b558 Reviewed-on: https://go-review.googlesource.com/c/tools/+/568817 Reviewed-by: Than McIntosh LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Knyszek Auto-Submit: Gopher Robot --- go.mod | 6 +++--- go.sum | 12 ++++++------ gopls/go.mod | 4 ++-- gopls/go.sum | 9 +++++++-- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 9458c1500cc..6791902dae1 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,13 @@ go 1.19 require ( github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.15.0 - golang.org/x/net v0.21.0 + golang.org/x/mod v0.16.0 + golang.org/x/net v0.22.0 ) require golang.org/x/sync v0.6.0 require ( - golang.org/x/sys v0.17.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 ) diff --git a/go.sum b/go.sum index 5e7611e6cca..c39ea36a534 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ 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.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= diff --git a/gopls/go.mod b/gopls/go.mod index 3262b0aedb2..cc810dc7ee9 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -6,7 +6,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/jba/printsrc v0.2.2 github.com/jba/templatecheck v0.7.0 - golang.org/x/mod v0.15.0 + golang.org/x/mod v0.16.0 golang.org/x/sync v0.6.0 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 golang.org/x/text v0.14.0 @@ -22,7 +22,7 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/google/safehtml v0.1.0 // indirect golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect - golang.org/x/sys v0.17.0 // indirect + golang.org/x/sys v0.18.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/gopls/go.sum b/gopls/go.sum index 4a9335ea552..228800182b5 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -14,23 +14,28 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 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.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 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= -golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 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 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=