From 22abcd60864317fb7ed734a54cfe914669d4cd4a Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Sat, 7 Oct 2023 07:20:41 -0400 Subject: [PATCH 001/100] oogo/analysis/passes/bools: remove duplicate functions Replaced two unexported functions with calls to the equivalent ones in analysisutils. Change-Id: I81a791a35b25ed20f51fe47f60cbb4a50dee3fba Reviewed-on: https://go-review.googlesource.com/c/tools/+/533178 Reviewed-by: Alan Donovan Run-TryBot: Jonathan Amsterdam TryBot-Result: Gopher Robot --- go/analysis/passes/bools/bools.go | 48 ++----------------------------- 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/go/analysis/passes/bools/bools.go b/go/analysis/passes/bools/bools.go index 4219f087b98..5ebafb7df3e 100644 --- a/go/analysis/passes/bools/bools.go +++ b/go/analysis/passes/bools/bools.go @@ -83,7 +83,7 @@ func (op boolOp) commutativeSets(info *types.Info, e *ast.BinaryExpr, seen map[* i := 0 var sets [][]ast.Expr for j := 0; j <= len(exprs); j++ { - if j == len(exprs) || hasSideEffects(info, exprs[j]) { + if j == len(exprs) || analysisutil.HasSideEffects(info, exprs[j]) { if i < j { sets = append(sets, exprs[i:j]) } @@ -162,46 +162,13 @@ func (op boolOp) checkSuspect(pass *analysis.Pass, exprs []ast.Expr) { } } -// hasSideEffects reports whether evaluation of e has side effects. -func hasSideEffects(info *types.Info, e ast.Expr) bool { - safe := true - ast.Inspect(e, func(node ast.Node) bool { - switch n := node.(type) { - case *ast.CallExpr: - typVal := info.Types[n.Fun] - switch { - case typVal.IsType(): - // Type conversion, which is safe. - case typVal.IsBuiltin(): - // Builtin func, conservatively assumed to not - // be safe for now. - safe = false - return false - default: - // A non-builtin func or method call. - // Conservatively assume that all of them have - // side effects for now. - safe = false - return false - } - case *ast.UnaryExpr: - if n.Op == token.ARROW { - safe = false - return false - } - } - return true - }) - return !safe -} - // split returns a slice of all subexpressions in e that are connected by op. // For example, given 'a || (b || c) || d' with the or op, // split returns []{d, c, b, a}. // seen[e] is already true; any newly processed exprs are added to seen. func (op boolOp) split(e ast.Expr, seen map[*ast.BinaryExpr]bool) (exprs []ast.Expr) { for { - e = unparen(e) + e = analysisutil.Unparen(e) if b, ok := e.(*ast.BinaryExpr); ok && b.Op == op.tok { seen[b] = true exprs = append(exprs, op.split(b.Y, seen)...) @@ -213,14 +180,3 @@ func (op boolOp) split(e ast.Expr, seen map[*ast.BinaryExpr]bool) (exprs []ast.E } return } - -// unparen returns e with any enclosing parentheses stripped. -func unparen(e ast.Expr) ast.Expr { - for { - p, ok := e.(*ast.ParenExpr) - if !ok { - return e - } - e = p.X - } -} From 395d3932613eda19b1f78d2ca1b5fbd19f2a60c1 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Sat, 7 Oct 2023 07:33:06 -0400 Subject: [PATCH 002/100] go/analysis/passes/internal/analysisutil: add IsNamedType IsNamedType replaces similar logic in several passes. Change-Id: I0091843170072a77cfef9721c2d37a45270d7f0b Reviewed-on: https://go-review.googlesource.com/c/tools/+/533179 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Jonathan Amsterdam --- go/analysis/passes/copylock/copylock.go | 4 +--- .../passes/httpresponse/httpresponse.go | 16 +++----------- .../passes/internal/analysisutil/util.go | 22 +++++++++++++++++++ go/analysis/passes/loopclosure/loopclosure.go | 17 +------------- go/analysis/passes/printf/printf.go | 7 +----- .../reflectvaluecompare.go | 6 +---- go/analysis/passes/slog/slog.go | 11 +--------- go/analysis/passes/tests/tests.go | 8 +------ go/analysis/passes/unsafeptr/unsafeptr.go | 13 ++--------- 9 files changed, 33 insertions(+), 71 deletions(-) diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go index ec7727de769..db70dea0f17 100644 --- a/go/analysis/passes/copylock/copylock.go +++ b/go/analysis/passes/copylock/copylock.go @@ -319,9 +319,7 @@ func lockPath(tpkg *types.Package, typ types.Type, seen map[types.Type]bool) typ // In go1.10, sync.noCopy did not implement Locker. // (The Unlock method was added only in CL 121876.) // TODO(adonovan): remove workaround when we drop go1.10. - if named, ok := typ.(*types.Named); ok && - named.Obj().Name() == "noCopy" && - named.Obj().Pkg().Path() == "sync" { + if analysisutil.IsNamedType(typ, "sync", "noCopy") { return []string{typ.String()} } diff --git a/go/analysis/passes/httpresponse/httpresponse.go b/go/analysis/passes/httpresponse/httpresponse.go index 61c3b764f7f..c6b6c81b420 100644 --- a/go/analysis/passes/httpresponse/httpresponse.go +++ b/go/analysis/passes/httpresponse/httpresponse.go @@ -116,7 +116,7 @@ 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 || !isNamedType(ptr.Elem(), "net/http", "Response") { + if ptr, ok := res.At(0).Type().(*types.Pointer); !ok || !analysisutil.IsNamedType(ptr.Elem(), "net/http", "Response") { return false // the first return type is not *http.Response. } @@ -131,11 +131,11 @@ func isHTTPFuncOrMethodOnClient(info *types.Info, expr *ast.CallExpr) bool { return ok && id.Name == "http" // function in net/http package. } - if isNamedType(typ, "net/http", "Client") { + if analysisutil.IsNamedType(typ, "net/http", "Client") { return true // method on http.Client. } ptr, ok := typ.(*types.Pointer) - return ok && isNamedType(ptr.Elem(), "net/http", "Client") // method on *http.Client. + return ok && analysisutil.IsNamedType(ptr.Elem(), "net/http", "Client") // method on *http.Client. } // restOfBlock, given a traversal stack, finds the innermost containing @@ -171,13 +171,3 @@ func rootIdent(n ast.Node) *ast.Ident { return nil } } - -// isNamedType reports whether t is the named type path.name. -func isNamedType(t types.Type, path, name string) bool { - n, ok := t.(*types.Named) - if !ok { - return false - } - obj := n.Obj() - return obj.Name() == name && obj.Pkg() != nil && obj.Pkg().Path() == path -} diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go index a8d84034df1..eb0ecd6a302 100644 --- a/go/analysis/passes/internal/analysisutil/util.go +++ b/go/analysis/passes/internal/analysisutil/util.go @@ -118,3 +118,25 @@ func Imports(pkg *types.Package, path string) bool { } return false } + +// IsNamedType reports whether t is the named type with the given package path +// and one of the given names. +// 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) + if !ok { + return false + } + obj := n.Obj() + if obj == nil || obj.Pkg() == nil || obj.Pkg().Path() != pkgPath { + return false + } + name := obj.Name() + for _, n := range names { + if name == n { + return true + } + } + return false +} diff --git a/go/analysis/passes/loopclosure/loopclosure.go b/go/analysis/passes/loopclosure/loopclosure.go index 5620c35faaa..fbcdc223dfe 100644 --- a/go/analysis/passes/loopclosure/loopclosure.go +++ b/go/analysis/passes/loopclosure/loopclosure.go @@ -359,20 +359,5 @@ func isMethodCall(info *types.Info, expr ast.Expr, pkgPath, typeName, method str if ptr, ok := recv.Type().(*types.Pointer); ok { rtype = ptr.Elem() } - named, ok := rtype.(*types.Named) - if !ok { - return false - } - if named.Obj().Name() != typeName { - return false - } - pkg := f.Pkg() - if pkg == nil { - return false - } - if pkg.Path() != pkgPath { - return false - } - - return true + return analysisutil.IsNamedType(rtype, pkgPath, typeName) } diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index b2b8c67c755..070654f0124 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -511,15 +511,10 @@ func isFormatter(typ types.Type) bool { sig := fn.Type().(*types.Signature) return sig.Params().Len() == 2 && sig.Results().Len() == 0 && - isNamed(sig.Params().At(0).Type(), "fmt", "State") && + analysisutil.IsNamedType(sig.Params().At(0).Type(), "fmt", "State") && types.Identical(sig.Params().At(1).Type(), types.Typ[types.Rune]) } -func isNamed(T types.Type, pkgpath, name string) bool { - named, ok := T.(*types.Named) - return ok && named.Obj().Pkg().Path() == pkgpath && named.Obj().Name() == name -} - // formatState holds the parsed representation of a printf directive such as "%3.*[4]d". // It is constructed by parsePrintfVerb. type formatState struct { diff --git a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go index 27677139e1f..b3c713f561c 100644 --- a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go +++ b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go @@ -68,11 +68,7 @@ func isReflectValue(pass *analysis.Pass, e ast.Expr) bool { return false } // See if the type is reflect.Value - named, ok := tv.Type.(*types.Named) - if !ok { - return false - } - if obj := named.Obj(); obj == nil || obj.Pkg() == nil || obj.Pkg().Path() != "reflect" || obj.Name() != "Value" { + if !analysisutil.IsNamedType(tv.Type, "reflect", "Value") { return false } if _, ok := e.(*ast.CompositeLit); ok { diff --git a/go/analysis/passes/slog/slog.go b/go/analysis/passes/slog/slog.go index 92c1da8ef4a..a1323c3e666 100644 --- a/go/analysis/passes/slog/slog.go +++ b/go/analysis/passes/slog/slog.go @@ -139,7 +139,7 @@ func run(pass *analysis.Pass) (any, error) { } func isAttr(t types.Type) bool { - return isNamed(t, "log/slog", "Attr") + return analysisutil.IsNamedType(t, "log/slog", "Attr") } // shortName returns a name for the function that is shorter than FullName. @@ -232,12 +232,3 @@ func isMethodExpr(info *types.Info, c *ast.CallExpr) bool { sel := info.Selections[s] return sel != nil && sel.Kind() == types.MethodExpr } - -// isNamed reports whether t is exactly a named type in a package with a given path. -func isNamed(t types.Type, path, name string) bool { - if n, ok := t.(*types.Named); ok { - obj := n.Obj() - return obj.Pkg() != nil && obj.Pkg().Path() == path && obj.Name() == name - } - return false -} diff --git a/go/analysis/passes/tests/tests.go b/go/analysis/passes/tests/tests.go index 9589a46a5ac..d0b0ebb1011 100644 --- a/go/analysis/passes/tests/tests.go +++ b/go/analysis/passes/tests/tests.go @@ -257,13 +257,7 @@ func isTestingType(typ types.Type, testingType string) bool { if !ok { return false } - named, ok := ptr.Elem().(*types.Named) - if !ok { - return false - } - obj := named.Obj() - // obj.Pkg is nil for the error type. - return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == testingType + return analysisutil.IsNamedType(ptr.Elem(), "testing", testingType) } // Validate that fuzz target function's arguments are of accepted types. diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go index e43ac20782e..a1ddeb832ad 100644 --- a/go/analysis/passes/unsafeptr/unsafeptr.go +++ b/go/analysis/passes/unsafeptr/unsafeptr.go @@ -104,8 +104,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { } switch sel.Sel.Name { case "Pointer", "UnsafeAddr": - t, ok := info.Types[sel.X].Type.(*types.Named) - if ok && t.Obj().Pkg().Path() == "reflect" && t.Obj().Name() == "Value" { + if analysisutil.IsNamedType(info.Types[sel.X].Type, "reflect", "Value") { return true } } @@ -153,13 +152,5 @@ func hasBasicType(info *types.Info, x ast.Expr, kind types.BasicKind) bool { // isReflectHeader reports whether t is reflect.SliceHeader or reflect.StringHeader. func isReflectHeader(t types.Type) bool { - if named, ok := t.(*types.Named); ok { - if obj := named.Obj(); obj.Pkg() != nil && obj.Pkg().Path() == "reflect" { - switch obj.Name() { - case "SliceHeader", "StringHeader": - return true - } - } - } - return false + return analysisutil.IsNamedType(t, "reflect", "SliceHeader", "StringHeader") } From a3b5082fb05ec5017f52a0292989f3827dc092f5 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Sat, 7 Oct 2023 09:26:03 -0400 Subject: [PATCH 003/100] go/analysis/passes/appends: improve check for append builtin To find calls to the `append` builtin, use typeutil.Callee instead of looking at the parse tree. In general, using typeutil.Callee is more robust. For example, it will find calls where the function is parenthesized. Here it may not matter, because a call like (append)(arg) is presumably illegal, since arguably the builtin is used in something other than a call expression. (As of this writing, that code crashes the compiler.) But I think it is still preferable, if only as a model for subsequent authors of vet checks. Also, add a test where `append` is redefined as a variable. This doesn't cause the original code to fail, but it is a new case that wasn't previously covered. Change-Id: Ic1c45c6586c2e58a804fb960ed16072545d6095c Reviewed-on: https://go-review.googlesource.com/c/tools/+/533575 Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot Run-TryBot: Jonathan Amsterdam --- go/analysis/passes/appends/appends.go | 10 ++++------ go/analysis/passes/appends/testdata/src/b/b.go | 6 ++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/go/analysis/passes/appends/appends.go b/go/analysis/passes/appends/appends.go index f0b90a4920e..6976f0d9090 100644 --- a/go/analysis/passes/appends/appends.go +++ b/go/analysis/passes/appends/appends.go @@ -15,6 +15,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/go/types/typeutil" ) //go:embed doc.go @@ -36,12 +37,9 @@ func run(pass *analysis.Pass) (interface{}, error) { } inspect.Preorder(nodeFilter, func(n ast.Node) { call := n.(*ast.CallExpr) - if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" { - if _, ok := pass.TypesInfo.Uses[ident].(*types.Builtin); ok { - if len(call.Args) == 1 { - pass.ReportRangef(call, "append with no values") - } - } + b, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Builtin) + if ok && b.Name() == "append" && len(call.Args) == 1 { + pass.ReportRangef(call, "append with no values") } }) diff --git a/go/analysis/passes/appends/testdata/src/b/b.go b/go/analysis/passes/appends/testdata/src/b/b.go index 87a04c4a7bd..b4e99d44acc 100644 --- a/go/analysis/passes/appends/testdata/src/b/b.go +++ b/go/analysis/passes/appends/testdata/src/b/b.go @@ -16,3 +16,9 @@ func userdefine() { sli = append(sli, 4, 5, 6) sli = append(sli) } + +func localvar() { + append := func(int) int { return 0 } + a := append(1) + _ = a +} From 59ac17fcd9b4fdc33b3a3136bfc20ce0f90ef034 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Sat, 7 Oct 2023 21:07:30 -0400 Subject: [PATCH 004/100] go/analysis/passes/internal/analysisutil: remove Unparen It duplicates astutil.Unparen. Change-Id: I5e54003e781de9d8da72b7f47789f2033810a3ad Reviewed-on: https://go-review.googlesource.com/c/tools/+/533157 Reviewed-by: Alan Donovan Run-TryBot: Jonathan Amsterdam TryBot-Result: Gopher Robot --- go/analysis/passes/assign/assign.go | 3 ++- go/analysis/passes/cgocall/cgocall.go | 3 ++- go/analysis/passes/copylock/copylock.go | 5 +++-- go/analysis/passes/internal/analysisutil/util.go | 11 ----------- go/analysis/passes/unsafeptr/unsafeptr.go | 5 +++-- go/analysis/passes/unusedresult/unusedresult.go | 3 ++- 6 files changed, 12 insertions(+), 18 deletions(-) diff --git a/go/analysis/passes/assign/assign.go b/go/analysis/passes/assign/assign.go index 10489bea17e..3bfd501226f 100644 --- a/go/analysis/passes/assign/assign.go +++ b/go/analysis/passes/assign/assign.go @@ -18,6 +18,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" ) @@ -77,7 +78,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // isMapIndex returns true if e is a map index expression. func isMapIndex(info *types.Info, e ast.Expr) bool { - if idx, ok := analysisutil.Unparen(e).(*ast.IndexExpr); ok { + if idx, ok := astutil.Unparen(e).(*ast.IndexExpr); ok { if typ := info.Types[idx.X].Type; typ != nil { _, ok := typ.Underlying().(*types.Map) return ok diff --git a/go/analysis/passes/cgocall/cgocall.go b/go/analysis/passes/cgocall/cgocall.go index 98d9a777a79..4e864397574 100644 --- a/go/analysis/passes/cgocall/cgocall.go +++ b/go/analysis/passes/cgocall/cgocall.go @@ -19,6 +19,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" + "golang.org/x/tools/go/ast/astutil" ) const debug = false @@ -64,7 +65,7 @@ func checkCgo(fset *token.FileSet, f *ast.File, info *types.Info, reportf func(t // Is this a C.f() call? var name string - if sel, ok := analysisutil.Unparen(call.Fun).(*ast.SelectorExpr); ok { + if sel, ok := astutil.Unparen(call.Fun).(*ast.SelectorExpr); ok { if id, ok := sel.X.(*ast.Ident); ok && id.Name == "C" { name = sel.Sel.Name } diff --git a/go/analysis/passes/copylock/copylock.go b/go/analysis/passes/copylock/copylock.go index db70dea0f17..2eeb0a330ac 100644 --- a/go/analysis/passes/copylock/copylock.go +++ b/go/analysis/passes/copylock/copylock.go @@ -16,6 +16,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/typeparams" ) @@ -223,7 +224,7 @@ func (path typePath) String() string { } func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath { - x = analysisutil.Unparen(x) // ignore parens on rhs + x = astutil.Unparen(x) // ignore parens on rhs if _, ok := x.(*ast.CompositeLit); ok { return nil @@ -233,7 +234,7 @@ func lockPathRhs(pass *analysis.Pass, x ast.Expr) typePath { return nil } if star, ok := x.(*ast.StarExpr); ok { - if _, ok := analysisutil.Unparen(star.X).(*ast.CallExpr); ok { + if _, ok := astutil.Unparen(star.X).(*ast.CallExpr); ok { // A call may return a pointer to a zero value. return nil } diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go index eb0ecd6a302..acb20fd7629 100644 --- a/go/analysis/passes/internal/analysisutil/util.go +++ b/go/analysis/passes/internal/analysisutil/util.go @@ -55,17 +55,6 @@ func HasSideEffects(info *types.Info, e ast.Expr) bool { return !safe } -// Unparen returns e with any enclosing parentheses stripped. -func Unparen(e ast.Expr) ast.Expr { - for { - p, ok := e.(*ast.ParenExpr) - if !ok { - return e - } - e = p.X - } -} - // ReadFile reads a file and adds it to the FileSet // so that we can report errors against it using lineStart. func ReadFile(fset *token.FileSet, filename string) ([]byte, *token.File, error) { diff --git a/go/analysis/passes/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go index a1ddeb832ad..32e71ef979d 100644 --- a/go/analysis/passes/unsafeptr/unsafeptr.go +++ b/go/analysis/passes/unsafeptr/unsafeptr.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/analysis/passes/internal/analysisutil" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" ) @@ -68,7 +69,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // Check unsafe.Pointer safety rules according to // https://golang.org/pkg/unsafe/#Pointer. - switch x := analysisutil.Unparen(x).(type) { + switch x := astutil.Unparen(x).(type) { case *ast.SelectorExpr: // "(6) Conversion of a reflect.SliceHeader or // reflect.StringHeader Data field to or from Pointer." @@ -117,7 +118,7 @@ func isSafeUintptr(info *types.Info, x ast.Expr) bool { // isSafeArith reports whether x is a pointer arithmetic expression that is safe // to convert to unsafe.Pointer. func isSafeArith(info *types.Info, x ast.Expr) bool { - switch x := analysisutil.Unparen(x).(type) { + switch x := astutil.Unparen(x).(type) { case *ast.CallExpr: // Base case: initial conversion from unsafe.Pointer to uintptr. return len(x.Args) == 1 && diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index cb487a21775..fd2d5300f9e 100644 --- a/go/analysis/passes/unusedresult/unusedresult.go +++ b/go/analysis/passes/unusedresult/unusedresult.go @@ -24,6 +24,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" ) @@ -82,7 +83,7 @@ func run(pass *analysis.Pass) (interface{}, error) { (*ast.ExprStmt)(nil), } inspect.Preorder(nodeFilter, func(n ast.Node) { - call, ok := analysisutil.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr) + call, ok := astutil.Unparen(n.(*ast.ExprStmt).X).(*ast.CallExpr) if !ok { return // not a call statement } From f5fd4c914f79a8c8248e3c58aa40610df13291ff Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Oct 2023 08:01:47 -0400 Subject: [PATCH 005/100] go/analysis/passes/bools: use astutil.Unparen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CL 533178 added a use of analysisutil.Unparen, and CL 533157 removed analysisutil.Unparen, resulting in a broken build. Change-Id: I57090838f313fadf2fefed05e71bb89c81b86529 Reviewed-on: https://go-review.googlesource.com/c/tools/+/533735 Reviewed-by: Eli Bendersky LUCI-TryBot-Result: Go LUCI Reviewed-by: Daniel Martí Auto-Submit: Robert Findley --- go/analysis/passes/bools/bools.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/go/analysis/passes/bools/bools.go b/go/analysis/passes/bools/bools.go index 5ebafb7df3e..564329774ef 100644 --- a/go/analysis/passes/bools/bools.go +++ b/go/analysis/passes/bools/bools.go @@ -14,6 +14,7 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" + "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/inspector" ) @@ -168,7 +169,7 @@ func (op boolOp) checkSuspect(pass *analysis.Pass, exprs []ast.Expr) { // seen[e] is already true; any newly processed exprs are added to seen. func (op boolOp) split(e ast.Expr, seen map[*ast.BinaryExpr]bool) (exprs []ast.Expr) { for { - e = analysisutil.Unparen(e) + e = astutil.Unparen(e) if b, ok := e.(*ast.BinaryExpr); ok && b.Op == op.tok { seen[b] = true exprs = append(exprs, op.split(b.Y, seen)...) From 0b06fd80c120f5fd72ebea7e998be40e352a13e8 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Sat, 7 Oct 2023 22:22:08 -0400 Subject: [PATCH 006/100] cmd/gonew: skip Test if exec is unsupported The test can't run on platforms where exec is not implemented. Make similar skip messages a bit more detailed and consistent. For golang/go#61104. Change-Id: I40cecd428cf0a7b6195aee146df6ed92deea0950 Cq-Include-Trybots: luci.golang.try:x_tools-gotip-js-wasm Reviewed-on: https://go-review.googlesource.com/c/tools/+/533158 Reviewed-by: Dmitri Shuralyov Reviewed-by: Ian Lance Taylor LUCI-TryBot-Result: Go LUCI Auto-Submit: Dmitri Shuralyov --- cmd/godoc/godoc_test.go | 2 +- cmd/gonew/main_test.go | 4 ++++ gopls/internal/lsp/regtest/regtest.go | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/godoc/godoc_test.go b/cmd/godoc/godoc_test.go index 42582c4b228..b7b0e1ba8d9 100644 --- a/cmd/godoc/godoc_test.go +++ b/cmd/godoc/godoc_test.go @@ -47,7 +47,7 @@ var exe struct { func godocPath(t *testing.T) string { if !testenv.HasExec() { - t.Skipf("skipping test that requires exec") + t.Skipf("skipping test: exec not supported on %s/%s", runtime.GOOS, runtime.GOARCH) } exe.once.Do(func() { diff --git a/cmd/gonew/main_test.go b/cmd/gonew/main_test.go index 590bda0a1a7..142788b9d1a 100644 --- a/cmd/gonew/main_test.go +++ b/cmd/gonew/main_test.go @@ -17,6 +17,7 @@ import ( "testing" "golang.org/x/tools/internal/diffp" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/txtar" ) @@ -28,6 +29,9 @@ func init() { } func Test(t *testing.T) { + if !testenv.HasExec() { + t.Skipf("skipping test: exec not supported on %s/%s", runtime.GOOS, runtime.GOARCH) + } exe, err := os.Executable() if err != nil { t.Fatal(err) diff --git a/gopls/internal/lsp/regtest/regtest.go b/gopls/internal/lsp/regtest/regtest.go index 7def1d77da7..6e14b916766 100644 --- a/gopls/internal/lsp/regtest/regtest.go +++ b/gopls/internal/lsp/regtest/regtest.go @@ -104,7 +104,7 @@ func Main(m *testing.M, hook func(*source.Options)) { } if !testenv.HasExec() { - fmt.Printf("skipping all tests: exec not supported on %s\n", runtime.GOOS) + fmt.Printf("skipping all tests: exec not supported on %s/%s\n", runtime.GOOS, runtime.GOARCH) os.Exit(0) } testenv.ExitIfSmallMachine() From 5874869c2b4603f5d8e36c88e5507669085f2704 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Sat, 7 Oct 2023 10:26:40 -0400 Subject: [PATCH 007/100] go/analysis/passes/internal/analysisutil: add IsFunctionNamed Add a utility function that reports whether a types.Func refers to a particular function, by package and name. Use it to simplify several passes. Change-Id: I71d612b0c78352a78d4222d5f9a4d97fbe912060 Reviewed-on: https://go-review.googlesource.com/c/tools/+/533615 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Jonathan Amsterdam --- go/analysis/passes/atomic/atomic.go | 16 ++-------- go/analysis/passes/atomic/testdata/src/a/a.go | 7 ++-- go/analysis/passes/atomicalign/atomicalign.go | 32 +++++++------------ .../passes/atomicalign/testdata/src/a/a.go | 4 ++- .../passes/deepequalerrors/deepequalerrors.go | 7 ++-- go/analysis/passes/defers/defers.go | 4 +-- go/analysis/passes/defers/testdata/src/a/a.go | 4 +-- go/analysis/passes/errorsas/errorsas.go | 7 ++-- .../passes/internal/analysisutil/util.go | 21 ++++++++++++ .../reflectvaluecompare.go | 7 ++-- go/analysis/passes/sortslice/analyzer.go | 9 ++---- go/analysis/passes/timeformat/timeformat.go | 7 +--- .../passes/unusedresult/unusedresult.go | 1 - 13 files changed, 54 insertions(+), 72 deletions(-) diff --git a/go/analysis/passes/atomic/atomic.go b/go/analysis/passes/atomic/atomic.go index b40e081ec26..931f9ca7540 100644 --- a/go/analysis/passes/atomic/atomic.go +++ b/go/analysis/passes/atomic/atomic.go @@ -8,12 +8,12 @@ import ( _ "embed" "go/ast" "go/token" - "go/types" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/analysis/passes/internal/analysisutil" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" ) //go:embed doc.go @@ -52,18 +52,8 @@ func run(pass *analysis.Pass) (interface{}, error) { if !ok { continue } - sel, ok := call.Fun.(*ast.SelectorExpr) - if !ok { - continue - } - pkgIdent, _ := sel.X.(*ast.Ident) - pkgName, ok := pass.TypesInfo.Uses[pkgIdent].(*types.PkgName) - if !ok || pkgName.Imported().Path() != "sync/atomic" { - continue - } - - switch sel.Sel.Name { - case "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr": + fn := typeutil.StaticCallee(pass.TypesInfo, call) + if analysisutil.IsFunctionNamed(fn, "sync/atomic", "AddInt32", "AddInt64", "AddUint32", "AddUint64", "AddUintptr") { checkAtomicAddAssignment(pass, n.Lhs[i], call) } } diff --git a/go/analysis/passes/atomic/testdata/src/a/a.go b/go/analysis/passes/atomic/testdata/src/a/a.go index dc12bd012e0..e784605eb9b 100644 --- a/go/analysis/passes/atomic/testdata/src/a/a.go +++ b/go/analysis/passes/atomic/testdata/src/a/a.go @@ -14,9 +14,10 @@ type Counter uint64 func AtomicTests() { x := uint64(1) - x = atomic.AddUint64(&x, 1) // want "direct assignment to atomic value" - _, x = 10, atomic.AddUint64(&x, 1) // want "direct assignment to atomic value" - x, _ = atomic.AddUint64(&x, 1), 10 // want "direct assignment to atomic value" + x = atomic.AddUint64(&x, 1) // want "direct assignment to atomic value" + _, x = 10, atomic.AddUint64(&x, 1) // want "direct assignment to atomic value" + x, _ = atomic.AddUint64(&x, 1), 10 // want "direct assignment to atomic value" + x, _ = (atomic.AddUint64)(&x, 1), 10 // want "direct assignment to atomic value" y := &x *y = atomic.AddUint64(y, 1) // want "direct assignment to atomic value" diff --git a/go/analysis/passes/atomicalign/atomicalign.go b/go/analysis/passes/atomicalign/atomicalign.go index 01683e45a2b..aff6d25b3e1 100644 --- a/go/analysis/passes/atomicalign/atomicalign.go +++ b/go/analysis/passes/atomicalign/atomicalign.go @@ -18,6 +18,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/go/types/typeutil" ) const Doc = "check for non-64-bits-aligned arguments to sync/atomic functions" @@ -42,31 +43,20 @@ func run(pass *analysis.Pass) (interface{}, error) { nodeFilter := []ast.Node{ (*ast.CallExpr)(nil), } + funcNames := []string{ + "AddInt64", "AddUint64", + "LoadInt64", "LoadUint64", + "StoreInt64", "StoreUint64", + "SwapInt64", "SwapUint64", + "CompareAndSwapInt64", "CompareAndSwapUint64", + } inspect.Preorder(nodeFilter, func(node ast.Node) { call := node.(*ast.CallExpr) - sel, ok := call.Fun.(*ast.SelectorExpr) - if !ok { - return - } - pkgIdent, ok := sel.X.(*ast.Ident) - if !ok { - return - } - pkgName, ok := pass.TypesInfo.Uses[pkgIdent].(*types.PkgName) - if !ok || pkgName.Imported().Path() != "sync/atomic" { - return - } - - switch sel.Sel.Name { - case "AddInt64", "AddUint64", - "LoadInt64", "LoadUint64", - "StoreInt64", "StoreUint64", - "SwapInt64", "SwapUint64", - "CompareAndSwapInt64", "CompareAndSwapUint64": - + fn := typeutil.StaticCallee(pass.TypesInfo, call) + if analysisutil.IsFunctionNamed(fn, "sync/atomic", funcNames...) { // For all the listed functions, the expression to check is always the first function argument. - check64BitAlignment(pass, sel.Sel.Name, call.Args[0]) + check64BitAlignment(pass, fn.Name(), call.Args[0]) } }) diff --git a/go/analysis/passes/atomicalign/testdata/src/a/a.go b/go/analysis/passes/atomicalign/testdata/src/a/a.go index 45dd73d3ac5..deebc30d222 100644 --- a/go/analysis/passes/atomicalign/testdata/src/a/a.go +++ b/go/analysis/passes/atomicalign/testdata/src/a/a.go @@ -4,6 +4,7 @@ // This file contains tests for the atomic alignment checker. +//go:build arm || 386 // +build arm 386 package testdata @@ -102,7 +103,8 @@ func arrayAlignment() { atomic.LoadInt64(&a.b) // want "address of non 64-bit aligned field .b passed to atomic.LoadInt64" atomic.LoadInt64(&a.c) - atomic.LoadUint64(&a.e) // want "address of non 64-bit aligned field .e passed to atomic.LoadUint64" + atomic.LoadUint64(&a.e) // want "address of non 64-bit aligned field .e passed to atomic.LoadUint64" + (atomic.LoadUint64)(&a.e) // want "address of non 64-bit aligned field .e passed to atomic.LoadUint64" } func anonymousFieldAlignment() { diff --git a/go/analysis/passes/deepequalerrors/deepequalerrors.go b/go/analysis/passes/deepequalerrors/deepequalerrors.go index 3a1818764af..1a83bddbcec 100644 --- a/go/analysis/passes/deepequalerrors/deepequalerrors.go +++ b/go/analysis/passes/deepequalerrors/deepequalerrors.go @@ -46,11 +46,8 @@ func run(pass *analysis.Pass) (interface{}, error) { } inspect.Preorder(nodeFilter, func(n ast.Node) { call := n.(*ast.CallExpr) - fn, ok := typeutil.Callee(pass.TypesInfo, call).(*types.Func) - if !ok { - return - } - if fn.FullName() == "reflect.DeepEqual" && hasError(pass, call.Args[0]) && hasError(pass, call.Args[1]) { + fn, _ := typeutil.Callee(pass.TypesInfo, call).(*types.Func) + if analysisutil.IsFunctionNamed(fn, "reflect", "DeepEqual") && hasError(pass, call.Args[0]) && hasError(pass, call.Args[1]) { pass.ReportRangef(call, "avoid using reflect.DeepEqual with errors") } }) diff --git a/go/analysis/passes/defers/defers.go b/go/analysis/passes/defers/defers.go index ed2a122f2b3..5e8e80a6a77 100644 --- a/go/analysis/passes/defers/defers.go +++ b/go/analysis/passes/defers/defers.go @@ -7,7 +7,6 @@ package defers import ( _ "embed" "go/ast" - "go/types" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -36,8 +35,7 @@ func run(pass *analysis.Pass) (interface{}, error) { checkDeferCall := func(node ast.Node) bool { switch v := node.(type) { case *ast.CallExpr: - fn, ok := typeutil.Callee(pass.TypesInfo, v).(*types.Func) - if ok && fn.Name() == "Since" && fn.Pkg().Path() == "time" { + if analysisutil.IsFunctionNamed(typeutil.StaticCallee(pass.TypesInfo, v), "time", "Since") { pass.Reportf(v.Pos(), "call to time.Since is not deferred") } case *ast.FuncLit: diff --git a/go/analysis/passes/defers/testdata/src/a/a.go b/go/analysis/passes/defers/testdata/src/a/a.go index e8bc8cde3ba..6f0118a3e98 100644 --- a/go/analysis/passes/defers/testdata/src/a/a.go +++ b/go/analysis/passes/defers/testdata/src/a/a.go @@ -26,8 +26,8 @@ func good() { defer fmt.Println(evalBefore) do := func(f func()) {} defer do(func() { time.Since(now) }) - defer fmt.Println(Since()) // OK because Since function is not in module time - + defer fmt.Println(Since()) // OK because Since function is not in module time + defer copy([]int(nil), []int{1}) // check that a builtin doesn't cause a panic } type y struct{} diff --git a/go/analysis/passes/errorsas/errorsas.go b/go/analysis/passes/errorsas/errorsas.go index 2fcbdfafb64..43996b80a5a 100644 --- a/go/analysis/passes/errorsas/errorsas.go +++ b/go/analysis/passes/errorsas/errorsas.go @@ -51,15 +51,12 @@ func run(pass *analysis.Pass) (interface{}, error) { inspect.Preorder(nodeFilter, func(n ast.Node) { call := n.(*ast.CallExpr) fn := typeutil.StaticCallee(pass.TypesInfo, call) - if fn == nil { - return // not a static call + if !analysisutil.IsFunctionNamed(fn, "errors", "As") { + return } if len(call.Args) < 2 { return // not enough arguments, e.g. called with return values of another function } - if fn.FullName() != "errors.As" { - return - } if err := checkAsTarget(pass, call.Args[1]); err != nil { pass.ReportRangef(call, "%v", err) } diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go index acb20fd7629..1bc429322af 100644 --- a/go/analysis/passes/internal/analysisutil/util.go +++ b/go/analysis/passes/internal/analysisutil/util.go @@ -129,3 +129,24 @@ func IsNamedType(t types.Type, pkgPath string, names ...string) bool { } return false } + +// IsFunctionNamed reports whether f is a top-level function defined in the +// given package and has one of the given names. +// It returns false if f is nil or a method. +func IsFunctionNamed(f *types.Func, pkgPath string, names ...string) bool { + if f == nil { + return false + } + if f.Pkg().Path() != pkgPath { + return false + } + if f.Type().(*types.Signature).Recv() != nil { + return false + } + for _, n := range names { + if f.Name() == n { + return true + } + } + return false +} diff --git a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go index b3c713f561c..6789d73579a 100644 --- a/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go +++ b/go/analysis/passes/reflectvaluecompare/reflectvaluecompare.go @@ -49,11 +49,8 @@ func run(pass *analysis.Pass) (interface{}, error) { } } case *ast.CallExpr: - fn, ok := typeutil.Callee(pass.TypesInfo, n).(*types.Func) - if !ok { - return - } - if fn.FullName() == "reflect.DeepEqual" && (isReflectValue(pass, n.Args[0]) || isReflectValue(pass, n.Args[1])) { + fn, _ := typeutil.Callee(pass.TypesInfo, n).(*types.Func) + if analysisutil.IsFunctionNamed(fn, "reflect", "DeepEqual") && (isReflectValue(pass, n.Args[0]) || isReflectValue(pass, n.Args[1])) { pass.ReportRangef(n, "avoid using reflect.DeepEqual with reflect.Value") } } diff --git a/go/analysis/passes/sortslice/analyzer.go b/go/analysis/passes/sortslice/analyzer.go index 1fe206b0fc3..6c151a02c16 100644 --- a/go/analysis/passes/sortslice/analyzer.go +++ b/go/analysis/passes/sortslice/analyzer.go @@ -47,12 +47,7 @@ func run(pass *analysis.Pass) (interface{}, error) { inspect.Preorder(nodeFilter, func(n ast.Node) { call := n.(*ast.CallExpr) fn, _ := typeutil.Callee(pass.TypesInfo, call).(*types.Func) - if fn == nil { - return - } - - fnName := fn.FullName() - if fnName != "sort.Slice" && fnName != "sort.SliceStable" && fnName != "sort.SliceIsSorted" { + if !analysisutil.IsFunctionNamed(fn, "sort", "Slice", "SliceStable", "SliceIsSorted") { return } @@ -131,7 +126,7 @@ func run(pass *analysis.Pass) (interface{}, error) { pass.Report(analysis.Diagnostic{ Pos: call.Pos(), End: call.End(), - Message: fmt.Sprintf("%s's argument must be a slice; is called with %s", fnName, typ.String()), + Message: fmt.Sprintf("%s's argument must be a slice; is called with %s", fn.FullName(), typ.String()), SuggestedFixes: fixes, }) }) diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go index c45b9fa54bc..59d668b6c8b 100644 --- a/go/analysis/passes/timeformat/timeformat.go +++ b/go/analysis/passes/timeformat/timeformat.go @@ -105,12 +105,7 @@ func isTimeDotFormat(f *types.Func) bool { } func isTimeDotParse(f *types.Func) bool { - if f.Name() != "Parse" || f.Pkg().Path() != "time" { - return false - } - // Verify that there is no receiver. - sig, ok := f.Type().(*types.Signature) - return ok && sig.Recv() == nil + return analysisutil.IsFunctionNamed(f, "time", "Parse") } // badFormatAt return the start of a bad format in e or -1 if no bad format is found. diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index fd2d5300f9e..7f79b4a7543 100644 --- a/go/analysis/passes/unusedresult/unusedresult.go +++ b/go/analysis/passes/unusedresult/unusedresult.go @@ -93,7 +93,6 @@ func run(pass *analysis.Pass) (interface{}, error) { if !ok { return // e.g. var or builtin } - if sig := fn.Type().(*types.Signature); sig.Recv() != nil { // method (e.g. foo.String()) if types.Identical(sig, sigNoArgsStringResult) { From f6d8589ec3d32e676022fe9a9d62e6f141cafde9 Mon Sep 17 00:00:00 2001 From: Cherry Mui Date: Mon, 9 Oct 2023 12:30:11 -0400 Subject: [PATCH 008/100] cmd/compilebench: pass linker flags to prebuild Some linker flag, e.g. -linkmode=external, can change the go command's behavior, e.g. which packages to include. Pass the linker flags to prebuild, as well as "go list" for constructing the importcfg file. This ensures that in -linkmode=external mode runtime/cgo is built and included, avoiding a linker warning. Fixes golang/go#63383. Change-Id: I185e8dd2e6de7601a343739186117bce3c19e492 Reviewed-on: https://go-review.googlesource.com/c/tools/+/533855 LUCI-TryBot-Result: Go LUCI Reviewed-by: Than McIntosh --- cmd/compilebench/main.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/cmd/compilebench/main.go b/cmd/compilebench/main.go index 681ffd346b2..15323c2ee7e 100644 --- a/cmd/compilebench/main.go +++ b/cmd/compilebench/main.go @@ -350,7 +350,7 @@ func (c compile) run(name string, count int) error { return err } - importcfg, err := genImportcfgFile(c.dir, false) + importcfg, err := genImportcfgFile(c.dir, "", false) // TODO: pass compiler flags? if err != nil { return err } @@ -418,12 +418,19 @@ func (r link) run(name string, count int) error { } // Build dependencies. - out, err := exec.Command(*flagGoCmd, "build", "-o", "/dev/null", r.dir).CombinedOutput() + ldflags := *flagLinkerFlags + if r.flags != "" { + if ldflags != "" { + ldflags += " " + } + ldflags += r.flags + } + out, err := exec.Command(*flagGoCmd, "build", "-o", "/dev/null", "-ldflags="+ldflags, r.dir).CombinedOutput() if err != nil { return fmt.Errorf("go build -a %s: %v\n%s", r.dir, err, out) } - importcfg, err := genImportcfgFile(r.dir, true) + importcfg, err := genImportcfgFile(r.dir, "-ldflags="+ldflags, true) if err != nil { return err } @@ -643,15 +650,19 @@ func genSymAbisFile(pkg *Pkg, symAbisFile, incdir string) error { // genImportcfgFile generates an importcfg file for building package // dir. Returns the generated importcfg file path (or empty string // if the package has no dependency). -func genImportcfgFile(dir string, full bool) (string, error) { +func genImportcfgFile(dir string, flags string, full bool) (string, error) { need := "{{.Imports}}" if full { // for linking, we need transitive dependencies need = "{{.Deps}}" } + if flags == "" { + flags = "--" // passing "" to go list, it will match to the current directory + } + // find imported/dependent packages - cmd := exec.Command(*flagGoCmd, "list", "-f", need, dir) + cmd := exec.Command(*flagGoCmd, "list", "-f", need, flags, dir) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { @@ -667,7 +678,7 @@ func genImportcfgFile(dir string, full bool) (string, error) { } // build importcfg for imported packages - cmd = exec.Command(*flagGoCmd, "list", "-export", "-f", "{{if .Export}}packagefile {{.ImportPath}}={{.Export}}{{end}}") + cmd = exec.Command(*flagGoCmd, "list", "-export", "-f", "{{if .Export}}packagefile {{.ImportPath}}={{.Export}}{{end}}", flags) cmd.Args = append(cmd.Args, strings.Fields(string(out))...) cmd.Stderr = os.Stderr out, err = cmd.Output() From fda3fe3aa61fded83939a05bfa4352a08209d89a Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 9 Oct 2023 09:33:23 -0400 Subject: [PATCH 009/100] gopls/internal/lsp: use the correct options for semantic tokens legend For its server capabilities, gopls simply reflects back to the client the semantic token types that the client supports. Previously, this logic relied on the fact that server options were mutated while processing client capabilities. In CL 526417, this was noticed as potentially racy and mutation was updated to occur on a cloned copy of the options. This broke the semantic tokens legend. Fix this, and update the semantic tokens regression tests to use the server-supplied legend, which would have caught the regression. Fixes golang/go#63291 Change-Id: Ieb0b78fd0f4edd933aff950fc8e3450cb3e21560 Reviewed-on: https://go-review.googlesource.com/c/tools/+/533755 Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI Reviewed-by: Peter Weinberger --- gopls/internal/lsp/fake/editor.go | 79 ++++++++++++- gopls/internal/lsp/general.go | 6 +- .../regtest/misc/semantictokens_test.go | 109 +++++------------- 3 files changed, 107 insertions(+), 87 deletions(-) diff --git a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go index ccee51e6867..b8e69011d99 100644 --- a/gopls/internal/lsp/fake/editor.go +++ b/gopls/internal/lsp/fake/editor.go @@ -44,7 +44,8 @@ type Editor struct { config EditorConfig // editor configuration buffers map[string]buffer // open buffers (relative path -> buffer content) serverCapabilities protocol.ServerCapabilities // capabilities / options - watchPatterns []*glob.Glob // glob patterns to watch + semTokOpts protocol.SemanticTokensOptions + watchPatterns []*glob.Glob // glob patterns to watch // Call metrics for the purpose of expectations. This is done in an ad-hoc // manner for now. Perhaps in the future we should do something more @@ -306,8 +307,13 @@ func (e *Editor) initialize(ctx context.Context) error { if err != nil { return fmt.Errorf("initialize: %w", err) } + semTokOpts, err := marshalUnmarshal[protocol.SemanticTokensOptions](resp.Capabilities.SemanticTokensProvider) + if err != nil { + return fmt.Errorf("unmarshalling semantic tokens options: %v", err) + } e.mu.Lock() e.serverCapabilities = resp.Capabilities + e.semTokOpts = semTokOpts e.mu.Unlock() if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { @@ -318,6 +324,19 @@ func (e *Editor) initialize(ctx context.Context) error { return nil } +// marshalUnmarshal is a helper to json Marshal and then Unmarshal as a +// different type. Used to work around cases where our protocol types are not +// specific. +func marshalUnmarshal[T any](v any) (T, error) { + var t T + data, err := json.Marshal(v) + if err != nil { + return t, err + } + err = json.Unmarshal(data, &t) + return t, err +} + // HasCommand reports whether the connected server supports the command with the given ID. func (e *Editor) HasCommand(id string) bool { for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { @@ -1492,3 +1511,61 @@ func (e *Editor) DocumentHighlight(ctx context.Context, loc protocol.Location) ( return e.Server.DocumentHighlight(ctx, params) } + +// SemanticTokens invokes textDocument/semanticTokens/full, and interprets its +// result. +func (e *Editor) SemanticTokens(ctx context.Context, path string) ([]SemanticToken, error) { + p := &protocol.SemanticTokensParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: e.sandbox.Workdir.URI(path), + }, + } + resp, err := e.Server.SemanticTokensFull(ctx, p) + if err != nil { + return nil, err + } + content, ok := e.BufferText(path) + if !ok { + return nil, fmt.Errorf("buffer %s is not open", path) + } + return e.interpretTokens(resp.Data, content), nil +} + +// A SemanticToken is an interpreted semantic token value. +type SemanticToken struct { + Token string + TokenType string + Mod string +} + +// Note: previously this function elided comment, string, and number tokens. +// Instead, filtering of token types should be done by the caller. +func (e *Editor) interpretTokens(x []uint32, contents string) []SemanticToken { + e.mu.Lock() + legend := e.semTokOpts.Legend + e.mu.Unlock() + lines := strings.Split(contents, "\n") + ans := []SemanticToken{} + line, col := 1, 1 + for i := 0; i < len(x); i += 5 { + line += int(x[i]) + col += int(x[i+1]) + if x[i] != 0 { // new line + col = int(x[i+1]) + 1 // 1-based column numbers + } + sz := x[i+2] + t := legend.TokenTypes[x[i+3]] + l := x[i+4] + var mods []string + for i, mod := range legend.TokenModifiers { + if l&(1< Date: Mon, 9 Oct 2023 16:07:17 -0400 Subject: [PATCH 010/100] go/types/internal/play: fix slice OOB when *ast.File is selected Change-Id: I31f2338a49270ac19d4a6f28ea513ad3ace943c9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/533856 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/types/internal/play/play.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go index 79cdc5afc7c..7a760e2d57d 100644 --- a/go/types/internal/play/play.go +++ b/go/types/internal/play/play.go @@ -174,7 +174,7 @@ func handleSelectJSON(w http.ResponseWriter, req *http.Request) { } // selection x.f information (if cursor is over .f) - for _, n := range path[:2] { + for _, n := range path[:min(2, len(path))] { if sel, ok := n.(*ast.SelectorExpr); ok { seln, ok := pkg.TypesInfo.Selections[sel] if ok { @@ -334,3 +334,12 @@ textarea { width: 6in; } body { color: gray; } div#out { font-family: monospace; font-size: 80%; } ` + +// TODO(adonovan): use go1.21 built-in. +func min(x, y int) int { + if x < y { + return x + } else { + return y + } +} From 0e4fc907c8122a61ea961c10eae4115641de5d82 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 5 Oct 2023 13:35:16 -0400 Subject: [PATCH 011/100] internal/refactor/inline: add missing spread context (return) A call f() where f() has tuple type may appear as the sole operand of a return statement. I forgot this case in the "reduce spread-context call" strategy. Plus a test. Fixes golang/go#63398 Change-Id: Ie851c977c3a2d237feabc95dbed4c50e6a1c96ad Reviewed-on: https://go-review.googlesource.com/c/tools/+/533176 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- internal/refactor/inline/inline.go | 13 ++++++++++--- internal/refactor/inline/inline_test.go | 6 ++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 9615ab43c59..02db0f5eb30 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -844,23 +844,30 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu // x, y = f() // or the sole argument to a spread call: // printf(f()) + // or spread return statement: + // return f() res.old = context switch context := context.(type) { case *ast.AssignStmt: - // Inv: the call is in Rhs[0], not Lhs. + // Inv: the call must be in Rhs[0], not Lhs. assign := shallowCopy(context) assign.Rhs = results res.new = assign case *ast.ValueSpec: - // Inv: the call is in Values[0], not Names. + // Inv: the call must be in Values[0], not Names. spec := shallowCopy(context) spec.Values = results res.new = spec case *ast.CallExpr: - // Inv: the Call is Args[0], not Fun. + // Inv: the call must be in Args[0], not Fun. call := shallowCopy(context) call.Args = results res.new = call + case *ast.ReturnStmt: + // Inv: the call must be Results[0]. + ret := shallowCopy(context) + ret.Results = results + res.new = ret default: return nil, fmt.Errorf("internal error: unexpected context %T for spread call", context) } diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 8362e445b66..2a20a55d32a 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -617,6 +617,12 @@ func TestSpreadCalls(t *testing.T) { ) }`, }, + { + "Spread call in return (#63398).", + `func f() (int, error) { return 0, nil }`, + `func _() (int, error) { return f() }`, + `func _() (int, error) { return 0, nil }`, + }, }) } From be4e4d62f43e2fb2b170bbb5111a583cd49963c5 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Tue, 10 Oct 2023 09:42:00 -0400 Subject: [PATCH 012/100] go/analysis/passes/internal/analysisutil: account for nil Func.Pkg Change-Id: Ib479903ac1d92aa65796ebdb4fdfc5b20999222b Reviewed-on: https://go-review.googlesource.com/c/tools/+/534137 Run-TryBot: Jonathan Amsterdam Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- go/analysis/passes/internal/analysisutil/util.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go index 1bc429322af..c0060753f9f 100644 --- a/go/analysis/passes/internal/analysisutil/util.go +++ b/go/analysis/passes/internal/analysisutil/util.go @@ -137,7 +137,7 @@ func IsFunctionNamed(f *types.Func, pkgPath string, names ...string) bool { if f == nil { return false } - if f.Pkg().Path() != pkgPath { + if f.Pkg() == nil || f.Pkg().Path() != pkgPath { return false } if f.Type().(*types.Signature).Recv() != nil { From b268156353131f2544a51a7076c76f92f1256e63 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Tue, 10 Oct 2023 11:10:38 -0400 Subject: [PATCH 013/100] gopls: allow all drive letters in cache/filemap_test.go On Windows paths may start with a drive letter. The old code only tested for C:, the new code allows any drive letter. Fixes: golang/go#63363 Change-Id: I6dd890087ea7734fb992234fc25d0cc1f8a52f77 Reviewed-on: https://go-review.googlesource.com/c/tools/+/534138 Reviewed-by: Robert Findley Run-TryBot: Peter Weinberger TryBot-Result: Gopher Robot --- gopls/internal/lsp/cache/filemap_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/filemap_test.go index 3d5bab41c67..a1d10af2427 100644 --- a/gopls/internal/lsp/cache/filemap_test.go +++ b/gopls/internal/lsp/cache/filemap_test.go @@ -7,7 +7,6 @@ package cache import ( "path/filepath" "sort" - "strings" "testing" "github.com/google/go-cmp/cmp" @@ -56,7 +55,12 @@ func TestFileMap(t *testing.T) { // Normalize paths for windows compatibility. normalize := func(path string) string { - return strings.TrimPrefix(filepath.ToSlash(path), "C:") // the span packages adds 'C:' + y := filepath.ToSlash(path) + // Windows paths may start with a drive letter + if len(y) > 2 && y[1] == ':' && y[0] >= 'A' && y[0] <= 'Z' { + y = y[2:] + } + return y } for _, test := range tests { From 7e7568c76577038c7eeac6868534ce9e0c759c9f Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Mon, 9 Oct 2023 16:54:10 -0400 Subject: [PATCH 014/100] go/analysis/passes/httpmux: check for enhanced ServeMux patterns Go 1.22 introduces some enhancements to net/http.ServeMux patterns, along with changes in how patterns are matched against escaped paths. If such patterns are inadvertently used with older Go versions, they will not fail to register; instead, the results of matching will be unexpected. This analysis pass, intended as a vet check, should catch many such cases. Because any enhanced pattern is also a valid (though odd) Go 1.21 pattern with a different meaning, this check will have false positives, so it should not be run as part of `go test`. Updates #61410. Change-Id: I3d511e303aacb960fd16df66bcd0e9f08adba47c Reviewed-on: https://go-review.googlesource.com/c/tools/+/534135 TryBot-Result: Gopher Robot Run-TryBot: Jonathan Amsterdam Reviewed-by: Alan Donovan --- go/analysis/passes/httpmux/httpmux.go | 186 ++++++++++++++++++ go/analysis/passes/httpmux/httpmux_test.go | 37 ++++ .../passes/httpmux/testdata/src/a/a.go | 47 +++++ 3 files changed, 270 insertions(+) create mode 100644 go/analysis/passes/httpmux/httpmux.go create mode 100644 go/analysis/passes/httpmux/httpmux_test.go create mode 100644 go/analysis/passes/httpmux/testdata/src/a/a.go diff --git a/go/analysis/passes/httpmux/httpmux.go b/go/analysis/passes/httpmux/httpmux.go new file mode 100644 index 00000000000..fa99296b5ec --- /dev/null +++ b/go/analysis/passes/httpmux/httpmux.go @@ -0,0 +1,186 @@ +// 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 httpmux + +import ( + "go/ast" + "go/constant" + "go/types" + "regexp" + "strings" + + "golang.org/x/mod/semver" + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/analysis/passes/internal/analysisutil" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" +) + +const Doc = `report using Go 1.22 enhanced ServeMux patterns in older Go versions + +The httpmux analysis is active for Go modules configured to run with Go 1.21 or +earlier versions. It reports calls to net/http.ServeMux.Handle and HandleFunc +methods whose patterns use features added in Go 1.22, like HTTP methods (such as +"GET") and wildcards. (See https://pkg.go.dev/net/http#ServeMux for details.) +Such patterns can be registered in older versions of Go, but will not behave as expected.` + +var Analyzer = &analysis.Analyzer{ + Name: "httpmux", + Doc: Doc, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/httpmux", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, +} + +var inTest bool // So Go version checks can be skipped during testing. + +func run(pass *analysis.Pass) (any, error) { + if !inTest { + // Check that Go version is 1.21 or earlier. + if goVersionAfter121(goVersion(pass.Pkg)) { + return nil, nil + } + } + if !analysisutil.Imports(pass.Pkg, "net/http") { + return nil, nil + } + // Look for calls to ServeMux.Handle or ServeMux.HandleFunc. + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.CallExpr)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + call := n.(*ast.CallExpr) + if isServeMuxRegisterCall(pass, call) { + pat, ok := stringConstantExpr(pass, call.Args[0]) + if ok && likelyEnhancedPattern(pat) { + pass.ReportRangef(call.Args[0], "possible enhanced ServeMux pattern used with Go version before 1.22 (update go.mod file?)") + } + } + }) + return nil, nil +} + +// isServeMuxRegisterCall reports whether call is a static call to one of: +// - net/http.Handle +// - net/http.HandleFunc +// - net/http.ServeMux.Handle +// - net/http.ServeMux.HandleFunc +// TODO(jba): consider expanding this to accommodate wrappers around these functions. +func isServeMuxRegisterCall(pass *analysis.Pass, call *ast.CallExpr) bool { + fn := typeutil.StaticCallee(pass.TypesInfo, call) + if fn == nil { + return false + } + if analysisutil.IsFunctionNamed(fn, "net/http", "Handle", "HandleFunc") { + return true + } + 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") +} + +func isMethodNamed(f *types.Func, pkgPath string, names ...string) bool { + if f == nil { + return false + } + if f.Pkg() == nil || f.Pkg().Path() != pkgPath { + return false + } + if f.Type().(*types.Signature).Recv() == nil { + return false + } + for _, n := range names { + if f.Name() == n { + return true + } + } + return false +} + +// stringConstantExpr returns expression's string constant value. +// +// ("", false) is returned if expression isn't a string +// constant. +func stringConstantExpr(pass *analysis.Pass, expr ast.Expr) (string, bool) { + lit := pass.TypesInfo.Types[expr].Value + if lit != nil && lit.Kind() == constant.String { + return constant.StringVal(lit), true + } + return "", false +} + +// A valid wildcard must start a segment, and its name must be valid Go +// identifier. +var wildcardRegexp = regexp.MustCompile(`/\{[_\pL][_\pL\p{Nd}]*(\.\.\.)?\}`) + +// likelyEnhancedPattern reports whether the ServeMux pattern pat probably +// contains either an HTTP method name or a wildcard, extensions added in Go 1.22. +func likelyEnhancedPattern(pat string) bool { + if strings.Contains(pat, " ") { + // A space in the pattern suggests that it begins with an HTTP method. + return true + } + return wildcardRegexp.MatchString(pat) +} + +func goVersionAfter121(goVersion string) bool { + if goVersion == "" { // Maybe the stdlib? + return true + } + version := versionFromGoVersion(goVersion) + return semver.Compare(version, "v1.21") > 0 +} + +func goVersion(pkg *types.Package) string { + // types.Package.GoVersion did not exist before Go 1.21. + if p, ok := any(pkg).(interface{ GoVersion() string }); ok { + return p.GoVersion() + } + return "" +} + +var ( + // Regexp for matching go tags. The groups are: + // 1 the major.minor version + // 2 the patch version, or empty if none + // 3 the entire prerelease, if present + // 4 the prerelease type ("beta" or "rc") + // 5 the prerelease number + tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc)(\d+))?$`) +) + +// Copied from pkgsite/internal/stdlib.VersionForTag. +func versionFromGoVersion(goVersion string) string { + // Special cases for go1. + if goVersion == "go1" { + return "v1.0.0" + } + if goVersion == "go1.0" { + return "" + } + m := tagRegexp.FindStringSubmatch(goVersion) + if m == nil { + return "" + } + version := "v" + m[1] + if m[2] != "" { + version += m[2] + } else { + version += ".0" + } + if m[3] != "" { + version += "-" + m[4] + "." + m[5] + } + return version +} diff --git a/go/analysis/passes/httpmux/httpmux_test.go b/go/analysis/passes/httpmux/httpmux_test.go new file mode 100644 index 00000000000..f2cb9c799c3 --- /dev/null +++ b/go/analysis/passes/httpmux/httpmux_test.go @@ -0,0 +1,37 @@ +// 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 httpmux + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + tests := []string{"a"} + inTest = true + analysistest.Run(t, testdata, Analyzer, tests...) +} + +func TestGoVersion(t *testing.T) { + for _, test := range []struct { + in string + want bool + }{ + {"", true}, + {"go1", false}, + {"go1.21", false}, + {"go1.21rc3", false}, + {"go1.22", true}, + {"go1.22rc1", true}, + } { + got := goVersionAfter121(test.in) + if got != test.want { + t.Errorf("%q: got %t, want %t", test.in, got, test.want) + } + } +} diff --git a/go/analysis/passes/httpmux/testdata/src/a/a.go b/go/analysis/passes/httpmux/testdata/src/a/a.go new file mode 100644 index 00000000000..ad5b3ba2a1c --- /dev/null +++ b/go/analysis/passes/httpmux/testdata/src/a/a.go @@ -0,0 +1,47 @@ +// 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. + +// This file contains tests for the httpmux checker. + +package a + +import "net/http" + +func _() { + http.HandleFunc("GET /x", nil) // want "enhanced ServeMux pattern" + http.HandleFunc("/{a}/b/", nil) // want "enhanced ServeMux pattern" + mux := http.NewServeMux() + mux.Handle("example.com/c/{d}", nil) // want "enhanced ServeMux pattern" + mux.HandleFunc("/{x...}", nil) // want "enhanced ServeMux pattern" + + // Should not match. + + // not an enhanced pattern + http.Handle("/", nil) + + // invalid wildcard; will panic in 1.22 + http.HandleFunc("/{/a/}", nil) + mux.Handle("/{1}", nil) + mux.Handle("/x{a}", nil) + + // right package, wrong method + http.ParseTime("GET /") + + // right function name, wrong package + Handle("GET /", nil) + HandleFunc("GET /", nil) + + // right method name, wrong type + var sm ServeMux + sm.Handle("example.com/c/{d}", nil) + sm.HandleFunc("method /{x...}", nil) +} + +func Handle(pat string, x any) {} +func HandleFunc(pat string, x any) {} + +type ServeMux struct{} + +func (*ServeMux) Handle(pat string, x any) {} +func (*ServeMux) HandleFunc(pat string, x any) {} From dbf6f4218216266c7697a78ca0786816cf512c80 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Tue, 10 Oct 2023 07:42:51 -0400 Subject: [PATCH 015/100] go/analysis/passes/httpmux: add command Updates #61410. Change-Id: I45ce06fe157228bc441039be656289119b427e5a Reviewed-on: https://go-review.googlesource.com/c/tools/+/534136 TryBot-Result: Gopher Robot Reviewed-by: Alan Donovan Run-TryBot: Jonathan Amsterdam --- go/analysis/passes/httpmux/cmd/httpmux/main.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 go/analysis/passes/httpmux/cmd/httpmux/main.go diff --git a/go/analysis/passes/httpmux/cmd/httpmux/main.go b/go/analysis/passes/httpmux/cmd/httpmux/main.go new file mode 100644 index 00000000000..e8a631157dc --- /dev/null +++ b/go/analysis/passes/httpmux/cmd/httpmux/main.go @@ -0,0 +1,13 @@ +// 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 httpmux command runs the httpmux analyzer. +package main + +import ( + "golang.org/x/tools/go/analysis/passes/httpmux" + "golang.org/x/tools/go/analysis/singlechecker" +) + +func main() { singlechecker.Main(httpmux.Analyzer) } From 187911b8731bed07e0a88a74d56fb8cbae71a9cb Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 5 Oct 2023 15:04:05 -0400 Subject: [PATCH 016/100] internal/refactor/inline: more precise SelectorExpr effects The treatment of MethodVal and FieldVal selections should mostly be consistent, as both apply the same implicit field selection operations to the explicit receiver. (Only the final implicit &v or *ptr operations differ.) However, Selection.Indirect has a bug that masked this similarity. This change handles SelectorExpr more carefully throughout. The indirectSelection method helper provides a non-buggy implementation of Selection.Indirect. The net result is that, in the callee effects analysis, selection operations that don't indirect a pointer are pure, and those that do indirect a pointer act like a read, allowing them to commute with other reads. Change-Id: I1500785a72d0b184e39ae0a51448a165789c3ca3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/533156 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- internal/refactor/inline/calleefx.go | 19 ++++++++++-- internal/refactor/inline/escape.go | 8 +++-- internal/refactor/inline/inline.go | 34 +++++++++----------- internal/refactor/inline/inline_test.go | 15 +++++++++ internal/refactor/inline/util.go | 41 +++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 25 deletions(-) diff --git a/internal/refactor/inline/calleefx.go b/internal/refactor/inline/calleefx.go index 6e3dc7994be..11246e5b969 100644 --- a/internal/refactor/inline/calleefx.go +++ b/internal/refactor/inline/calleefx.go @@ -116,10 +116,23 @@ func calleefx(info *types.Info, body *ast.BlockStmt, paramInfos map[*types.Var]* visitExpr(n.X) case *ast.SelectorExpr: - if sel, ok := info.Selections[n]; ok { + if seln, ok := info.Selections[n]; ok { visitExpr(n.X) - if sel.Indirect() { - effect(rinf) // indirect read x.f of heap variable + + // See types.SelectionKind for background. + switch seln.Kind() { + case types.MethodExpr: + // A method expression T.f acts like a + // reference to a func decl, + // so it doesn't read x until called. + + case types.MethodVal, types.FieldVal: + // A field or method value selection x.f + // reads x if the selection indirects a pointer. + + if indirectSelection(seln) { + effect(rinf) + } } } else { // qualified identifier: treat like unqualified diff --git a/internal/refactor/inline/escape.go b/internal/refactor/inline/escape.go index d05d2b927c0..795ad4feab6 100644 --- a/internal/refactor/inline/escape.go +++ b/internal/refactor/inline/escape.go @@ -72,9 +72,11 @@ func escape(info *types.Info, root ast.Node, f func(v *types.Var, escapes bool)) if sel, ok := n.Fun.(*ast.SelectorExpr); ok { if seln, ok := info.Selections[sel]; ok && seln.Kind() == types.MethodVal && - !seln.Indirect() && - is[*types.Pointer](seln.Obj().Type().(*types.Signature).Recv().Type()) { - lvalue(sel.X, true) // &x.f + isPointer(seln.Obj().Type().(*types.Signature).Recv().Type()) { + tArg, indirect := effectiveReceiver(seln) + if !indirect && !isPointer(tArg) { + lvalue(sel.X, true) // &x.f + } } } diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 02db0f5eb30..1882c12e3e8 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -1093,7 +1093,7 @@ func arguments(caller *Caller, calleeDecl *ast.FuncDecl, assign1 func(*types.Var debugFormatNode(caller.Fset, caller.Call.Fun), fld.Name()) } - if is[*types.Pointer](arg.typ.Underlying()) { + if isPointer(arg.typ) { arg.pure = false // implicit *ptr operation => impure } arg.expr = &ast.SelectorExpr{ @@ -1105,8 +1105,8 @@ func arguments(caller *Caller, calleeDecl *ast.FuncDecl, assign1 func(*types.Var } // Make * or & explicit. - argIsPtr := arg.typ != deref(arg.typ) - paramIsPtr := is[*types.Pointer](seln.Obj().Type().(*types.Signature).Recv().Type()) + argIsPtr := isPointer(arg.typ) + paramIsPtr := isPointer(seln.Obj().Type().(*types.Signature).Recv().Type()) if !argIsPtr && paramIsPtr { // &recv arg.expr = &ast.UnaryExpr{Op: token.AND, X: arg.expr} @@ -1910,27 +1910,23 @@ func pure(info *types.Info, assign1 func(*types.Var) bool, e ast.Expr) bool { return true case *ast.SelectorExpr: - if sel, ok := info.Selections[e]; ok { - switch sel.Kind() { + if seln, ok := info.Selections[e]; ok { + + // See types.SelectionKind for background. + switch seln.Kind() { case types.MethodExpr: // A method expression T.f acts like a // reference to a func decl, so it is pure. return true - case types.MethodVal: - // A method value x.f acts like a - // closure around a T.f(x, ...) call, - // so it is as pure as x. - return pure(e.X) - - case types.FieldVal: - // A field selection x.f is pure if - // x is pure and the selection does + case types.MethodVal, types.FieldVal: + // A field or method selection x.f is pure + // if x is pure and the selection does // not indirect a pointer. - return !sel.Indirect() && pure(e.X) + return !indirectSelection(seln) && pure(e.X) default: - panic(sel) + panic(seln) } } else { // A qualified identifier is @@ -1978,7 +1974,7 @@ func callsPureBuiltin(info *types.Info, call *ast.CallExpr) bool { // integer literals, unary negation, and selectors x.f where x is not // a pointer. But we would not wish to duplicate expressions that: // - have side effects (e.g. nearly all calls), -// - are not referentially transparent (e.g. &T{}, ptr.field), or +// - are not referentially transparent (e.g. &T{}, ptr.field, *ptr), or // - are long (e.g. "huge string literal"). func duplicable(info *types.Info, e ast.Expr) bool { switch e := e.(type) { @@ -2009,10 +2005,10 @@ func duplicable(info *types.Info, e ast.Expr) bool { return false case *ast.SelectorExpr: - if sel, ok := info.Selections[e]; ok { + if seln, ok := info.Selections[e]; ok { // A field or method selection x.f is referentially // transparent if it does not indirect a pointer. - return !sel.Indirect() + return !indirectSelection(seln) } // A qualified identifier pkg.Name is referentially transparent. return true diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 2a20a55d32a..319ea545eb8 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -721,6 +721,21 @@ func TestParameterBindingDecl(t *testing.T) { `func _() { f(g(1), g(2), g(3)) }`, `func _() { func(int, y any, z int) { defer g(0); println(int, y, z) }(g(1), g(2), g(3)) }`, }, + { + "An indirect method selection (*x).g acts as a read.", + `func f(x *T, y any) any { return x.g(y) }; type T struct{}; func (T) g(x any) any { return x }`, + `func _(x *T) { f(x, recover()) }`, + `func _(x *T) { + var y any = recover() + x.g(y) +}`, + }, + { + "A direct method selection x.g is pure.", + `func f(x *T, y any) any { return x.g(y) }; type T struct{}; func (*T) g(x any) any { return x }`, + `func _(x *T) { f(x, recover()) }`, + `func _(x *T) { x.g(recover()) }`, + }, }) } diff --git a/internal/refactor/inline/util.go b/internal/refactor/inline/util.go index 98d654eeb51..267ef745e32 100644 --- a/internal/refactor/inline/util.go +++ b/internal/refactor/inline/util.go @@ -12,6 +12,8 @@ import ( "go/types" "reflect" "strings" + + "golang.org/x/tools/internal/typeparams" ) func is[T any](x any) bool { @@ -117,3 +119,42 @@ func convert(T, x ast.Expr) *ast.CallExpr { Args: []ast.Expr{x}, } } + +// isPointer reports whether t is a pointer type. +func isPointer(t types.Type) bool { return t != deref(t) } + +// indirectSelection is like seln.Indirect() without bug #8353. +func indirectSelection(seln *types.Selection) bool { + // Work around bug #8353 in Selection.Indirect when Kind=MethodVal. + if seln.Kind() == types.MethodVal { + tArg, indirect := effectiveReceiver(seln) + if indirect { + return true + } + + tParam := seln.Obj().Type().(*types.Signature).Recv().Type() + return isPointer(tArg) && !isPointer(tParam) // implicit * + } + + return seln.Indirect() +} + +// effectiveReceiver returns the effective type of the method +// receiver after all implicit field selections (but not implicit * or +// & operations) have been applied. +// +// The boolean indicates whether any implicit field selection was indirect. +func effectiveReceiver(seln *types.Selection) (types.Type, bool) { + assert(seln.Kind() == types.MethodVal, "not MethodVal") + t := seln.Recv() + indices := seln.Index() + indirect := false + for _, index := range indices[:len(indices)-1] { + if tElem := deref(t); tElem != t { + indirect = true + t = tElem + } + t = typeparams.CoreType(t).(*types.Struct).Field(index).Type() + } + return t, indirect +} From ecbfa885b278478686e8b8efb52535e934c53ec5 Mon Sep 17 00:00:00 2001 From: Jonathan Amsterdam Date: Tue, 10 Oct 2023 19:02:05 -0400 Subject: [PATCH 017/100] go/analysis/passes/timeformat: simplify isTimeDotFormat Change-Id: I58d807654ca63114bf1295acdad40791592fa85f Reviewed-on: https://go-review.googlesource.com/c/tools/+/534396 Run-TryBot: Jonathan Amsterdam Reviewed-by: Alan Donovan TryBot-Result: Gopher Robot --- go/analysis/passes/timeformat/timeformat.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/go/analysis/passes/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go index 59d668b6c8b..eb84502bd99 100644 --- a/go/analysis/passes/timeformat/timeformat.go +++ b/go/analysis/passes/timeformat/timeformat.go @@ -88,20 +88,12 @@ func run(pass *analysis.Pass) (interface{}, error) { } func isTimeDotFormat(f *types.Func) bool { - if f.Name() != "Format" || f.Pkg().Path() != "time" { - return false - } - sig, ok := f.Type().(*types.Signature) - if !ok { + if f.Name() != "Format" || f.Pkg() == nil || f.Pkg().Path() != "time" { return false } // Verify that the receiver is time.Time. - recv := sig.Recv() - if recv == nil { - return false - } - named, ok := recv.Type().(*types.Named) - return ok && named.Obj().Name() == "Time" + recv := f.Type().(*types.Signature).Recv() + return recv != nil && analysisutil.IsNamedType(recv.Type(), "time", "Time") } func isTimeDotParse(f *types.Func) bool { From f38ff07b84d8553ade8c4f59c2c6a0ea6a5fdd38 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 11 Oct 2023 09:40:24 -0400 Subject: [PATCH 018/100] internal/refactor/inline: T{} is duplicable for struct/array This change makes empty composite literals of aggregate (struct/array) types duplicable. Nonempty literals remain nonduplicable on grounds of verbosity; and map and slice literals are nonduplicable because they allocate a new variable. Change-Id: I9e2d778e004fb4743fd242c4e81d00e55830a6bd Reviewed-on: https://go-review.googlesource.com/c/tools/+/534397 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- internal/refactor/inline/inline.go | 20 +++++++++- internal/refactor/inline/inline_test.go | 50 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 1882c12e3e8..618b179867b 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -1998,10 +1998,28 @@ func duplicable(info *types.Info, e ast.Expr) bool { case *ast.UnaryExpr: // e.g. +1, -1 return (e.Op == token.ADD || e.Op == token.SUB) && duplicable(info, e.X) + case *ast.CompositeLit: + // Empty struct or array literals T{} are duplicable. + // (Non-empty literals are too verbose, and slice/map + // literals allocate indirect variables.) + if len(e.Elts) == 0 { + switch info.TypeOf(e).Underlying().(type) { + case *types.Struct, *types.Array: + return true + } + } + return false + case *ast.CallExpr: // Don't treat a conversion T(x) as duplicable even // if x is duplicable because it could duplicate - // allocations. There may be cases to tease apart here. + // allocations. + // + // TODO(adonovan): there are cases to tease apart here: + // duplicating string([]byte) conversions increases + // allocation but doesn't change behavior, but the + // reverse, []byte(string), allocates a distinct array, + // which is observable return false case *ast.SelectorExpr: diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 319ea545eb8..189ac3f8861 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -391,6 +391,56 @@ func TestBasics(t *testing.T) { }) } +func TestDuplicable(t *testing.T) { + runTests(t, []testcase{ + { + "Empty strings are duplicable.", + `func f(s string) { print(s, s) }`, + `func _() { f("") }`, + `func _() { print("", "") }`, + }, + { + "Non-empty string literals are not duplicable.", + `func f(s string) { print(s, s) }`, + `func _() { f("hi") }`, + `func _() { + var s string = "hi" + print(s, s) +}`, + }, + { + "Empty array literals are duplicable.", + `func f(a [2]int) { print(a, a) }`, + `func _() { f([2]int{}) }`, + `func _() { print([2]int{}, [2]int{}) }`, + }, + { + "Non-empty array literals are not duplicable.", + `func f(a [2]int) { print(a, a) }`, + `func _() { f([2]int{1, 2}) }`, + `func _() { + var a [2]int = [2]int{1, 2} + print(a, a) +}`, + }, + { + "Empty struct literals are duplicable.", + `func f(s S) { print(s, s) }; type S struct { x int }`, + `func _() { f(S{}) }`, + `func _() { print(S{}, S{}) }`, + }, + { + "Non-empty struct literals are not duplicable.", + `func f(s S) { print(s, s) }; type S struct { x int }`, + `func _() { f(S{x: 1}) }`, + `func _() { + var s S = S{x: 1} + print(s, s) +}`, + }, + }) +} + func TestExprStmtReduction(t *testing.T) { runTests(t, []testcase{ { From 9b63f3d1df640d27dca483027da2c496b52e3c54 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 11 Oct 2023 14:05:52 -0400 Subject: [PATCH 019/100] gopls: upgrade x/telemetry dependency Change-Id: Ibd41434963b77bf58479229322dff77276fc3399 Reviewed-on: https://go-review.googlesource.com/c/tools/+/534615 Auto-Submit: Robert Findley Reviewed-by: Peter Weinberger LUCI-TryBot-Result: Go LUCI --- gopls/go.mod | 2 +- gopls/go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/gopls/go.mod b/gopls/go.mod index 866fb259581..051e7f81582 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -10,7 +10,7 @@ require ( golang.org/x/mod v0.13.0 golang.org/x/sync v0.4.0 golang.org/x/sys v0.13.0 - golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3 + golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052 golang.org/x/text v0.13.0 golang.org/x/tools v0.13.1-0.20230920233436-f9b8da7b22be golang.org/x/vuln v1.0.1 diff --git a/gopls/go.sum b/gopls/go.sum index 365fd0c1a4f..e22f5e522a8 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -46,6 +46,8 @@ golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3 h1:vxxQvncMbcRAtqHV5HsHGJkbya+BIOYIY+y6cdPZhzk= golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3/go.mod h1:ppZ76JTkRgJC2GQEgtVY3fiuJR+N8FU2MAlp+gfN1E4= +golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052 h1:1baVNneD/IRxmu8JQdBuki78zUqBtZxq8smZXQj0X2Y= +golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052/go.mod h1:6p4ScoNeC2dhpQ1nSSMmkZ7mEj5JQUSCyc0uExBp5T4= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From b9b97d982b0a16d0bc8d95fb1dc654120b0d4940 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 10 Oct 2023 12:19:55 -0400 Subject: [PATCH 020/100] go/types/objectpath: remove method sorting It was expensive, and unnecessary if we can rely on go/types and export data preserving source order (we can). Fixes golang/go#61443 Change-Id: I28d93c35f89eb751991c9d25aeb1c1904ba7b546 Reviewed-on: https://go-review.googlesource.com/c/tools/+/534139 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/analysis/unitchecker/unitchecker.go | 4 +- go/types/objectpath/objectpath.go | 117 +++++-------------------- gopls/internal/lsp/cache/analysis.go | 4 +- internal/facts/facts.go | 12 +-- internal/facts/facts_test.go | 10 +-- internal/typesinternal/objectpath.go | 24 ----- 6 files changed, 33 insertions(+), 138 deletions(-) delete mode 100644 internal/typesinternal/objectpath.go diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go index 53c3f4a806c..0a40652c1b5 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go @@ -319,7 +319,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re analyzers = filtered // Read facts from imported packages. - facts, err := facts.NewDecoder(pkg).Decode(false, makeFactImporter(cfg)) + facts, err := facts.NewDecoder(pkg).Decode(makeFactImporter(cfg)) if err != nil { return nil, err } @@ -418,7 +418,7 @@ func run(fset *token.FileSet, cfg *Config, analyzers []*analysis.Analyzer) ([]re results[i].diagnostics = act.diagnostics } - data := facts.Encode(false) + data := facts.Encode() if err := exportFacts(cfg, data); err != nil { return nil, fmt.Errorf("failed to export analysis facts: %v", err) } diff --git a/go/types/objectpath/objectpath.go b/go/types/objectpath/objectpath.go index fa5834baf72..e742ecc4644 100644 --- a/go/types/objectpath/objectpath.go +++ b/go/types/objectpath/objectpath.go @@ -26,13 +26,10 @@ package objectpath import ( "fmt" "go/types" - "sort" "strconv" "strings" - _ "unsafe" "golang.org/x/tools/internal/typeparams" - "golang.org/x/tools/internal/typesinternal" ) // A Path is an opaque name that identifies a types.Object @@ -123,20 +120,7 @@ func For(obj types.Object) (Path, error) { // An Encoder amortizes the cost of encoding the paths of multiple objects. // The zero value of an Encoder is ready to use. type Encoder struct { - scopeMemo map[*types.Scope][]types.Object // memoization of scopeObjects - namedMethodsMemo map[*types.Named][]*types.Func // memoization of namedMethods() - skipMethodSorting bool -} - -// Expose back doors so that gopls can avoid method sorting, which can dominate -// analysis on certain repositories. -// -// TODO(golang/go#61443): remove this. -func init() { - typesinternal.SkipEncoderMethodSorting = func(enc interface{}) { - enc.(*Encoder).skipMethodSorting = true - } - typesinternal.ObjectpathObject = object + scopeMemo map[*types.Scope][]types.Object // memoization of scopeObjects } // For returns the path to an object relative to its package, @@ -328,31 +312,18 @@ func (enc *Encoder) For(obj types.Object) (Path, error) { // Inspect declared methods of defined types. if T, ok := o.Type().(*types.Named); ok { path = append(path, opType) - if !enc.skipMethodSorting { - // Note that method index here is always with respect - // to canonical ordering of methods, regardless of how - // they appear in the underlying type. - for i, m := range enc.namedMethods(T) { - path2 := appendOpArg(path, opMethod, i) - if m == obj { - return Path(path2), nil // found declared method - } - if r := find(obj, m.Type(), append(path2, opType), nil); r != nil { - return Path(r), nil - } + // The method index here is always with respect + // to the underlying go/types data structures, + // which ultimately derives from source order + // and must be preserved by export data. + for i := 0; i < T.NumMethods(); i++ { + m := T.Method(i) + path2 := appendOpArg(path, opMethod, i) + if m == obj { + return Path(path2), nil // found declared method } - } else { - // This branch must match the logic in the branch above, using go/types - // APIs without sorting. - for i := 0; i < T.NumMethods(); i++ { - m := T.Method(i) - path2 := appendOpArg(path, opMethod, i) - if m == obj { - return Path(path2), nil // found declared method - } - if r := find(obj, m.Type(), append(path2, opType), nil); r != nil { - return Path(r), nil - } + if r := find(obj, m.Type(), append(path2, opType), nil); r != nil { + return Path(r), nil } } } @@ -448,22 +419,13 @@ func (enc *Encoder) concreteMethod(meth *types.Func) (Path, bool) { path = append(path, name...) path = append(path, opType) - if !enc.skipMethodSorting { - for i, m := range enc.namedMethods(named) { - if m == meth { - path = appendOpArg(path, opMethod, i) - return Path(path), true - } - } - } else { - // This branch must match the logic of the branch above, using go/types - // APIs without sorting. - for i := 0; i < named.NumMethods(); i++ { - m := named.Method(i) - if m == meth { - path = appendOpArg(path, opMethod, i) - return Path(path), true - } + // Method indices are w.r.t. the go/types data structures, + // ultimately deriving from source order, + // which is preserved by export data. + for i := 0; i < named.NumMethods(); i++ { + if named.Method(i) == meth { + path = appendOpArg(path, opMethod, i) + return Path(path), true } } @@ -576,12 +538,7 @@ func findTypeParam(obj types.Object, list *typeparams.TypeParamList, path []byte // Object returns the object denoted by path p within the package pkg. func Object(pkg *types.Package, p Path) (types.Object, error) { - return object(pkg, string(p), false) -} - -// Note: the skipMethodSorting parameter must match the value of -// Encoder.skipMethodSorting used during encoding. -func object(pkg *types.Package, pathstr string, skipMethodSorting bool) (types.Object, error) { + pathstr := string(p) if pathstr == "" { return nil, fmt.Errorf("empty path") } @@ -747,12 +704,7 @@ func object(pkg *types.Package, pathstr string, skipMethodSorting bool) (types.O if index >= t.NumMethods() { return nil, fmt.Errorf("method index %d out of range [0-%d)", index, t.NumMethods()) } - if skipMethodSorting { - obj = t.Method(index) - } else { - methods := namedMethods(t) // (unmemoized) - obj = methods[index] // Id-ordered - } + obj = t.Method(index) default: return nil, fmt.Errorf("cannot apply %q to %s (got %T, want interface or named)", code, t, t) @@ -779,33 +731,6 @@ func object(pkg *types.Package, pathstr string, skipMethodSorting bool) (types.O return obj, nil // success } -// namedMethods returns the methods of a Named type in ascending Id order. -func namedMethods(named *types.Named) []*types.Func { - methods := make([]*types.Func, named.NumMethods()) - for i := range methods { - methods[i] = named.Method(i) - } - sort.Slice(methods, func(i, j int) bool { - return methods[i].Id() < methods[j].Id() - }) - return methods -} - -// namedMethods is a memoization of the namedMethods function. Callers must not modify the result. -func (enc *Encoder) namedMethods(named *types.Named) []*types.Func { - m := enc.namedMethodsMemo - if m == nil { - m = make(map[*types.Named][]*types.Func) - enc.namedMethodsMemo = m - } - methods, ok := m[named] - if !ok { - methods = namedMethods(named) // allocates and sorts - m[named] = methods - } - return methods -} - // scopeObjects is a memoization of scope objects. // Callers must not modify the result. func (enc *Encoder) scopeObjects(scope *types.Scope) []types.Object { diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index dfb09951494..0466af33735 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -1201,7 +1201,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) { // by "deep" export data. Better still, use a "shallow" approach. // Read and decode analysis facts for each direct import. - factset, err := pkg.factsDecoder.Decode(true, func(pkgPath string) ([]byte, error) { + factset, err := pkg.factsDecoder.Decode(func(pkgPath string) ([]byte, error) { if !hasFacts { return nil, nil // analyzer doesn't use facts, so no vdeps } @@ -1343,7 +1343,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) { panic(fmt.Sprintf("%v: Pass.ExportPackageFact(%T) called after Run", act, fact)) } - factsdata := factset.Encode(true) + factsdata := factset.Encode() return result, &actionSummary{ Diagnostics: diagnostics, Facts: factsdata, diff --git a/internal/facts/facts.go b/internal/facts/facts.go index 8480ea062f7..f0aa97ec1bf 100644 --- a/internal/facts/facts.go +++ b/internal/facts/facts.go @@ -48,7 +48,6 @@ import ( "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/types/objectpath" - "golang.org/x/tools/internal/typesinternal" ) const debug = false @@ -205,9 +204,7 @@ type GetPackageFunc = func(pkgPath string) *types.Package // // Concurrent calls to Decode are safe, so long as the // [GetPackageFunc] (if any) is also concurrency-safe. -// -// TODO(golang/go#61443): eliminate skipMethodSorting one way or the other. -func (d *Decoder) Decode(skipMethodSorting bool, read func(pkgPath string) ([]byte, error)) (*Set, error) { +func (d *Decoder) Decode(read func(pkgPath string) ([]byte, error)) (*Set, error) { // Read facts from imported packages. // Facts may describe indirectly imported packages, or their objects. m := make(map[key]analysis.Fact) // one big bucket @@ -247,7 +244,7 @@ func (d *Decoder) Decode(skipMethodSorting bool, read func(pkgPath string) ([]by key := key{pkg: factPkg, t: reflect.TypeOf(f.Fact)} if f.Object != "" { // object fact - obj, err := typesinternal.ObjectpathObject(factPkg, string(f.Object), skipMethodSorting) + obj, err := objectpath.Object(factPkg, f.Object) if err != nil { // (most likely due to unexported object) // TODO(adonovan): audit for other possibilities. @@ -271,11 +268,8 @@ func (d *Decoder) Decode(skipMethodSorting bool, read func(pkgPath string) ([]by // // It may fail if one of the Facts could not be gob-encoded, but this is // a sign of a bug in an Analyzer. -func (s *Set) Encode(skipMethodSorting bool) []byte { +func (s *Set) Encode() []byte { encoder := new(objectpath.Encoder) - if skipMethodSorting { - typesinternal.SkipEncoderMethodSorting(encoder) - } // TODO(adonovan): opt: use a more efficient encoding // that avoids repeating PkgPath for each fact. diff --git a/internal/facts/facts_test.go b/internal/facts/facts_test.go index 7eb766e4ec3..4f1e8d60d55 100644 --- a/internal/facts/facts_test.go +++ b/internal/facts/facts_test.go @@ -311,7 +311,7 @@ func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) } // decode - facts, err := facts.NewDecoder(pkg).Decode(false, read) + facts, err := facts.NewDecoder(pkg).Decode(read) if err != nil { t.Fatalf("Decode failed: %v", err) } @@ -345,7 +345,7 @@ func testEncodeDecode(t *testing.T, files map[string]string, tests []pkgLookups) } // encode - factmap[pkg.Path()] = facts.Encode(false) + factmap[pkg.Path()] = facts.Encode() } } @@ -413,7 +413,7 @@ func TestFactFilter(t *testing.T) { } obj := pkg.Scope().Lookup("A") - s, err := facts.NewDecoder(pkg).Decode(false, func(pkgPath string) ([]byte, error) { return nil, nil }) + s, err := facts.NewDecoder(pkg).Decode(func(pkgPath string) ([]byte, error) { return nil, nil }) if err != nil { t.Fatal(err) } @@ -528,7 +528,7 @@ func TestMalformed(t *testing.T) { packages[pkg.Path()] = pkg // decode facts - facts, err := facts.NewDecoder(pkg).Decode(false, read) + facts, err := facts.NewDecoder(pkg).Decode(read) if err != nil { t.Fatalf("Decode failed: %v", err) } @@ -555,7 +555,7 @@ func TestMalformed(t *testing.T) { } // encode facts - factmap[pkg.Path()] = facts.Encode(false) + factmap[pkg.Path()] = facts.Encode() } }) } diff --git a/internal/typesinternal/objectpath.go b/internal/typesinternal/objectpath.go deleted file mode 100644 index 5e96e895573..00000000000 --- a/internal/typesinternal/objectpath.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package typesinternal - -import "go/types" - -// This file contains back doors that allow gopls to avoid method sorting when -// using the objectpath package. -// -// This is performance-critical in certain repositories, but changing the -// behavior of the objectpath package is still being discussed in -// golang/go#61443. If we decide to remove the sorting in objectpath we can -// simply delete these back doors. Otherwise, we should add a new API to -// objectpath that allows controlling the sorting. - -// SkipEncoderMethodSorting marks enc (which must be an *objectpath.Encoder) as -// not requiring sorted methods. -var SkipEncoderMethodSorting func(enc interface{}) - -// ObjectpathObject is like objectpath.Object, but allows suppressing method -// sorting. -var ObjectpathObject func(pkg *types.Package, p string, skipMethodSorting bool) (types.Object, error) From 348453412b6e53c54f9184ae79d3054af0b4c50a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 2 Oct 2023 18:37:51 -0400 Subject: [PATCH 021/100] internal/refactor/inline: docs for 0.14 release notes Change-Id: Ib1b0a228b47f1083b87285717d86ca111e62827e Reviewed-on: https://go-review.googlesource.com/c/tools/+/532276 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/doc/inline-after.png | Bin 0 -> 30596 bytes gopls/doc/inline-before.png | Bin 0 -> 27797 bytes gopls/doc/refactor-inline.md | 161 +++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 gopls/doc/inline-after.png create mode 100644 gopls/doc/inline-before.png create mode 100644 gopls/doc/refactor-inline.md diff --git a/gopls/doc/inline-after.png b/gopls/doc/inline-after.png new file mode 100644 index 0000000000000000000000000000000000000000..843a8454136bd247591ab406d34b1ffcfdda90ce GIT binary patch literal 30596 zcmd?Qb9Cm<(k~iKIMKw`#GKf+&55mvZCexDwkEc1+n(4uPx9OEe)qZWy=&cd{yj4* z>*@YBs=B+ntNK&bA+pjUu+W&$KtMpSVxofbKtRC0fInwQaKIO}>$f`~Am~C<0RdSt z0RenjTPq_|3qv3v(UAB=2t|b@)WC_hBZBMzEdi@(4IuJt2-#@3oNvE@vi%Vt@f{7U z!TNneA(icU1-sv$~1X{}Hk!ub`kBkvpP_ z;G5rrD8r+vjn`1@MMIj+cABJ-Z_0F8EP1~Q`e(Lu1{eL1yry}2hJ5N0%@eNi0l(- zUn`?sQ1iZo-G`z%PaY(bt1hY}VUygE+M;j2G=*d%UZ?fJYO_yZ3=zD+{J_=g;6GQMfydy*B}!&~YuObSJDcFb@JGKOeFx zlqx=A9tiLtxOo?eEKq1CTopu|pTH&=E1a|sLl)Feu!T-3YczWwtqZW=PCDl=B%Kg+ zU-%H<2nCQuF{b@*@R0_gbNFbY(U5_@2*}A&#Q|O76V#zDe?{RVmSwXCEAjF485dZ{ zT9IYBfyDIh&uW-NL;n)1!={QeA2g~By$0fLTGU} zqs}iK*B?De@TyeGz~4}f3P_G(|fn9hgp%*7-$d%dJVUs zt)Hz)ZB(qe8g*AmTfwxDtOD-)MRr^sINizLz~1QFA$WW<2}O}^AS3;GK~k$ys?x2) zC;1yi&`Bf_4THf2jrGWWgfxbz3YQVB41tNU(}h#cqmQ#F53aQrk%A$!;@< zB^XFv;rB)R=UL{NO)E_kO>@vEm&rI1iNrDttqd~lp6nX$%KSzaV-O4cu3_tE5MYpN z5VdPg?i-hZtZA07SfN-vt5)nLzcUSH>R}3RT5#if1buWp?J*rokBlLa%$uy3te1>L z-(VruEC%~Ai8Mace>5jDg)po0b8;We@GRcSHex(?yo3on~=%DH(7|$ErE&p8LTy`&X zX?b*l(Fj8h(?^w|GNgW`x~9^g+EB;Ps4k-@!&QwfZ!2d}Wzp!U{!zWPh+TKokiSS% zy{5Nf_B=MeF0G?t%wkk&j%VJn(H=!20+WL>^NURfdliS7^{4&AsmVdok#qm045>n) zK_QZKZWt`89V(s%=Wf~As6F>Km2Gx&Z!@+dVv|`Dy(Xn*X3J4y^;*fQ@S*U{_b0I@ zb0-)lA2+{e5qOa(2V4~{xeV@%fQ)pl8ZBL|J*|n>`ZlCi^EOg<3y=ASpSOJvPtSjE z&h(2IRKqd*{WIG(*gKfLetQLbHN9U0?Evxdj(l?t+4!v~0iJM7y+>^c4dxTxsp%u* z_tkgWkA|v;21Z@E`c$aO@M3xM4ncgdBpQhTkAF>|A&47_;VV*LeV|0Zfd7KOSNKIZ zSqOqqibzFRD^~uuz6i6(exV={6(Jd#d-ZnXN9%A)Nxsn@6mUvg&H3o;Idu70~%nqr!=nXb7bc>-(4 z{Y`2o_2A#6EgJTwS2kcY9wz865-=fq;bTqO`>hmnOU}XhY!N95M_Z_0e|cP9Dq`Da z=cVPR)uTSstZaT(Q15qgcoJC_G(S*owU$*&;MH>>oEb5|g2+N_^~5wmSo!_@bw7BF z`tkS?>pIBu;xYRVZw;^PWJ90nNH*RSkFD3~s#%-E#{7A}X*Y)us!+a=c9>e9lV{CK z@oC|a<>d-{!-^$OBWR0X!_JoULi&J&fw-+ipd7w*SEtmyp^8xApv{Uvy zmy)LOy1$}r{2o)L*6Pkrd$iRZoSZve1-DdZ7He#6cF$O3umKUWJIDPY1-&G4c8YfQ zE*;n9)Y8C8om^x*3OUN;m&o|K(c|b6c`L#Mw>ow!@d131XZ5J({D20eAS2-7Lpmm^Gi^hxX zCG_3QtaTGUndh#9+1=x>Oal)G2_b3V2IRVA2eoJ6OT|;$*z^2Q){rrtJddOY)l1&X z(Sz~w!5uB4ws1jB$98*y2gB3+)!O@Wr~UB_VHBom4*Wy>=;X^QS;XZP1agc0~v(NR8iGlRZ5aw-^!9k*T728kjB~4`lAUDr!zYsX=!M$ zi|=e{@ym|gnTznV20I}A@tKwo|Fep{ITxX-lq|l0m8~H@GYuUL9U(U~K0ZFDt$`7{ zyrA&EngeoNgeLa(*6g&jPEJlVP7E|ww#KwS*x1-;>F88SxVsO?;S+3Pw}|FR?c zr<4EcN6^qt-`3RH-qh+B{zt#MdR7khT!e%lg#P{SA3hD8P5+zHFS~yY3ot<1j}qD+ zG<3B8Z_VsYjs8EHeU$vu>~mcIkmLO5j9u2$+0a5w(9{wzs{q!x8QADJKgs-$lK*D( zpO#8?hPDD$mVk!#-2c5S|7!f7h5yfnpCncPnGPb4wYPuhD z$PH-oKfe8|J}2$R1pmi`|FfE(UjfU>4b4gWZ_(g}j)Y2G1p?v)5)_{aiL zg1o{0(^VQE&F~IqkuHsH6G9g_b0a~!0+H6oPiig zd!zUyzXLtoC4XcALtyzo9RhDYB?zU4+hWUte|=l*P_C1mU0|vCVT0VvY= zh?}F7onlX!4IdmnEW6gFw`21~jg_r(F!7r5yuaY0(q?GU4AF*{g5$|hzC5Xz5wM*I zsh(lZJG&-HWH;7;JKvs!?n@kW{RuQys~x^!#{G5SOopCt=jghpLcg@6uCt17oR`1R z(|!J}dh48#ICSEJE~1aP0@(VPTW7JbrvGabGHIf;z~LQj>0tt8#p2)~x_{t0RbnSO z(N6>$QiubA8t5T5M;fFlnCLxMNdnr zp~;xvl$b~fPaNH?X!^ISdl)l8nR#$bJSZX-%K}?72GveLO#Se5a|=d8MJ7);fPj>% zN!DEq7&Rv!8tzx{>MCa%j~7mI@^VC7K3%0Mbn5tQaaeSslZ-@Kc{$VRsYT51-;r+j zS35s{V#mbC=MtY>ill9sSot)agjVbf!wxyy)nI`;*!9kPJhgFYsHzcO_OiLGBHia5 zQCV8@Qrl%TinwiH4oXRhIP(8yhf0p4>f=&d4z}-BV_)tW=9{A&l$ua1q%0p;RyP6O z%K5vWzzkEU50K1Gu&))6>Ri`6tj@FY8^Qw%p7C!jiSRqqmDhP4Ggw#%9!6*tzupwJ z+`#HZcBam^yJcz-@m#TeJV<#-HJ$I?SF1k&qN<@GrnXSA*1tOh zGi&>NV;YpqAhNaDRqK4eW@Y%`5A{lCqvvI@=8B%_`4q-(d7xYZ?l3WxvoU>-JAx1j z4;|CqQ>c&2LV?BJ6IU^jV}kq z{OXAjsrANM&p#J!P`KCOfZEIgvxZ7Zn(5kD_eR9JP!hGAF4A2&0QPgKQP!vs_)xfK zL-3blfyu)H;oS?jGvg_k23i#Vb{MqDQVlM<)2~2E6~Zd(t%<7Y>h6oJwV2r4*Sn|f zu*{zIbyPer&id#g%N4kt?4XofiNyWGxl-8fo?PD0&GGaWb#ZTc@W&-tjWCYtt!#=?D-dtKAfv{L6vAY~i~w1V!s@psi1i@eyua?(-GnqHVUYTiTTY6O*3Srt1iQ zaWshDE}#T0mjW6=-@Dor%xAIJ@fNtB;f`XQ@`BTPbKK~^jL25tootb{y=RcSS@Fko z8Gr1R1n({sJ|n&GvLDF~SGIPcOh<9{<_qsyAuWk;!#xc>vc;-#oDR*AF(i?bXYT@| zO>OiK`&%1vhw|xbYf22N`~p@4RHq4}*IPk6$it;KaEwwsDIrE<>OR_D9(a{)cm8w` zh7w3K;TRH(v7#MOi7314}=|*hW_5$ zL+?W+zr))0Jpd;M6eVt=t7=ey4AYJY+4#^)?5Cp}2(Hs4we_BCaF_b*^D$S|)N~fC znKrr-Z7{0p8A*TW2tKGo&s%1bm?xO17F)q|HcBR7<@rYjRqVDGgVi;Opjp%5;6j-q|JOZ%MwK|hta zYhghZ9jD&fO(rI6bIB$~E)OM}a)U+j#pFmw`)30EKtOQd`!9Z6lV*$9=KRJZ`ovBQ zfRdO@^hP8HiI>Xw)vGSaG`DA4OMm5e4n4z$1PB8&f z+{g$GRZC6>i?fFfHhV-{fX4vCBz`COTm9E{3?^ingb^@YRuJKANR$5PD`6S`HNWL} z40|*C#?hUnB8lW{Go!3a@43?VXwXhZ%HY-9D93q8`J4_<4{grt(cIw3$bq3)m8o(K z%A3FCn7i|%?sp|-%<`qee^R>C-yEfsY>X>RCVozfD?*{UboUC4@3%inZV<*N%V_p( zvTy(-F5J&st~J3hzqd#2E7)_LhoHO7DV6bi9EwaY4^Ou6$a>NJpgpVee2ib;U%k7S z2nD8}&*m=0y>fmsR`03IIJ_1XApMbNl-HW=*xBRfXJ>F;@0eF9MyN4Ig_f=JVLa!duQyE;=sPGsx3 zi&;?^HtBKE9fC2~eXw8uAO3NB}glN_kt0LP!o3~PY;6uG(GXI-XRtwgPN zy2y>rSQPuzEDY;3n)zPVn!#kwCLDPRY-|icrFs#2iYVJ#TIm^4+3pG@kbg z$liicy}Z7{vOuf#KNDTcx~@|HC~R}rPF+^I{)U&{1f|9jldAS7s_Ia`ItyQ$jqB#- zXp(-NZz*=+vbz6mj;Z>k!N9$dicg~R<^8&|=uO(>{eil-AC0R|kuo+$J9r#Q+Nuba z6awZ)Ds&R%Uza%&Wk}(-#YR_nR^VKDFnHDzLMz(F;p?{W|E6e1RbG^PEPT5M1WK#d3%C|>&-`d zDjod%v;;|H5dkKnBA8G@*QX__3oz*t?I__1Xk$|~@{$VU`0uy$IZUVa|xl?{N^MFL}`-b$SxEgn@y0V8Lz$OW6KLWb!V0 zK_DQ}plVU(UKekG$#^`~vM*GRnOD7D-_I`eA7VV-=$p(m#=*;d6 z&hyasKku;r0JbC1aV*F#SOd@LSTt`C)|?B?e&Te8Kw}}#?U^LK9f}-;A-$Z+9lM;H zdA%9dUP>n68EL7+cwnKu43S5!5i$iWC;mw3xUY z47cb2#3jSM9ZwAP;%OxuAlfgu)#1_e$LWp6P!VFhocuoV{q+p2&6I|Zh_V}@1*g7QZ zqitP&iI~fV zwR1E$NVNf5MTY`xR2cE4O^@Jh<&Hu99xvpNAMU^(;`7bBx$7%7;{Mfy16(BSIB{=q zD<`i8$TNtjAnj{M^t*k%GM624!89Hp6o$aA>J*NQq=s*a@UIbGo-gA25l|f^p8Fwk zEH?ac4ts;YoEI#EMT|y;&xb0vPAfKIknKd$D{{DQ?kbM{{92=69-e9gmJ}2OE09hv zN&Z>5@%*OY)myJ&U8)7^5#%T$xG1$mRP^^On*knz9>BEv2h(}P{UHrcth1IgLfqMh zCn`4A)28Kxb0r&)E^QXeI&9JIp6AT1Z96%KP({o+Lqo%HE@@n?hRtz}N&NI=dbgDN zhVD$~>Y|NENDF?c=7Vm1N#p<;o z+g2Oih1wpb%sFlG5<5?v*Wm0C?&_@(<{jA_Eh?>29`A3jSpta~^=9!EoAd$s`2{04 zIL!@wXWr_n)fW(|>v-3L$ogMSKdzr1fK3n0-9soz{v{U0;Y^_tiajdDceh1uvHl9l zAA2RrF)C810ZK1Hip>yB$w+Q%B#}1A!H3U+_^iq9pvq*5aIa?2&LYlCd#At>li<{r zRT`V!kE9%H_y)=eDQt8A5wOFP;DFCmlxKSKJy>z~dFd9Uu-_;kL` z?>92FK$9z7`^RLl7!5O+=;pzbt+Px`+2*Z4DmBM&B!M0Z7S7mEsdepbHpcF7kpVqI zgPHewvp**&E{yn`Mu;;tFw~6^AkLD3-hB5|1%^eY%3=E&CFIf7cJ{(&B;_`wH#v-F zhYH5(GDSwR46-r=m7Ny29=jLl_#xkjA4+YP&xYw!sRaUBcFG#4(R?VYPQj0wF%W%o z1|Mn&Jrn?}2*emReQGWw0zgecTx6BlPjH0{#HgXypG4Hy&6stGosfnJcd2P9MefuX z1VuP2%t}Pk{I)F4iHRGm7Jf`&1Q+uLU6@g}T*l#IU)SBiESbF`YhZdEF5wUJpqMy0 zx>EMS7-y=E(FIJ=)?q|P&;91idwY@WV*%ml`Z8{6V3~lKBc`-SW>?TVji7YKo&33B zTcXPHD<1pYF%T)1Nca*xB2+SzA&PGFjZrZq<^$N)Be#Ownp%DJa828W?s?xUIPfTc zvUh4aRx3{1#n=%_uw;AjmtBJ!)4YQ`eY26FqNCGX6qSpd^^w@E*&-V02(Km`#o`Ja z(38%BW~^^fnbgd$dHYCPN1Gw<=`Y}g`gJqP_l^UH#(~|n!VVT1_h@e%*3GJFh7om6 zbpscq5ZP)g{npmD2+!IEJHtx|?(coHgQJsL`}t)xOfxHss`^hHzD?S&vdK1ju;u#C zGI|s9UeOr^>PhrOq@Y%(UT9pe&APlg+BdBp$X=Qo`Q!zXZ{UCV4$&sDT7I4ONfVF~ zuT(*|QN=Sz2+`%8;Wx53>P$sWAjTs{EE>$oj~D_Em?rb8L7k$)^127!VQt>_T2KCH zA3`Dym4NAh3L{KFq5V-SDr?sZ)cD8DH&*|>a&cFxgr$2u*sE%1SzG-m!^;X9v%xG2 zF87t3LLG6|3fr5;gB@S<)DO{t0h6h>hNPS$tiI)VuAf7JM?O(gD=J-;g1AUcv$yZN zcY=DQ+z6H*?s-_ylVhhw>_JIi=C{x%@fW3HNVAq%<)D)ts5?`lT5670>TUY%8${hI zPNRItQ_EB0L18AW?fx~$vghDCtjG#LtCT+E z=jW5>P`pTTo~5q%$H(yEi$~nCaPngnCOu!=1Zw@t}85j69`SY zzXl==6B{gTq$dnp?^%0l%PF<9bJNIrWDPuoP(IWQ0vnbDmdpG-ybW<;lG@W;z%6xY z+2g1hh><+lk@NU}F5)(@9cUD&BE?Zy_TYKDp(f?P(9mitVA+ zt~A%oUg73Hw&+dXir5I#b4}{(w*Kyt^o$E>b&6gnJnUQBDyN|38&<=-WxNaKs0&mi@S|02T*l8|t|Zbtwqd&1FL98m&w8-jOOCIVc zQp97kmzO{#$=Z}O;{#GlWY0oNf;ku1GIG22^*XEd9^IWbxA!05@)*iHMmiEF-FHW@ zu(c@=4X5t<*dW6>T+Hh-PP~UBthQ3|Hc+VshZALo+!74wx8Pt%$#nL59qa0adMCmP!HuGf#dDAf%`)95pvHQ~C1`W>L#IxA(vYHE z@TNqkWA&+7`mc_x*tW=b%#KO(Cu#;zdo@p;>WWUb167+{$E~}Uwzu8yx2ZHJ)9?^p z-^B6JWE^m!l3%quYfCdkQVtCj3t-;OV9PW$H+gQM7UwQz)fjKV^c-<{x4jMw*fX;| z5bF)mY-gqsn{_bCuyHn7T#(azL2NE>qw>FMwgLCKBNBqw!?4SP#Gjc=oHa+h^u6J^ zK(;xTcZXTcRkLB%f93bvzunw9p&pg{FL z&K}O`;oBTAQ&&3fB#I%M!#$PDA&>tY5jIRoHX_Lt98cSB%gM>n%XG1jEJOIb^&H!( zRBFwk{M1`KjldJX#v#r-;HA-Qf2ED)V!=G@K*geQ)zO74k<;!se^aZntUm1{&Lj49 zg#ylalSi~KT98ih@#Lu%;Nr`2NiWEP0t>~m(f;w`{D7wWt(m~DA!9j`ls!Ut!1b*9 zxm%0aG(W9YZ--ArT95wJ40RnPiXtXG+CMnHWALf>%|kX4V)1#N_jvAPFj68~oFw9!}!G-E4~pOhM(I$z}J9Oo6r1OwT3@;^y82K3@R3_3A=it z|LV)e36=3ywalga$w8I=14&v4Tab$gYTWSnxyhOLeM_s~zLpeXENWQaK*uC`M+xYd zIAR=Fs>QU}(U3e8nW-sNwvUX*(gX7B?sFx!{BMjsHa2Exz@-LpvLv?*&(d8VaO0if zU`c3df4)D5z-eABQ7X0na)Ki)p450^OM9D{ZP=_Z9X+O<3?exp`(uyX7TaGf1UmvN zS|JYRk#?;;Cm*s%g#VY>#L!jQW8vNFONws)rc4`lL4|NLx+013-fj^b@zLqX}VKa!UfSgF+JX%oa%qh(G*gB2J+jNQFUb|UH?t|P6HH<`%bs&+h6FH@@p zcXV_VuX>VV1fDFTP61X%Mf2<^oXU~5bND07bX_PY zuj4OE#~3y!6wV|&!GBH0S}RK33&wfhEA5g}8)UDf_V~+tzxCv#KOBR|t2PXcc3who z0JjSr0HNra8FO;GA1OSJIzUlav3)UAzP66%qtTOX&bt1#q4eVRtlG6nuleqFz{+{b zmFJKxi9t#5;~tU`|D3Y{OD5E3>po~4;7ef6F^>IZ8#w$n0>LD&RIxClz{ILrD2>ww zJuWH1cXLa5VU9|5jkocXt7Y5T)<$sGklo9w=%k&!lbQT4 zk?(fm6lwG6DY-(=1-4$dXFTuu$OIGiQM)xGk4iSm6(tM8GqMbJ5v9hae?oax?vs)E z1g0h@Z@QCTGZW!?7}>v;zNrzt#=nZlfhhhROJ%vGd0X6PLVwtX*yd5ejkMG%O~lr0 zz4@jXDk&sYxk3zX+QRZTbU+tzPW2Jhh4FWn}nF%mQ2*5`}Rf75%ZSa4=U$nlaHQe2r6B3irSyN~os;nuDTm7fb=(JYYi}X?L~1-+QLP#V3G^r zAWERO+UZD}_F+=3g;^Hr%dP?IvD?#lw%<@Hr&F$kO-Av&k&Ojycs^7Av0O2#a#a>& z%lo=oq7thhd>$ON#E6oa9*;Bs^(Q%ah!WRUc1IMSxN_+F{m$ND2p+B)Irw%TgqvPO zcHvu+I*p5~ilB&a?HS^sPBvUlLvM7^iyHcIF!V)uaxL<$Zwo4=Dd3844Y5YQIcfR#0VFFTZ~`^M#ypjY*%+x1o2 zbU{hF(*l_i&{=g-84G}lh1mcC3MMrF$oPwfc-U^Ng2ne>3 zo`*GsyF{m~>yx~f0C>G@Kvc8&LWQWbbbqlz;Z4OCrftiWKYbZoZdnsr-F4wY?{AMM z<*m2#8jbFcHS4!q;U54SE-*8!(iVf;-=+gPm#badxh#o9nj}Wk-#;(q@c1Z|%Q_dU z^{aJzz&&d0Z~^S#0s(N5l1&13cJ}Tiwfa#o%S%yDcKh<{^OKXXyR((j8Opi(>gpKX zy6=o^XlQ8l*u=2ure0eBXgFJcAQGQJmQN&e%GkgFpR!bMo(qG^l`#-jp?qGgs_aS& ziA6I!80S*w1HJSFTV%|`Yp~sW0xZv*#M$(=TB%B9!c}*7_r-B(r8rN3*Ugm3F&7Y7 z^t@Fvk5_wmAs3MA?J@D|&B5)0w#sv%R4U0m+TZmy4x`beY?`ody}iAg5iIMx{QSP` z9zw3*aM)xhVFa$|+MbX3ieNIFm;TMe5m+q!t4(&*b_de#*JCWl)^~svk#z-(S$v|T zTV7sYMuWriq6@+{T&K(Ti~QYM%OfRM``xm+JX9K|%eUvdvmSpaq{#|Rj+>(y5oXsq z1#~9QIy=>kc8_X{CHiPW0yL~r^}5LQJJT1R1dzzEuvmTIxV!vBt!}sTHqsg8emr?j zMqK&0R5m-K;(lT{JZ>r{GxPOUw($%u%05^ecEt?XZ0FDt+8_oWhzNjaQ8?!;RA|QL zPeX?$Cnq<$)?ZLW0Rscul54xJ5R14ZP^tL>gMy2GiH?f{V^^;;A=t56ZIly*BS&B# zOp`BFraoP3LF010;XPX}!6`84hkm$T)Q{?#Q>q`!)N7Z!lfnOTGl1tcm?0`BBqS(^ zjFOMi?)mCk3|Vxkf(*RI6OV|4Qy!l{3&70CxB!7c`Nm47iL)yH$C5(?iev5Jl;mNVXVV`Wfl)@B0PcTF^1FI zwV$1&bkJ34FK=JbJ;~Cx0@-D z5*ptBh>Q^@k*%iYAKJ5lh6R}wWoKnI@h@s+ROW?k-w(X?^z_Wa5dH$;62HW)rKJ_H zJ6o=Sc6oWJT5pEJFCeg&eAB=zUnIKu_pcswn}>U8)4m}&L) zWMSa>ydz2=2u^plNUo;U*R69pv2(XS3Xe7(dMIy6XjU~aD98_WCD-PQZRl4QxlQnqETghx*`Zk({at#P}iteemF-Bbe+JM2!zm_O`@rFqpz?3X34NHN( zymuB)w#+OI$4=h#1#xa68TFCe)uPJK!gxC zyS8%%tv^HOIwe0K+gOOPhM^C1mka>e`u_hcOByR9*b|^;aH()l{Xlqq%b4V4s6C<{=eMj&O>mT?l?pL%9A#aoR6#C7{l+0p2A^Kt)Eu|NaYDuT%s zPw7oIf|qcZobQ$LZZUj9*lCbSb2`;mI+etMx!EEJ1%J|f;h0tkNVpP=5y-6{Mgb@A z5+dCQu1`cfEC30Pj!z9Jg(46gm85Thm|y`E10l%*x^My;mV$@D{0i>||MmUV=DQ&= zWtY3r2P&RK3|xDP_|P&RQ-H4wV)U4LkP7&o1@ytue8T+T@Cc2!FYPCn`(mA^$UYGd zp7>SjK)*;f7$-=_oyI=biMO+?A9CLzJ>rhL>s!~Ixfd#LSK`Mi0AGLCQ4ZCy8)SZQ zu_WdH*6rL}WH_K@1M#5XRbcj{%hmo-%pAGfHQ?`)L-Cp z$&7>m;fZQozuMJ47L@nHn+?sdh47K_Rzm~?Qz}QLS|xm-`Lcj;O5bitR{u|EUm0xF z?5RkNOO2>S4G`!1@bFM%j`HoB@2Q2>z5uL0mt+?7$I%7a9-oiGK5rgHDMt`27CI%s zzh`8+Tt{v;TjaCW;&h@(_isfVosghx#6~0>&I@qF%{{KWo>2d6cUSWm`_Q~4kwB?| zH#+A!C*DVGnS2d_nm2=*k81sqIAMSzGrn|_MzdLGI#2B4{(eDXJQRhJ_ja*L*H7z_ z4GFN~fkZlvp`GK)sDHNc5B3iO`|B}%yBTpTf6jE0LNcvgNns#c5xgy}EcWnW@19zm z?lJ|asqN?Hy`(I}B@E}igyuRAi|F1|0+5IY#nNoSksI+g_cimO<`N=_qw0o+pxRo7 zva&J*xT~41!$Vb3-^xnb&?-^ zyW8CEqhn%x0B!kgus`O1?i(G)%;ayIOnh=KD1fsB@&-EkW{AgTiD8=O&$Y={i)5>A zQBjFrH>?9OB-yM)p8ZH?nb%&j^xt>Spgh}AwH$`4$Hob|?|Xg^M)%MzYb04HS{N6E zfQub+;jgq9-SiWuUc0t}&3hz`A4PLSW__0@Q?QE<8>pjrOc0TOA_vDt0$O5IN9|~? zK)9E$p#Fw<<=X%*y$K`)#AkrBFh9}aTE9O$#uMO|a|hTU)^-Z~azz1~{cx!eFENGA>Q z_-+?b>j_@0WC^crU|<{!4hS0GXb;7WXc@9?X7N;IJ|&RTeRv$KEIPpnp288{jPPN& zyppj3!2hDHfBsr7@lN1Ap*m_W7XEt44a1ZhsWGtxLgA6L9rzecVr_Zz_m* zuV2uL72n()id}55!XuGP@^(C1&X&pKUTm@>oi9@(n&5J~6a1Hj;~x+p##UWjeI;r2 zjEIQ1SYrrdf{@hzO%yJk<=1NCcFCX(U`L_@e;fn79(VolLOr+ycwNcCc2}I7&Q*-M z?k(?bOZu!Bo|sbZSlRK~FtWL6aT(!*jj7guRFZ|hO@G_1A1yn`oIIo(ebl_YE0^f? zwG4|%h=)5)mZc@=oavbTh#cg|BI4rG3~hJ4HOnRh0Rf5r{o5DdcOGs8n8>lHLrKvSu5xwZTCh~tgc$LCKb=4F1 z`$?7)3?!u=ohvmJ;Uqd+PxzhpP8?cT9aZ0mBI!?_IsH-ejA66+9VLBsBCic?%;w8!Ui8yvPRuIp-aPovpo+nwf*O5lRXSTU$E=>|1>M zCV+cplGT4(9imRLRmpaIXeb7jehg^(CHUm;TFW%D@D4<1ZZZ$!2pItZAJlJS!#`1p ziD)!htwksc#R{b4LH*y0Sok6(rJJ#n!w2O!y+pFY5Q2XA43kFl98_P8FgJf&z*`|J zuwUNVnZnC5_Zz>szv~aXOUc)1Tem~c6a^nH@xs(pKg0TZRlSsf21BGQDtb0%h<*xLz)L zvXX`DWh=eG(?LN&Vb(y`W&j{BuQuk)C}A@ocCJ!c%^l$I=iU2Og`gc;^0g0~ED;mL zj7-w9Dw_KuCu-=_!or4QD{?+=yhexcdNSRgY{`vGqP;{^qs`TUZ6s)v>4}>bQ>Wk@ zQ55RKFzvMeFqqPf`Oi#t1|I%qk#)%NOV#QgZu;4e{q}1rhD((KL_1TlK zOy}|>6DQv9FYLaliv*3yI8@Z$oVXdxjE)JPPfN&{fc;Vf<5E_oMq{-kD6^qHVjDk< zqbYnDaz&>6z0A3t?dx-UO=}hu+`IHfTLY<2)y*(q%G^%RP zY8FheMSNmvcrU#1P6?R56jT4$jC4K}gtDewzHWNnjm@bgjT%!;kP)L5IU4(VpR7vq z#(H-d!;%MZagC~xfl#{F5dx2RN-6t3ej21YAWD*1)2?6!)>|GbU? zNdcpfLR?%ucgr)r5&=tr_kR6g%cq1ymORv(o6uZ-0+o&JephaeWo+-$o=)QB2KI|W zr1BgC6>w$m;wSU-BW1XfFv9zoaDWE;0_;cW+W}WVbR|tg6KJSA;A5ZLXj%dGPw_5N zQQbM@RhYqlRY&p>#~`9Bz}j#AoTMagW`OK3b3TNA?it6AJ+nr1fc0rQn*d($WR3Y% z{$rhrbY|5CVq;^E!jWyzeXjN_q;*v1YzF#5;-^vC1@?8BNcK1-V|)XijOxR52W%~7 zz`c_04{7)9KarC30BZ4H)4~8d8^AjjAlH8<*vS1k2jV1vsF>eAb{~qF4p{#~uog2Q z7lJ-JANq*&q_?K|e2=7K@v$jakhY#a2SJexFo(s#$NN6zAsm24fatps{>1wb4qjrw zAY_L+n^-?ZrTW8;wvh2Z>G|NU$N*qtQ(&;70f3%QUdtfE&qAy{J_j5x35Xvh20hsR z%=HTcPK9bN#0vP2p=bRDSky%Zn?JEXCt~sAjQWp3`iL)Ohu_mw>=q9H!_zRYw;PzD zcc0j&_`nka``tl0S8hXGlJ?y9eq7#!0p;+^NPEwp(-ap27!WDKt%j+uZ?E> z`sFL1tLuQ^D$4_wW>#;9wdD)4vhV?_Tot#yy`7ktrNuebjHart`p2Tj`(*@t!q{CI z!lSY@T$OEAC4$->O3l$VW@Ak`bN5>jXh#ZoJzZi$Nm>Uo7{wWs`Vw2tqyM9IiJXcu z<=^3s#)lDz{Gx5Y^xXiPxy>EZ0PS~iroo8uR2Fz;Wo7xha*c+I7l2nh4_g7*?BvMD z=sh(emFJi6?0(0HI}ECCsaxT^Xp=3h+LV*SK945>l0F1`Ze$b_;osduNrmOkeeH3+ z)r0|LJI`e6~W zu)rToXp0ybc@kNgm+~jX$G_4X?E z_I@{++xqttrn(aoB1sg0_f1qv3ME4H&5ItokcgTZ5C;oDC5u!laev3g+B{wv?(Xds z?+gk+dq?_ii4*=V%v0ztIt8FaE;8ci=|gpb0{}C%?l>!%gpLkDu`oIs;rn-!jY^~D zk~@uH<2cF(E$1W9%a##i$=jN$I{*B1*em*4U0r=xhz@npHdn5W;q7}}Ln{~wHx3LV zv58f_J<>f}nU`P4iD@Z8@8iZf3KDLM%m1smvkb_i>C$v?cMI-r!2<+$PjGj4ch}$! z!QI^nPLSXP4-niRBxsNz+Yj$Iv%9mqv$MaJpLAE#-Bn#x=RVhc&?IBj>13kTbF!#J z3q`m9{bGGXyj*e8WqaX+eY^D!d;_^4<^+Ie+b;>!Ox0=vVkNcLb7SKps!6F2wr11} zl)7F%?@L$`un4ljKA9?uXE?ZcZ4lwn-AdhT!0n}x?t@$IwTHI}RN?f=w}r{PF`M}O zKb9XJ^mOm6xVOdPDeI?A=aET5?-F(#>qwdAOb6G7?@2c#B}G%ZN`;DT36Q;Dip8<< zE~cm38ysStwYKj=&~hcq6ct3q0*SZ=Im~@&2wgk$oA!xk-s%_~{%AW=?l@f)8txCV zxH#+FFrF#2vYa1L$`Rt@BUxWRQ=V!x(>Wh!^+~Zk{3W~KdBU4ni%O{k=vZui53Y~b zb4r@M{@r}`7u!SST1_;PaYXccem4sm<)4be`Eo%WKz`L0kWPu!lMasSdc#{-10FGe?d1@lUNH?w&vf0c`#3s}kuKc>(w&GG4v*uea15!CJ_7@_?Lat6qu^Le zL-wh)XW>zj$HD@=C4sZm;+va|%<$c&^sU*h0{Qf;w76q~nnZIP_6E*}a~!v4f)MXu zROiGhf3a)Qzw=JJyiROL{gWr?zvfXR6wNG$Quhn@Jdi?-m7hvgSi*4GQCc4D$!EV& zY;OK$B=l7lHwAUH*jHh17*5GKBA3^&M9n6TD@+d_mFcTRq%ZDJ>FbZ+{KzYroH~k0 z0Q$23zNN{~>^KlM#2~yVT}ZLUQ^a^gQI3HiBFqzNb}QQ#v^Dz>zo}a#Xzf9Q&`W~l zXjTN0Qh@pq8{QlF>qLI$yiVfe-tXM6Y~I@exR2n8pzDpaLAk_qEOryTuM0UDxswPr@AF`X>dp ztF0)2yxI@si)K1K9I^%d2*5!=2GD~}rejhepx0NwgcU;lP* zRARl$C7De8xgO&yeZ{%YKEj=V0HQ^hpTN_wg0ANtrDk;hD(vqUQezKd(Q8z-oje?3 z9qIMus!9^GPV-i8`e3;x+@N(3Vb@FH6bS^5v_R089JqwL4c&*Ds*Yzvf9z(C&b=tk| zcmV6r*QDaXrq0ePc`O2NBqO6X@ghQU!s~>DEb9FQc|es3)9X|uV`rD-jN>hA5R5xN zr@N4>5!e~{K8|RGANIoWNi5)KmioT+H>k35&Lm!dCi-WWS5pU_w_I6U06R{i@_YA_ zFEKXr<%xU>UIN|TH^JTA-R}}}zW4b3MJFYVPi54QZHO0qDF2Y`6*-&W6*W7P&v|3+ ze)99v`Zh{Z-ujohZ}x$S`go>z+|VcXaNb0yiQpz4eO*4 zS3YEhAYp7)!(Q_-zbHK~rqf5}CcIKn6Lz}Cuk3t`v53u3817|e++?|N^1c6~ zGk)=BmMcGgzqI(utOP!6j8HR7lvzMEv1&yl=Mq^$;8p0Bg1f?MWfJ3s!D7ofk zB+(ZZi$*@-A_`w_^mu>2)N|ag)}Xg1Wd?{InSj7iF1M`ATAOMidYD!k;^}gnRhiyy zqlZ|h)pkbK!5S};cB-l>+ewA)^5GeD-011stv3BLp~*&>HKL*_g{0Qoslizp1yo5n zwpFl{nn1Yh=+Y2c6aoWUV5=xCBJ!{&(TY8^+V2HvMpu+}=-Q`+``wUq6Ym`)a^sE_ zD*Q~8UsK2+kUcXdXngoA}@ zMuqiJ#Dkd~u4mo&-Ru!OaGrR?7SLH0vfdP-^eT`;?wo=uiwb6$MywM1BpZMnBmfryFNSKS#T zHC?YWt;A2~M0sB#BAbj&&m#@|*b3vZ)}KH07_#?pe}aVNK}iiqntZFgen{W#^KtSz ztp3d#`UP+;fX-RdIoV_aVwO4qLbn-2)?=9N{yP1qi262GboU1d_P>BnP(7MlCEk2z zqb%O~TstQpnPkXao?AmNj=%#A4Q;tx`2$K1L!9mTE+9IE0fMNkk8h{DgbU?WqOrNM z>5q){eJ;WPiQ1(7u~f!cxma&y<)lTCDA~E0pM#yhd!pG)hDWrdfF0pt3`;72N(y}k zWUIZc?d?Jiw2T3|NkE6JGm<+Zxo!^>U>57td zD0i%^+dm=q8|U?6!*XxMEsUtTpeU_-H)Vkh9V3ci`tQC(=$6iyq^5u|*@$77!CEsN(9r9u-;UD1yf@q&%^7mdTlVBVze4KMeLu8X$*&^x-(Z$nP!=jDZM` z0Tm^ILh;o5$ESVlWff2J;;}!vANw47pJ+llT#q2N`{IXDu%(*ZM^H!wiE6Z4z7aZf zL4c^S_Y`wK&=WqU>w6>kJzOV-jHmqBpUk@S*Z03SLnh#a=RB-BvfRuZBVjj%*AIAd zv#KrRdAYwjkZZy&DLE>XOJhoOetLQ`QwlSukdTtHiDF1KxU%b2#doDDggD#I&WTTd z%ZZWzRh<%Njxp0B7UbU77PC7JbUj!j)Ycdb*K*~LY`MFG;a3O&$Y(VLx zR#tRQPZ|))7n&+8j~hBm#3P{ZUv>@~tFX&G|GNtk57Dsr10BL78ADAGi`(&{h~->q z(1hp)hHlr}I2=aRxpO*TSHnX=)(5xN*_QPEXpS84UH7PC$S&!sofaw}a_W38#S$d4 zt(*IRO+XL@z!%GbFTmzR+e}cblV%n8WXZ%TdZV-mg!P^^s@|DZ}qjq1W&4pUeFB5B9brC(3N|Gf zieqlJw$&dik@noQp6O1;lFAHMl1#x~N$oj$0UtM5;^I`L56^`U{DHw6c-Tx4wGP#q z5qwIhcpT1>S0UW`+3ph!BW2K-8dr8xe?sGSwq*R5RF2gXjebZ znzGtuO`_Ll^S^+H`0{Uqka(VLCsm&(k5x=Jk9-NJKz8Kt%Xg&M_tMldqPo{Yj18 z>BlM~q#L{MYsgZvy36S%ACV0IiP(vtCloO}?;a@2#n7@Ju6RsKtsOA5wByvdTjBk$ z5kj9l-mq~~uncLnL~XQKLU z`8AWglg)9M$S$#dIoRo>e0C4DtX_;ZZipF#jCCFZ^y#k-?{i_Sd;Hv|B&Qtw)(((Syoz_rc&ts zXa6*_Pnjve_s=2B*RjLRi}*furV*)Ho8Gve^$Y=K>ZjShLx9{@0rnG~-$sSqIjl}w zt_4u7pn4Onrgep^YN+X8&=M5MkuNGMveiD>`_O zrO9UUmZ<9MPNWy3)BTT)yMym_yG?QbHIF9D(pK4|R+RRJ5Cw&3x2>zw&3VtuBB|3U zEt>$fBDVnFPG{J z(5fi7SqsdnQ&WZj7x7!Ly5~^NYYXeiAoSZ^B z1)r-!$O=SDz3~=WeV5RFVnu_Jz1R5GBQT6qiGhz4W?3i2;jciRx55vvd2FhU*l)F| zd1Sjk{hNJUnK~5En{3DEI}O$3I`n&dzy8!vzojglG{I%oR!xE*lw3=-%51d?tsmcd z?{tWq*=$cYfO!ULJ64YjbA|*IJ%-f4Ka4gdxO!Y+W)|in!C+%%rLea$q%AQ~i#g`k zrAsM$MQ%V+5?cI7J_ot@);j#fo3CQkoWRnTsf7g4l@B3en_kx_5-Lb@9?eHGfe$@+uTruE zm~OxIwP{$ETG<$6-53YBG0pY?2K0i>29M4C8;d1&0i5jR8rC10Va;cZGtS1LBzF#hl%Z`yeWF;cZZNtiYkAY7@Er zuh_o(EW8rTk*?m}z)La5W*6~f?byxza_x031dSK4x@;M&wolFCMMqdB%XwbK$8oQBvOxrOV)JwHi&gwpjh*k ztiJJq`mR~H~9dDcY8ZuQ_3jw^1xa1N;D({yL;)LrlqSKaB!iECqH z8(y!7lO>;!kdW)yFMQzZUqnvxUxA~bq%8WWW>!%htdpdS4|7{pdK`{ISO{e6!)6&_ zR5F2BX_S*n!@z>_IiAXt<|r%YN{t<&3G|O!Pyb=M=6^8jXN z!>(axLh-|FV;$e=>hXq@=7iggo0^aQoj;nbc*yxypE;-!yp)v*KH*48pw*HH%SXINAvw4-cU!gX)xLxvq_SbSjPT2b$uw=F>7xfDlV zCsrTXYKKC{o3ErMF^-558C!AOJe27#T@vxmXVR3&S`QPg_!b=E@w>|^3CFT7+KOj? z`1+q7!myz!Ka;o_xQEl(=9Rc7Y6`MC%M{c$7Z*}d6PRkKjIH)VV@%- z`vCxT3j~S^?i`ts?Y`Qd8~}nKH|Yi9>z#O^Z9oBICoMSy;Mm-NXD?r%uJ;{~Z8j|x z^m`b2qQ?#sksAoO-%J15#u`uuFteUeKvLS8r8som_evqfJ-}viWm)T}PMmYEzSkKZ zz|lN!4zw<%1RVx_+HnF14bHS_=ow23dM15js2vqJ_m7{SP`Wc+rTAwyl}`9j4Ss9a z-iRLxx%KXMJ8g~`o%H&?v4hg4)UdTTkw5$PdtmuY{zSOXdswO`t*!n|<2oCkI|@pED8iT3nDMr{La7~lb^)#(@=pan z{U4OX2PMYzS5wZC&ma0SzI1>jTIAXP>V3t6R6ZA5K~Cz9_!j^(liW1WtUpa5N0+_; zw^-Pb%pVSerhY(;cEkBxx8Yim;Q#(E`6J|4&&p%ZiIW$|T)mqQv+Q%tv&S7^(n=ND zboL%VlTDLI1iVLhE&#Y|+{W56aypG!M>wRf_&tEmkP&V;?F=7b>QvSTPe*tYTqDoU z&0WkXZTk;49v08bXn`qWFqPuR4B$}_KYo0yAhV2~lOpv~EoD|KAc)kp)kD{0eiIN; zb*4fY)h#Itpl%S|_le_JFAys1Dj4`4D#s1i05qYmf<`9xC3;Fc8>>6qVQ@9Tkz7I; zfhRH?)V9+*{^5Gj%GUm6g|&p@ko`fcg`#}5H2Rkk@0Ox?My1?eS#b%8(GM_fUyD(k zQg|kVMQ^;asPLP3{OKk=cFPX_b4wmG)qk+$dHwG#dAfK4qRu)>cHgmbB#TVC|NQhb zszYWxn=X+{Hyv0l5QA%>@Z3$5+HEZ;em_c0OS_Y*p$|Z@E!GQ3A$^0(OG|C_s1Fze|#{ zfoLGL-BNCfh<{kxZeCBtn$`Y(e)=mC!O~{Ez@XRf9g&DhbJp=o?#aS3<1B^K^h%@j z%^cu1@QNaWGg7CJ+JW80-V=S6Ypb)QhXf@iWUaU~ez1A2@Du7a*ZlNcBo$w%(`FB3 z{nH6KVW6Qmfr=m6gj|z3{CEIB%R&`=a)r}UpaN`+N**JzXmJ@LhG|EL`CO$l8!XXI zYV|s|0U2aA#_hrNNZyEwl9?YxW4rd-#)1J|2&`kHQ?a2tjWCc{4T>+q*FDG3=Q{#e$TdI&Z#!+hscDGmfdN7qU0L;0ZA+pzaLoG^8 zxylGc2m(<;gLf0Lor#_Wzg;Z%mPE%nOX#2{gHsMNld~v8tY!nZr6aLjzj}Fj{^`Vr z2}#T->Nc|~fF^*LSD}y)-X{m)C-OQ0usjHxa}h9;ytP|tmMQi6d)kJ9ja}>}1VSG2 zly2S5&d$cJdywy40=VIdI-Gd+MX?Lhw8Epx`DpUJo3qZ}##`UQ-aGGpPU`x3)iQW| zx4p}w>Ew>Xjiwhbb&N7t(#|-vza_mxmTz~$700Kh#k@TfVa6FP-joR7M_4cSO`*IM z)+aB3;~^C)3gCz4qQ?_&Hbj3?{%{(k`Wk<^_W7MD6>XXpZZ|#ff^lsYtAlEnspb<7 z`4BS`$8_r@!xeA%N7#EFeJY5c@|~dnqqUB({Ao859WmS-(kl1F2yy1)YLE#g-iM4iA>HN+mW4@i%M3OeHwE! zs{-_h5AS1#p*Ybdp9V*J?brAwvU#!UdLAmV3+_=uN+-4-k9hInW#A>C;(c9ooQ`;F zK?siV!aMjU|GC+2P6hWrnC(ifiQ*idKC$wyudiG4H`dhDq%9kdrOd65lU$1CgoVWLuqq8MHPh<Vv~J4;<3f^gU}iR(4s4twYZZ)x-65`7<}60*tQ;|E z6@Z+4NkH^ehURPv-H)3-dFkTh4V8>AS`e~Hp-S~uPkDnQ>)`g#VUneG(@X2k^_{zJ zK`w>`&>|TmXv{J#`inaU$(WZPNo@qs9nk!VQEXsE@QXD-QWir7?}YSJrtPOn;}fzI zAv1}D1w!U{R1JD&Psp4(qfe5F<;3gy2ms@p);sT7BG!C1D(lU$ZEpJs(s=T#_|l>b ziJ6(1{Pjg3^~jR?4cH}_CI#OLFLC0ES+H+Sd`Wa@EsUQ!)CmE^V8hM6pmt{ov$)aD z4WELIDLeyTa<~$!KJVwp`}_+?WddM(-}pgx704uL#ls-sj^N9iP8E{m3$h4OLbPPt zd|T(+fG&$Cno{9~_Pp{Y&OQpEQik;aie8>j5X&%7e?Dme>}l=Ue#*H=p{dHtvP_{9 z9#ig$?aAetL&Xj=*RgN>NYBRt@n!(@Hdp((niI-;?CQU} z4Zd==3Wv-L)kugR<9c~{jp9YOx4*9`wEUxNXqbQEl(NSU%S?XDN>cljplNf6;w8cH zS%1Ci@>q7GXx9 z-?qy|^&|G<5f-bm7)set_n(|mM&{m-ZP$}VNO){d@$@>>QMbATw)!D)mmRb63*9aU zsE0rH$~A;HFjPZVNxg(wIPo+mN!4-HambU*-X;LS2Gq9&YQF7n2tRrGj-!+#N`}A7 z3N5=FMb7|rV^xU1=U!svqfI?4AYAn1t_Yr6R%r*X_ov%| zEkG+s$pM&`Mvkg_qUK+$H8S9&OSjoD`QUQK3oXFvG zjA?;eA{wP!-P&M4uBS-HD>VzbacWka1mhpa>#qpm2QaJ?e4v=s$IoBY!%~{lJ)#s* ztEJGaqXx|V3rDG-S%%b<2z5wmDWuQz&MBO(r8g!7T-Kp+#QbAspSgBMl%l_+HuHM- ze;Cq>ClMe3VjV`7$aSYd1w6`*--YU@^PTUeMxZA?123_1(`XKMe@v31xr>7tOP+;^ zNxmEPL)ctnfAianZOaN{ZQdjz=BHl(noTWROHiRpM_3BXzk0RG6)6%I%}#uF%8JLk zgx_XB>BXAJGXwdPi!vBLlqaH!_EN0U>j3F{JIkTx(<^Ewj1PcEbWqh%va(tJKlV3) zqOUqceQxQ9h?FOU0P^lS8+stF-APrISNe@mki`mR^Q@bC|<`Tt3ujHeO6o z@Yv8$*%EbVmp4obcsyaXt&+I$3!0m2p8$%r4BPCSgPw!{jecxKk}jRqfQ3Pu*#GW) z>&4;suaGK+v-i)G^^;u(3ADD8G<&}U=o`bm;1r15%D%`vjKMk3A6n>!bf!I{E(Z2@QW6y zpCxWL3(}f}!TlH?PQQ^W*xT2#(?CMx*T|chnwFm@C1IBNk_kI*(i05N7>$@td|?}t zj*^`h6?)f&iG@|x=F+wK(F+i@CLzCKHp3VIzHM=9KDYW9|L0x8p;Od?YZ44_CPDM`&(X? zOKX*odcM6wfD$S}&g~l{M~*YxT3)CnEVh|?-(k`dcU%8C%zIO~+v(8fZQYXp*_4Oz zCW$D@y?iF8%xSo2H`{UkKM!F6z24M$oKi0fv*rf?SVRKessatiH&DpGz?m`tF*0i> z3ZLAnKestA^M=3`lZ|NiEyndE0EluJ}Eje`wI4n0xI5vk? zRa9V9M;NxD6UF{%hF04*;tMhySJ`Ga5D63{beLLPf?GeAvZ;*TNBP?r0*j(7IdEaD z`Fp*4L6I!fqz(P?yzuQh5h@y*2ISUh+iC2yNd$WCV~KJj0lRIrUZ3od=+B0bMs4Pk1YAv* z6i77W5SVw7fo!;A=lIshF>m2llLmrW^%(~K$T}RO8U@o@omE@O2~2k+1-`(4s>KcV+py0zKgTQo@3XC2)8~Kq3EgcArOfD>9C@s~LK_v_fwK!mC@Uvgj zLICH9!GA=5J3C`bg8=kvJEgG5W*qX9Odzy$A3Egl3BfF@HU4bfKSw>~134h$Ke17O zPyLkzO$UEA>2FpATwc1*1ai1MT$sw?F^`Ik;nS2_PO_Rob@J5`izjjv2KaKD`qYLK zZQ(zHXjyVnE{}28$<4Y3!@rLOHHO?;_crRi-WVH1k*b)4Mo)5$g*%n0`Gxz+xq?$E(?)*`_9OO340RHJJWm7P`~;O@3fW!0W7!GII%Qa@ z#4i@>{SAH{1)wv713v|CMk7V&k>%roJLZc*Upp6MQ)gDqYyVn6Vna{KABw%cE{uG# zca@#Xny=%72I%XFP(kn?HpB)dpQ4VeSZeT)_)nS`&ig=5O_s&?!n*=I3oymi4B_D7 zj@9Tqt|#Nbvr3Z%0;8fUkdz3UDgF-$!-hKc7@C@pXJSY|a&cK15R>Q?=jZnw2eW+! z4EY0%9>4V~YHNFTe1Y1O3`znwDO_vYL(zMH%CqRB4?GKW+#5A9{rY(L*o6Cdi}7-} zaIeIb7nxF}JQmj8$4fn&54Jzw0pEPw^~v&Fl`psLPqK0_C(tcBEey>)}Tnd&0Mvn1zp`VubiWI=;$hcjwUi`j8y(GSAlN@O&rx>i4(xSG$v#wT(^A zw?h#~Ds)ZA;fio)i|XXcOop#&0dmTiNjQGGy$R_xwPhB9j975o74I0T>PBYm7*i4H zs0+%fzjVy6n0zkV>A5)CFkNa8V6b&@&pi?H`}nL|FS3Rv>o1Kff0tUlb}w~bxm$iY zesTp^f9-2e?k-8$j|%d1V7~iAw2rmD?2d!1#;Mskl|h7{5);*GS(mPpVxWpCZ5#8; z2dCquIt>PLVh=!T17UXDALRNd>Uo^~(gK7^>V}3`v%ahIt${`$SBwJ`&DO=|vCr-p zp029vvF;vbDk>}#H8wWh1)S^Ye@?TWv+dWr=ovL?Q?O_iHFvIpp{4WPFfAYQR{4$( zlC|BU<&p^7>@{J3q3vFWaj?_4r7&2yf~2|X@zk+LH(s7{uR+-q(}L`3{yY~vKyg@H znbAFVt#-3Wg8Xy^eI^*%TP?Vk=*eoFW}78WEJxdqi}^B_llOCtSI29!dp*!{-S*l= zg8;74wikAp-c7h?CD#S9HoH3`D7Jg`O>FV~q#lz&kM22iJVd9@?O7vG%@b!${d=$D zmMArJ91$-Cu)j+N%)_Gz36~2IJv3SaJ zg_W5(_3_cSpW@vjW&+w+7MqEri<;?MgalzmGQ(oBH@9Z1`c23uC6!2VX(Sp47O)od zIKu|Fy#?mTQd54+Rx(d}1tj>vB3~=_D^s3seU8xA%W*5JYgKO<8q$RUAk457U9ilzED$nfQ}l-=vZ0th`6}87>+qCrrzTK>m(C^geK!Ks+ZSqZyS$J zPNpJY(a=GBR8ymV&(6-S>M0`=`F`K!2BfA{Zx{rvHLnM3S=6HY4yN<)%CioEDM(ug zm{}omJ6h1|PbsSLH;IwF~HonDFj>}(!(>L>$!+(kP~!7XwTCgO#>+6 z49Fbt8nrzIBK4>RYtu<^pz*-$V-RRQFosyTmDNcPu&P#ktM|_*c7HZZ|72!X@<-XC zdzBWjj49^unc@8b>Izi&*3$$nLldbg))J{oI>I0LK-a?0#6v$SK19YY!%~%3AYlb=nlkTy?)r3;vZyhy?_4{l>;d zQ=rUzZ6R>a(tYvJG1(aHtNxM9c?dH(rcq`9hs@ss%vWBCZaY1bm*39uFSpC_RGTOB zdYC)o{fcXOPFAT{V{;f?B&U>b4g7CY4UsCUw89yMsDiEc73VoB$#{eudhScFe{UF^ zhhGFjK+RVRJCFhctY(D)F@VVNZMD$zok{1fcSwD$v;84(a_;Us>Z=<1UJ~b_@#%wL zX!)mBC{z<96JJ?z@%t7CKw7g(-c&F-Vfyw&o9An+f$IQ_&_}uaoIQmZNr?uuu_58W zpu!qv+-(@vE_qyBOa?rL@zYkwx=){^RDM{M1iOqODGeI~Sb2BDi!ik;Z^;{qDx%znx!{Bp&6hiy5 zz;oBgCp2|-j9;9tnf{rZVb=CCmjticRF1dMzCDr&>$Ivgu~7+lWt@mX{#b;)J}$Rv z7O3D8gdv$l`VKY-_&#YezK1Of8{LF3!Zf`XbP1Ujw4T9~{sQ;9F_7pxs3ecQx=bCO z=7WP-^{+69_-7kMg-!?Qj|J_Gi;g8Bf?hsl{uO-H&8WZ)Xb~-mBDVBpE!}tS3mDhH zmYgg|7~}ljxWQi2RW+fjx2Ne^4mHtc0p)V$fM<8fhV|I+bvG0U4JqE|zyTTkgb^8U z$8`2M(BaQcGI|UV21f8jhTUB=5N27&j`k%uPG@EVsBGz>u z7v?<#yGS-^nmobpjSNBue5d3>lLFTEm(-A}sGD5iXM@fRG=PTCr;s})W^i+BTfjJ! zZ1R0=5JUqhXh5mi`%W>Jgpm4_>D%UJBB28HRTQ{4rSsL$&%cu0E%g)7GuEZ6NVHr| zE7@pE6lo#+TVsEp`1}fKePE6P{!B9sq`wxWr2>k%9=LTN^a&6w{=C{G1|NJY6&T#t z&SC)+PXIn6aQG#FwhnF==md0j%&{}~m;YZ)HUqx3V8-!#BD22%eqBCl6Wg|JPi)(^ZQGn!6Hjbg6K689ZQD6d=H1`7_tF3J<0RK}RaIB5 zs#R58-D}zJq@R8QD`RvU<_08$|V{wH!0P(cVH6rr1m1K4m- z6qK5aph%x0m613qOmF~k*%25PSc+qP!^GwIV2@`ldy)^yj9o68}ba*QB+b=hr}OSON1Q! zB*F4*>F7V2eASZexRWho7LE{fGO|B*u{-4_kGq0EE#S&V$^h1wo4&iKyWF+qCY}lFv zPxK=2=*K7wkD0>^Dvc%ebX@WW3Mb5+x3b!wl zNByNm?KN+!Vf~kV=0R%~6k*??E*6ZF!>c10E*9dUraG=%Js2j#9S^^f4i5%DsRYj4 z@ISrKOQ1wT{Jq@C$;n)QBgC=&n*y>D z(j|*YIAVlNuQbe}%hb@-7Jw$!7TXrLbwAJeLQt<@_;dMy*jXNp=Y!%yd8c9IfFu@a zpY9UO3Gbl-)cAfVB(NQ3bxC9HNfU;ul*RbKxC9U}ik>up%Jsr|01F^O34KS^_@+UK zR0IP28^XGeTmdMm7rquUC0KZik`rDokR|_{4%lL^tOJHipxzZ&L@%QUIC(E5Blvej zcw%8x39Q+WJ3^G-&_BP^Ct#ogfeR}t(53)g6B0F|ufU>xCsW{Z0s9r`7dR!nn7^vP zaR-GRGMwKsgMkU2Y`~>~w-7#Qh%FQTzOCQ^o()tw(B{D32}d7Hvz_cf;se(W^Vm=P zgvJ{QLisHPL<+_<>SqD3LSUJN3a?avRy?&x>Q8D#*D`b!o)dnl5NvT@G2=g|tZ24T zZbGj=2WDtpxV`w=F?HkkyC%71l~Fu`L;$0o(9BP&ec7gyalvJB)FnWV6sCjtrXaKs&rT zP&leP@UmY!JdDZc0i>SV(h9&?aw5*`!b=bJ96V8!7CtNd7R9 zxgi`%2r05HvYJ($C7I=6&Zv}kBN0z#8D0I&ws*Q`z9*lAD#;=lMyl-;Y!YfxXcE6? zO%;@qi>hl?tWvE~KBrmkt+YD}X6b8*U|DkKbqsxcJL@}}%#4a9ogtW^l3|pA!rWpi zX6jh~QmVeWvMM>33d502>dY#yuz$<)$K3imN5#%vms8gv@Au`7yA z@J*7gaIOTd-7oz2rzgAE(Aaj^lGq(=^wu$!O02r9ZJ8ZxH5T1PW6H;Ks8(1_vD^j4 zCHoa*bKF1Y%h`%;^YKb@N_A`ARl>`J|3)q@SN;9PQczLeA>v+TS9M{`7lbd%plDyC z*Rdh)3;GWAPWK}FQV1ah0UN9ntQ8C_TwhRO*lt+74N6jpG)cL96rh;KWMgBUZJ}f6 zGY!UG$FiM?%Xq98qPbTa(NgB5m`635VwY*EWzB6-Vt{V&!+gQyVMS+=cg3gFv*Xzv zMmric+8AA))|Bp@_Lf$gc2f&WyRMSD5?>>^s;i1agG0Ngj;U^Y3AgdMrFeUDBzL(V|moWrcfn!vhevpb$#9Oft5Upp=X+%-IQP92x0GmFFYV~^o$c?#uHlTsA9 z!e}^jXLJH>-o4862^anyTBm~c!FF6Xq&BNIW?dTHypH46y7gacVn$*6uLw zf!@Kd;t1mLuK4PFin;u`p}9GF^?HVS`+C!zO0QFRvGO=f-6$ z8Zp?zA$eV!+&%36N&XT3ZJ)P5yFlLs$B{iEHITXO!v@U; z)6)*n!)U41or%_(Uaf3BAWHrIm4HG-Aha&r63+jP1r{Z&DNH(aBxEtfKjtciG7?cV zQ@lF56Q>w?DAp=&STtN*T~wa_QLEeh*&)VO=KI6|8U&4#+L~T@J+tY4v8qxZPGhEn6E>yxSqai=b4f4yM6 z0j-h~t{sUKHd9foLNE~GLFKpAqHKdpC2N9EW&nM3~ zx8Z(Q&jrnb^@0jBEkl;$1q6Qtoczz$th!t`7cN83`gugrMTO9+4Z)KI7i^{M9zq zcp=dxIjyv2PcNf$tcTl=ZhJ-H^w$E#B1w&ah4#XN=fLN1 zRn`VaP0Qcanl-0x*X!op^ILZd_solxb=$MrbBm|>hs7|tj;UeqhsVqg+;**Y40rtt z$MZI~&D)Pj$QVd>@3aRu0a)*)O^m-3C+3gw{8FBG2aXH7Gil1ai=s8)zE(mB$uNAJ(|7WHXm z)wzX`A@I<{?&EtQ-y*<6PD~ND3AG{9L+4leR{hd7`MNNgKWa{(Bp~BU`&RUJ{A9jz z_`raqFIH0Dv(x>s+49_bGu6eJPu350>Q^>zAUcLTTYa3fhF zsxAxb3=i0cr8D*igX^a=#7?Du)+cyRJ&4HtiK!>Ray2s5kTjE(1)>Jzp@2Yuk%2$~ zIbgs)P`D)!*uOjw5IG761A+vk$bg?hKFI&71=h<4{oi@zuZG{1g(W2csj{(? zsi~c_g}uw*n2RS0oIUMa3_a-VoJs!E$^Ys{ z#MIf?$)|_kG0s;~Qk`(!_;sJc73+baW zbo*KIs0S6o3e_ci)I}|uX&@{pB059h_D%v#tTaRq9TgQonU7jNi}W41IF0mRBR0r^*l8rs ze-#s;YPa?oKuCI_7(v9u0>En$fe$bKH8kiO-~19V0E>wMg3%)ZomyuCi2)+%4NB_) z{Ys%Qv4GJ##UQdfjQ@2F&fpLEpCOn*Xgy+KIo(G9oqoxFz<;GA1}0z{SJ+q%7jb}2 zF#)n~cwebl?{9z-Y|LA0c>mO(^?2s_QYR)L5y_y>+N8IQ=bsw1SRi7(8c;I1ylsQk z-5)Qf$fs>dNM%$2;VLQ`P1%F%{YU*{+~c{@+(pf)&=^5kkpNjZHb+%YIl$lw#YknW zeb$DyFxo9EwT-T2vp~lzoa*Z0oIGjv_y=aa+p4aNr=Bgux>No`c#!II^h$a^Kv-a4 zG}<`(COT(-=<%IB<~fT$c?sxj?#0u~_RsBS8-iJJgNS{eHq_RMpGxKx{KYx_V|T(~ z!FZQy6@NV~&=VccpQ%;t*~z&8-qb}~Yp1S-#DDrJ5u6uKMjq!tt)#vJQUlpu_`FEa z+y2AcGE2&f#$CPD-5SzaJBztV9eY_L%W#cQyvS^A=q*)}(|Q1^b_G;?&(*c|k?2JjGEp zF@mw;y%t&42R{~et`|V?Ww}WNIAH2Z-K8CFot@_hQ6|^lc%K!a%Vk8JG0f0^Ft*eR z>A68v;_4WhUdG(D;y_y_R^)UrROav{qC11PVYx@mZPxV4NQAkaV-qnj5FAeH3#+La z7Ff{ZFHVNW8r z@Az!*&iIa{4ts1FPX{@B$>EZ7Z-#y#!SAT%cM-*9N$zY-lR!Fw;oNv%724SdyGL4F zCiz>$kp95Lj_gQ;xuqpEG)L-ma{RZ_-{Y*hdIiM|WC6--Okic*tbxOY_dq+32aRk;2>1`jf>zA2K8{c1@*(NAv!c{X zl$jDIUioZ?$~#^g1?F#d`vTL_sUq*4%xkB!S{7v5etw)^kI@u59?f7)WIuP&5^&f7 z4afTC>)ts2y7GwFVzXhQjU0|!toS>DlQT1trvcl}G+uyrp0;aF|@0eyh_3k|gmKJGz-t zd7QOYq-e)od_fl%A)*3lIGiW#M4N%Cr~Ws#488VhFJ=qeqFGM1)4S1;%U2xtDg7|< zY4|JL8DVJUb2oH3ISGW0s8juSuskHtt))(;HSQ*{okWy~<$B0({_ivwtG(uP!L1LY zkG~_@$u#%KF>1=K9}CtHPVE>P{9#z2Da3l`5E(PKCZ*z%mk#!eqQxKAkY&LhwL?z5 zy9dsr{YT^NOSfwuVHG&8Pe00hOuFmOr3Z7?8O96a-b$($DRvJ^>Wh=sAI4I%>POew zXrftF8}((CgUJ8HV2#(gozr$cAMEybCXmRYGjul3*W=iWD|8 zX>_}N-S}*-Mb`q?9_a6f?Kzhh_7DjyKkkmr2tX<$f;v*8s(9K%?*CAWXQW|kOOWi_ zP;TP!d)+Z)leJ`lgwGZUtJQ|bT|(k^K2q$r^6^&LfKo{CM?qwe9miMA{dX~ z2g2{ok>a;;fgT?LtKo%8 z?U9poioVBULzWTGT%w*hE1Qq8)5tT9MyL;;S&?GEYEYH7jp6jR63@AScL><8lr6G& z2vK7)k+mS66zufI*3!9mjP zp3nY24dyg==d0CF@eSM5_K^#*(ThN<9Ln=K&@^Y7iXocd7<-3hFD>onaa7jsSqSBC zsU0oNbTlw_BTy>ZfRWo1T`3gp(w`ggz@&K~x4qQ7wk0=UnzZi4-@hvx9v(ArEUtyP zhopAFRA0QP)r4YmH^Q{L+egY~eF*vZAmjUY_Sj%xW0%`qo}WYbe`?+2@)De#*&rd$ zYHZ|jyGl}TM`Oq*+L?g~~awTJ$98=1Wu?cw&K_M})f>NRC^EO>Ihr)k|Ld;a&<0cKk z3-um=HyZiOoh~;LqclvdPcnA=NPC=d*vp#T4eZ3HTt~!*e<5dPYqVVzTyn)3Bi|eG z-tKp$)$bOa+=--k`K}E5aP5e;0XIGNa{RYv=;%+^){zCp*a9ItyW)?YkC#P_`aLqA zo6*m!qr!%{&fi(vH7FTJe>T)IahNZ@k%jh7F5{kOtr|>kVoAtQo%vVHtX-~HJd7g_ zo3UwbMaz%+{8%F7`F!Es=AnBlx@ROX*NN^w?-DO^7uk}g#y`}{jy&_2$DPMpqd>vR zqM`C?tQ%;_ieBlu{F_~Y6=)S305H-5@cr$Ea|*lC1elTSp!1aG3CuOUTO6;EGpj4M z&_X*aXS-Yk8N^vFwrJLe#e|`4~$z}80n#)dY1Aj05dd{p2jUZYq_k#=>(h<^$x)`u!Jv5ej*8AX_nJwf( z|0v{MmUX^ZU{}LrvV+2If*pQNHq_|xNj7Wxegi6YCzkesx3wb`4zn&QBLeI9=0j&E zI)ok4)4x}DD3-4&q0AWp8qUnp;P7(Uu+;TTxbt%t4cU{=`4v~sf)5L)-(TEG&8dL9 zn~4vMT6DMg@lJAgp28J9d_ZO1ghB{Q_V|_7i}g%PHC)4T*Qcik#_a2f(j)P&uPMkd|f${tEbE#ZAFOoY&~KEYvpE zi`?B?>_rMwvch&{oRojK9UvlhxVW|l30NX+g{gRx|REc;ua@* zpq_?!SwY0OkCTT3r+SAlXQjHyf$V|0ey}U4Qbc5zutsogv@`gqNL#ATBZ56RHWluy zQpJ39CF%;P`SFI!bNM&QA+N&Ehv%Jm*|t(<>p%8!5tTmb5A^V`DB;%{ni?cD;%MHL zf%3jZ5fQ&zl2rS6B6H#8iX5NY=f7uio>5SBT@i5)s&9&zgL!+ljpgTL{$Zq~W+saY zs3eU_%BBd?pIshIDV4yUTbP>CYNpT|9kq;zrs2(HZ_+CKk%NIw@Tp>0+}(Vfa^fUxzTp4J^I-of4M9b=F+J@8O}E1S{gO!c`c>W^v$BI@xQ8O{gO*d@ zcrDs^rraH>9quTC&Kmk7LJKj@=(GW zpiTj+5*y)9q#U8wUHyu>+X*|(#{rgc2gWle9 zP{>@|!m__V@`N?w(}FXpRtmh}ut(H;8gB##Hw7bZwCz9&ncnhaxZ>aXeJap(k`=zb zaTtjp@V=fQA{mt#jH=gc1YdTqQfpW1a0x@)<8s~Lv6b4c9#j;PGU&bWVsoIIzihCK z{oTH3vTBp+Yj!zCuhedlU$2hrIN_YU8k9a^CTn;)C;KhYgYmrx1pKnQDm;aL|NiVx zw9*wfHklX4j?7D0sKB5eAriSv_&a_F zr9sxyqFK!7Q!PquPPYO6&4n$OffhM7If(RUzoj32!?$h2^E2#~U7+Fn5~W96>Si_B9e09n z61`vU?2E!ydi>0>{G5!80()d>=X+&4Ps)7foPppxgh61J)iaY@D{Ekxkyl>$u>6+HOV&9OKdiD8<-rC+B68(rTMMmoh(*)5w zMQ50*f~1f-!LBDvC68w2aPW5$E_Y@rtS`5uRQd|-9bNXR@|qm05sQB963B~Zr&fxE z@{+Q(U4FhPTJ=#>=Lsoq4qNOmB2T%=&7vYwO+C3bx~AMoT1w`>-V}}3sWW>2eKXRe zU3)>AZD0w>Ai}y8n-K0mKlSG<5}XpZ=iuS+O)emi($%KhgL~%BdCYrQS#sw=Yx^kM{)39eC)0KOeVQ?>uT@8o_Jwly#TiQ}-&+ zxL^--;SkGI`lO%UT)A=1MP1gx1%5TI>YdxnD0ncdG)5K_HAzaz2edSBFo%j|*9?09 z{X_VuXF;qEsgBo_umNi#uBLPv@I$IusoMMD4dz8bqwstcBj5K`?8GsFBRD)f`K4RG z2hp{=F{9!UEV{-RPlsWbTPj*b74ThGJy17aJ?lf{G>zBuWN;>cUc}?SGv1ecj%EkOt2VcgK~R2fjo$(`a>A)Vs&T~ zwwXQC>*uw)$$MR_NA)A=1OW?9Pu@}(&bLI1^F@%oR7UFo$P-}Gs93Jqqjm`Q!TDC} z2qby~r2d7hq(oxNBo<_(Bdmb#Nv{D#5AsF?s~S7&3D~v(I*lt_zt?lZsl1-JI6NHb z+ukZ|@c2%o9q?do*R|^@p50>2o$^wA+FQib$6X8*rvz(`@wOB_#ahA{Os&l z4Q6OiLuAtI#0qCqk0T#V|?0O$~B%#wMQ4KUO88~l2 zCkDL{dREe-oX6QmALv* zN>K02v$Xp2Q_qk}Z%O6CCaT*LtJ|1am2$0{ z_YDlgO<$^I$TyxoZ!{v7x%5dpxZB$XVLLu0iP;634$kb4FqnLTkY&$8KKPw9d zwwt$O3jN~nOA|_DEt2*&q}gsgDvQHjt!$>a533@t-j4YPmY|&m0JKHMwoes{fc%Fg z2LvK9Mh$D1%HipBxXE_%sdf@n1Rf5Z{ezv$>Y-3SyX{i(m=R*W0Or7x*Z@V=4Xq|C z!@&roA>#Y{MSTqI5ww5)z#J=B46(pn^jctF#g^HfB~^uen(if%R8+!%iX# z$O}XpHy)BYilJkYl$Or3JMZp zAy+rYR9sSoD5Nq%u3-%rbJ^s~KLiFN;WZ*jqy0tzr zlKA~C87Pxt^dBT26!51LI|cr@RvZB%wKG0ydf=Q@wMefxx8^^~>H-#1eJzK504A>v z<8vj9{L+INOHu%5#XZa`@XxV>CNgm8TV+*1OWH5R+mCiout5Jh9Gsk->4UtUNo)kb zg<05@v??30Sj4?#rJ3;orKP12{$?aX-^hsQWIqYxQOIQi3JXd6-tW~##Km_qIuyBYKc26aqN24o+pemG zlsqOLiY9E$r^$!I<6ZTFB9wVPKR*{}x7a;>yj~)qp%v2~!jc%sfW=(Q8%&Hk>Z@V^ zuQ>4dy>Tv7YLdjS7ACUWZ*2*Uzcgc-8Oh!V20Ll5Vxj>ofPHml9D>~5~EsW?`}+bYO;FZZX?YA!!c_Q%tT#j4U- zEOz7KUUmU%3;`m>!lp93*E;ODTD`t5UQ`75$3o8*$W1Fud%@tK! zmzMfU0`@zYh?Eq}{c>Zd&H1pXmh9E;a--|JS?tV=0tDcwp?)={F*RE&A0-idFpk0% zY+g&#^OAf4TCCQ^xnmZSlY@6VTQa=CWCBVJ7CeTvN@WqdXo4UV)S@6euQ1DS)4SE{ z$a&}@uS%lWZns%x9vmF}Ez9Hdout6~^}*KX^}OYz4v`Ol3#UV+gM)!x6?_{O>CAkX zuQe172@hBQ`$xDv!PCP-0*;f1r(ui#%nKs>9YZi-hLaFWV|zT#d&(S5>(fEXH}3cSX$l4UyS3m`%K!cJz4t`xq4 zYph?pg?&2FU;;S#)FD7H8`O=Myr^GK2RJ}HE_0asmKFti!S{)rHzvN$MK!I-|VN$i@wd=s&M=lv)g5Z-%tFrxlfU`juILuw_ z|4P(gfj@ct)E_xu~pvr?s#(u zesa-tIggfIp7gc&N#jblkfx@Vfi)%a{jZxJVS971K5t{3cjrqWp!(_IM^7Si4=w`) z(j~@Y>5z9IyX&I}#Akis(?3)r~zRtHOPk}U&kz{`RsJEI z@VdejTXMv#&%?)nW8&421m8O!roGuJztfC?kE;OIJ@qC(i^Az! zo1*XboG*P@mB4R=g>@JdOSI(}O}TdKO|tI9ou!l=TC% zCGZ7JD>QO_5N_lG6;;}+ap;Q?eaj8rHT=tM)?h8U9OxL3eP#U6w4GKm;3I-By() zjn>eiu)ob+8BeXYH80~ns0ehHUH+Ac1toi%#6VkWLdc(Y9>dFe_qxX?E}QpTkv22d z&)3-292dxPEX}8$=|u^>-9h7~?#Kxs7{4Xj(NAtHa{Or$g^_VQb|KC`@xd(}EN;;w z&ZgNeWZXaN7#H6U=M_B^i;7GQX1%2ole5CLuO*K-JmEKxa`7^_mO1^lnb$%D@gK&B zsg5kqtS>So+FNXYUCvxebBOQ)&v7wbO7(X-wnZyQu<=1UeWr_)L8_!6bb0&l_AIRO_<(AMW>e2% ztW8rMx_mx0v7rB6Ue8C$+McJun2x29g;EW?n-3GH-PD5;R3bT=BMtnf-BNhX!l&Ye z!^=l4(m^DrieGovY{!&s%KP1pF&#{*mQ37k2-ev%GmR~o>+VKvteFC zmivl*ct~%a><(N-x`fC$b=&$-{dn{&)s8vilF4S&ZDMz|5nhR<1hvB*gPM}XDH$|+ z7Z&o^e%re5j{_y|oc?skd@HQO!bU7p3CC_ftw3j12ig)S!YiV!Y!n?s{asSJEm39) z7yr)E^9e1*b+p*hhSr8xu$cKgH;^_U9-vQ}=*B!$N&*5A{YUAu6Q0##qFaS&Bjq6% ze77JO=*`~>I&^F@XGpB{{JPiD%jt#EdU=Q&HCNtEP{!>5P z`iOjEN&CJpx__@8qic{vz_v4$A~76%IXUw3r=@Y7$-3Md5&NYPic&~bGYhUwrQ_QU z+H;U79RkJ-xsIddM&n3v+n)8c@$K}obU&L6TE^at|E~tNxNPV&oWR$f462Y@{Nt_} z^-i+i^gh13y4# z$zs=z_3(Kl+ROfolOlAn>O-GpNc$rl{J^n5R>6oK!tq6Lvkjhnjh9sO9P4Z>E%mJ_ zG-lSGF4LNJu_FnT%U<}8a6>%JjER#IZ} zbn*B3+_>F|9XPmwJlc;}lL%4|q1hU;R#e(0%N+=XkQY!$IT{U1Vt*J)mE6I8%KH~A zl~w~apHikq1A)U~A1vT$l9~^TSe$sxbJ^8;SjE3a*8=jxsJTzN?cRaZwhB{i1%5A^ z8AqeC%m5G zSx4W!H~)QCU2kxvq&(u<`q6OW)lQBH;Lov0q}f<8Ld8rJae{sDIwGNj4)A*d%j9u_ zM5}-3{jTe7b(^p2LgbZi-vlyv;F3itB>7wgh z>~W=>c(`U=+_lTRPg_^LIy1(*tyb|Ma^yyjNS4X+VbRKe(TdpzpPWR22&%TcK**rV zQ)b-Cs?1a3DnIBl( zewTmv^`>|Wg$X=BC@7Tn0Rfx3lry0q!A$Va{J;C++j)Tunf%A#hXxq@VM0mI|G0no z6aWqG!JFt;AA|@yg(Bb48YVtqVzBJlP1Fd!4-S}eqaTKg3R#0mIYsq4kc*@!ao+F7jbWpx_o~6KPU)T7~mN! z{!c~0YjOpdu>Q3S69V{71qmkn|M6%d0j4ltpb_(bJep8INtkHV=RY0|3SdhA{{;Wh zNdLdJgZZ;yHK9#(HtSXcvkQF+&gBa}6O#c06(`I5=xH@l~&+C;u8 z<|S(Ma*QqYziO$@o>m~o%{&2-@z7vA+A|xt{)$c+b{{nuL+#U@F?fNe#b#Oto@7gV zmm*r5vp+kd&kk4R6yN2b-XSp-y6l+)FgX5TjI`%Br-3h=OVPv?>FhfnOaz98hVCKZ z;l*O;`_$>YSt%J?%e%9_`k>oYFNdU0dQ0)Ku>@9VA1+A1b%dQQt#Ypt@T7&-3FB@b z8b?NGw{ctE(b-(@drS17N7icQ9j_K4=J+NMRIceJzFlSr?H6|)Zw|p%;b`uz72CXZ z^1(5++)9o2n;gehUwd&}(uH26;z>Ii9i!@Od=!rc)o@*#6S(!q9_c)jEzokP_?<1) z<;%;-#T95QVDdjNbN!C%h^;(&VxAoB-#`ae9vLVUA2C{Gc%#R1zXZ9}qy9vZeVLai z(vPETu6b3R^LzR}=k2-k{`*4vQ-Q%-lEu~!M9o%GRINFkkfIBKiw#@bS;Db6!+5YB z$Gol44(4vqL8j+v6GBBC*!ktYQ5KdmqfP3jYijhb^+S6uW7Rr2gr;_}%iFb>8ww6_kcSwzP#O!c^Hs;m31rIqI?08oo4I>EG4n~wrcI45T zn-(-vis>x<kJgRvb009VJeB_>*pTEQRlHTndX-LLexxX@=e9qyOI zfF7&tSUj!_$~9?%vZ*a_2P=M&WohMFyo`377+PjwXJ>MVynki|0cf(@OPzH^Ua>!PAQ#tgxFVFYQIac;NbFSXqL(4WvOk_ z1!O!i6{Vnil}D5p?7K%NIFf<50q*aX#;rgX9E>#&tIv_@lK1QGUdvz7Bd%krpdnhHDM z<=UgQntsJs>t>PA(jrHrzte)Bl__==kymttUML~25ct$0*L1%`ptj#0R1G&;7Bc#H zJPHN11oosN=Jx`)w(RxquaCwzd!t9IEj3yImIe1uxAPUH3f0D~r6rBCl_r?ak5`7b z_je}CSyJBXK@?7pD?+=?1zYDizMUQE$*M%3B&m;2=BfpCQJ z;duTR%KC-|Cie@SW%ub^zKY7>EWEscnHGVNrdoH1F7EQBQ66^7fHGB_1d3epw>(eW zk7TjknA&)=wTS-E#lt=&8#lJ}dt|%IXP7!3cq#tnI?+jV^ANfeB?;$K=5HI(Ec_sr zRdD9-@}_;;`Y2bm^ld3pRoKnomMkE^q_2k;0h^A6KnNjwNQa_mP zUUDxTUU&3cyX`|gvd06v%mchk{MHH3;*HH(0o34v{p#;x{jwr>&v5GQz7-{H`ooix znn^m{xS=%acF;r^L|~&*I_;>gJtd(hEG!Lm?w8+0#D-xEQ=->9K8Nm4nH*ebX9ml^ z)5*gWnWMhnBpGyhT>qRi%gIqS_BtvEHzD|nw><(56K%O_(lHS|J-m24eyVFMPDLXt zn;{po@5^0aRaF%$LxLd1ZQsrQIN1o1aM*7e%Qg|=19)cPwN6iEtJBpMdj|1ow8az| z;A3^W-?6`vyd=53pzE|Bw;uwH315E|M@N(P(8x$-B>wr`S?;wmCfQ!tj$xV^{{(&c z*O0nzwX(ZON#z&Kz34951F@yk%seLt7Z%cHmGIc;tq@R!AFQpfwd8De2q{;0C+mE2 zDsIP?q=4WHxjTk4?n@nffAd^uF(Vr-VzaeeG2kNwZ2RMIHJ|8^+MfAT&GOMu8R&;x zzxs;@(fKqvS&gWPKv0V76>DQg2LtiD(?xg=yFtU_*-4>7SUPZwmsf@5Ckar}4u)b{ zLx#pqb7ST7;*!I{AK_ycB`H*ku{az*H5<$b13RqdsS_Y1n^*ilp2dm!S{-*8U5*r* zG=<9@TwPV`_q!n=A*JyrvpCeV@KHPyk`*IM{N? zG`qdt$x+;g2eUHu7OkiRCqjet5>-UD)b)rG^Z)RpZQx#)bYJcnEJ*T}%M;Bq=*h`a z5=*#zo?x$AHxSl4T9C8U_%&LuxUS2(gm}C2W|wD2%a1f}FR^-1lY!VslY`vau`{CE zn+6YJYcotu(8xI*cR~;sG?liJ5DR4+0q0nLl32pVC{aO9Mkd8=E}25!s5bz3_M?J) zmZXqNjF=)h%uMM7gH9_127}&0ZSgjrSsKkho|3kyHMg8cm z&1z-Q^$*RxN@MK>IF*jEt8>GcIUf$6@wUpLoZJr1@*koJPL4O2KF5mf+s5_HqD<7aQB%~ zO`=QyVf$u7*`^lYn}@X<4apfY=u)0l{)e%x)Hvp;R;l%4)Mhx&lRCab_bTN8W zn`OzBDinZJZ}LNLP(o6Y>aKtQVp60eB7?tpAk1vCUOd^8lm~&1MB*&6 z_gvz^k8a*ke9kls+|vpVfbBm{V$R<#IfIXFf( zn$DU5?s-!~v11C3-3WGSYN@Hid3Am8WgE+68J0wa((Z6LoG^|}ALUE|iG~~w``o$K z$RCZ@@&fNFH*io;35|PH-?RLSWur>8R@^f08i_QN>aVYZgo6bW+(F22?BzY}F^;sA z8i-vzv4j$!w@(+E@OWR{LX{iBYF+-1=Dsqht{`j=$i>~=-QC^Yf(3VXcL^HYJvf8_ zL4&&l2ojv&?(S|qr1E{!nNBhV?s9hL?w)-gyG(SUzG3sET%^hYQ7kRW;2ql1 zrX|R~+8K_IXYZ~dq^&;=#Ofvq0C5UDO%8AoY*|Wmr!BuGlkrNYNR71lE~82Sn+7`w z*;f+g4{cphXibrmLOt>Y1g5C{@gHGp+*0|>wTGo;@1<_O2fjb;(QVR-R$b= zNP^0zzG>FlfyD6BVh!2nGV3MEs>H;^sq2Yviueq15EvThmv1-Qp!1R>Wv*B^|KRVH)Hsv>xq7~&X`oCkJ=%Wr?2M79 zZMvU4z^0btQGKGK7;x?la37r+opJ-3Ws-P^%b$qI$jq0$Awn+ta`-)qX+>$L zJCht>MoW+amX>v8= zrbirckCZAbcTsPBoI%aS`_MXH6z3HAd|Zl=5P?+s1o$cMUU66<|3MK@lKI;!&>JBE0bBYX7Rt$Y!Hn&-wsXWkbVJ=3R6axB zr-4%nhnrQ>fOyDZQ?Q$Z;{KrwwnTVN$;= zvy?N4T<#hkaQybl#)Mm!yzZ#mCihGiyXkY#51GE|GuJT zakPSG@YZpE)XvPnshw=b9>^b0a3F&z#+~{&J<;e2-`;XANgfx}W&*N-f}o=Il9g|8 zcR`K#u{4BPkM7-C8t+l(=`6if^|F3g#K)yl#*JRP{$+Jc);*>u6~?pZoCSN3#D>a} znV92n3j6iX(h$u^mV3%LRuB4t+NwmvxlC1pTb>x}pEqrtW zr5OIh>{XbSy|TVy^EC7NH8LKFU-3KRKtjiDa*J6~)3?#=$O`Qd^8tr;_gl(Y;u zuc(07Qxgr3%Gcp8e`C;8GCJyfEBOGUPKm=@-J8rr#8l6$p~>1^dC0Wkv$OE>krj-+ z^YZ$sBlc#sA>5-nJFTU~_5LIq2&J}QzYha#HK9yWMlB656DF;6N9xo)1FTB&tLVHwwtZI zPdEdzFj||)t4-ICqbD~CYR?@Pz{%GhVJcqFXeGWrDZ^|e)_8jKZMmjzZ5p;YTm3;> z;5zkbe)LU3Lytt)og+EU^FK$j2^D1?mFTiu{gv5~orqS-7o($S*Bx^UzK#)A&>YfF zPDVDfx0l8Ns^+7s@ zD%F*At&+ z?skwAkR9wmRNkFQJ5-i&c`Jd->3tB4neo$&4}d;Xs_usYfYFBpSWPWt1*4Vun8`82CzgGiRAi6Xv7SVoMAR*96N5j_4&kme-T1G(@`IYT+=1%4Me2 zT;J)*9R02~=txI3NG;iZ){TthF0n&U0U+b*W*WfhL`9Y;zkJ(&{LS8s9tiK77WhSe z@J+(kcyXWCA?rJ*Lrl42=Ytf8Vy0sTgVN+;v%kL}pDDi0U3WLT$vTFNoxV!Tt*>s{w}U`@zAw0X3L+3-82? zhpVglqzU0^CIJU-!*O+#P21H`3r~aXhbgqJ$r|}UMKpIIMp+on+Kb%{{O?Tk_TK<$ z&8G$%OwB62vMW7=DB*8M$;m~A_0bd4bjhHLdz;fc7cbJ@y!5G-WaG=>HvrrX3IN=M zA=kFLm1Tu}N+TC{+1IUKMl6u)ro)L==|k0ol#V}9b58GdE;6)Zbc!tX#P{}2!KJ_Q zs#uL>dL|_@2Ma}ni;iU(tgPfln=qf6TU*DSHeHA|?OH_B)jW`-H@#(4*3L*sdj%8HWjl`}Nz9pQXr~ewt#%&d9%X?lG4fm?t?X zL~1ov4z3f*fvODVeQ&qRMgdna8VMX**V5fItaJOHegh>S)>Q8`vB{3=;yiz$Lo!#h z`%W+iW{Nq58_@??r>W4ZhIYi^-fFt~i>fV1U4Fo2OQ4Mz-e(HPj0oRlMk`4dLGSXR zR&t}dY};A?4X@|-SBO6V&FlKl#Yv9=TW1ync#X}vZFtT9zx;`1K`WdyeRT3KD$?Iu z85wyArQ84j|7+Y6AXk}$nOMpb5)wLe*ymC9A74=6E)R{#uB5kc3xG$|J0d4#hedBp zf4jMn%Lwmj^~U`K|!k(Q6&z26=6VG>$A zl!Il!DQYw?^6>N=8aOHcxtOR;$9Vq9QC1jYLuX1VP}v`Wwy{>ML>^iDQF%^hY}Lx3 ziMIV~2jEu7T|`5c>vV9B(if+Pr{}x6hk`GCYP^tA?gg%eF*mHEfv}U{E>ed@%{=^5I&y$vP=)gxOI$VFAga!gVPwkX_0i0iV(5l!u<4UhWPn zstfIx8Z-FZ%lSCaVH+^%HA#eo4AS`d`Arw<%)^i>sKNOGIO{8aX%w_qSHSmxo5NY4 z%mxYFuYZtTtA>Rf%YFJOl9=Y{4tix4-CxUifzw8EZ; z^4?TfD^CH=QmU~JMA0Ou?5Bs3-WY@L(TRHEW~ zZo@Th>Yc1`M%Gp|x%zl)ZpYt%jtZN2S!VWeJkrbcoId0EgXFymCABVf=%8Njl>eHs z*AnSwWHButWW@UK64OoDeyH)gOa^W>t>*^3Q!$=+1l(^NsajLThIr=?r)J-bb_|6{ zr9=I)=KTo_0@edF+BYSOEz5dIF>Q+7ua+FUWPA95y43F`^$OOq$CJF0}en_ zn$B#P29ZgYj)2PsT6BKK$feZJ`H_RNjrL__?k-KjBFUYK)(fZ=OC${M>yO{-PI$#8EGzD@Gaaod&kJxHFo~N? zC<5T8B!DWTqM(qBmmnb^`~ol-xCPs_z&C#m>#1TG`MT;fvpL48{LtXV1`SstZU;S7 zIz0Rm0Gr+d6y?%+LjDsSzMc}2l0!}(OO*~Rbx4U0cn@F{5u}6U!Lph;Lqk>+6qL z0lL|dbq#dods}HzKHFc+Iau&ZCRuI$F{HSg{nL$?tfZ5X$0--=wug0 z)lLtx(({-{v6-!~hWs=gdg5IY>179@Y3@&9z_-Ql20M$6acD3=s>ca`NT2E8S zil3Sw;&IS$5W9zEN9Fu~{^Ic>U6Pv>FQBCDl<# zU;HiYvp4~`CA(U^0TKC*pUe@ZuC0v?oecfo)ZZXc z*+~^nYZ)s#9mdq;%1`Wxw{pRm1_LC%RlCyc4A?t>J{*1#$8Rf^n_~ap^vQ{iZoeWeadZ-sSgk>_GrlHl^j5)0ZNTZda)G$NRG+>qDvaYJ1Kr>pB_N>A z?0x_6kXqLF<68Zf9(+h(^ljbC#abX8dfxk7$MauX{17N8h#iC>EXl@?l@xI@P*x25t;2!3{Po{>14ZKXtj}^RhBT>u_J^Z2lvv>~j zr6yS(e<2SY4UK|%Q#3okTy|lztS?RxQ8J(1J67uoSwz(GZo%K=!!Geg+d+iZFC;F( zUW2%fV9W?jdJ3ryA0MCcC-RfB9|!Zh8g;aiABA3h7j_HbtLx(S0!}cRnC)9T_K;z& zdJzR?EMQgx_7x}aziiV<$>I|0r?z|ewo>!I94lTWWTi$0DEh+x0Rk)A`N?Hn!|os` zFI}|Tx6}%5bWGeL5ccn-h zT5ZrlCtJTKaz`OTnXJ&5G*d)Lm9aDuQ9eVod`B%fbFqBh|F? zXakyNI>Gsn^Q-2^nEMYJ3*B-g)irc=O@g=crY@fC0;iIgT-|ae3e18$i#+R^E($GD zB0{a(c~6VoZv~1X_zKUryk@XJlPp|3-OzPv;WyPB$tjCW3}8sm!MrhP#I%0CO>GZx zDP((6=In^<33+*Y5B*`Evg1T=f&Cby&>Cv}v2so2R|;?Jb2a zxl`o!W9<42;`(`HB$MU1z*{O=|B~2HLoL`L_oET?asf$Q(@loZeLM%!SP^OV>az+~{AWmcgBGXZ7bA|*GAhY|m6fZXWP0Fg5w z%scqKblCwUM!Gv`q0qm8s)mygJQ=-xn@TPBO49ZbJ9zKiqJ|kCGz7aE6_Mim546jF zk336@L7rp#7l7sjny=_tPta5jpXH3Qzmdq6wm*CqV?nHe@!h2$0qU_!vrK96>YyoW z^{3ktabQ1`JxtgR&9X+>mr)NR0tSvCuxz~YZZZ{~G(gXiYD?i(IHRU4SaQ`Y;*X<- zu~ch1D0any?Z3#12w;IJL_pzHGB|3)czklQGo!Nh5jR`q&v$6a&~w$X9TK(QJj zI1YKnuIN0!!lE!l8IRk60lA6lXRt@1TJge%i2Pk++D zuzO5UEvk`PNGNf;;2jR8sw zWm*WN1##!($*CIA@@$;0w3*8|wth5&;ro@<8%jS2m*i9;vB=Or`VpxqVEr>R1tZ~i zMQ9^@`ow%{Uk~SlR4s$V&qUD`TEhn{PFdp7Sd31)F^35Hl5JHmqSeug98W2ZaGG<@S)*hJ6YLm1`MrP?j8$GEQLm)nTr^0{<>h$HA+w3 z8*Ros)`FCO>VjCZ!rj9ojWdcj=y)Jye{_zJ--Cop)02R+u%PxRnI;?;)&Q+BYQ5xP zIx&j!VlIMvngwFu-R#4v!emN};T=5sAq#?*jSUS0ePm@t-?0jV>x%;VVx#_4BVs0v z3>jf=W#&GZr`&dfXd>$HR!hM-C8^S)-}E;{b5w8F1pD*=6&_ZIS#a>a*By-rT&*Hj zI%7i)`=7(-seC(~#YeW)9aLebX^gsNUn?^*aIB$A5V^&v(0&YmqD4TozL2xRn>r-+ z-cA%_vR(8a&tMgol9*tcb62*+Ko^b*l{dDdv0rXh#imVXh0s9y_fJ|5G~&S%RSLVq z79;ICD2zB2Auuo!1xV`2F=>nqtdf=ZXDhUn9SSFR2_>=e&)HhrA-?O&s@gR?*sM+C z(0E|`TP7Sc9!(z7z4ffB=Sz zM}U7pK=x+|GQb^Wi$@^IF4MBHK7e^1&d5r|Qlf+T|7OTPy7(H3x1d~x=>NA;w-iC+ zj6OC|ENi+|%ZyB6e=tlyY=7u8I=KoV3QNm`up0vfEE$@>EbSx|ZifUa9-hu_Ei?%9 zTc*GL$}mSWcTfi%*tX)F+8T9tHjj5{=JRGQ>+h#oYCM=@mBuUHIiT*8>i+evr8I#b z`)3y&mIiLDJJsOv?_rP%?>;W7#&rRGKp^?u?|m=+M<*8EYN(xl5W22qE)#J4sFQ*| zZT&AjYE+gsB**DDZxBLp4Nig&e7l7DG1+C48dkDiH1CJ)G%8EWPdqEp@fD%?{rXP` z2tHs{Tvhk%ht*k~WB=TwfCnz4xNJl2lG6@mEp-1H=braLvtVbv;5|H&mc%G2SWA0; zdsO06mM(3RAK-iiW<~>(QNKXQr_l5}E&HE1kCc zBK2Ki`nitGC@I?(?vs{URZ*fZ3ssX36rsNuN;c{s5fU8F`;edE#Jh_*4FC|ui`R>k zpBdg*Ha#IpqjnOF&JHv4U3{xf4^o(l*U~OX>_GHC)3Rrd;e5$&gS}08XE-&Y@2AP z^!_cONZ9))u?_QGVBBsP3`yV{qpHvx`fm~8B)chAhUhRlxu!(2PH z&SCN*edJ*oVd`X8xKPizb6JVHypOpafV6FYnoaewu(P+{*xv`Vh}_(f{Ldjit#SJpi^Oj_u{o9= zBdx@_+TkGu5jAdWF5YWYHJk>%PIy@MPdL<$U(?L>gZYhJ@+@l(y4F~WZ;Tr94Fa#Z z?NH3G2HK&aZK1b!_TUPRsFuD)^Tm621%7GU%o92Q9M(xxP+_+b!~qG~b(KQE?UhZL z>I)D+(9_>9N!oX@HOLzH>gO#C$iILbJT3jZ-E7D5E45N3Nd*N2!2Diox4`yE2g|Qg zrh6=h9~&so)TUj-QCZwUaIaztD3PtF?un_%k%(^&QuCEg*EY-Ig%kgmfje25m{zTe zPRO?W7zL9B^DZ`}6JCOeiOH2z&UNl$%{AoAD|dw9O@zS~or;sRJRoT%Lbd6XluSWi zEMsa4o#rEpSdkbgb_|ZDO_0xY#)k&bJzxFP&B%dG`aqRIu`pY;$O-?RehXj!1 zQ}2ZMM>RE8;0mKHEq0TvEYt%`rlea@Ru~d zc5|yA#Xd-Y#{ox|>Mn+K)Kp5-*|!OR);nGnSv-J79-m@JduVm&twZO;6S`@=*CQ zD$k-z!|`Y^Bq1W3PAgG19Wl2*SyVCsK!6MY7C^yi;(~2Ek^fm~_3_*c?;mSA72MPe zsrVnkGJ)J(JZ#%QE{j|hQWc-|<>i&AhZ$u9l2w@*{Z3YPHa#SXH;q+iy%XU3w$>qi zs!saN{<)n54IMp|%v)p8+uM5sxS@s}@_C*zJS6;;Uu2_XGX^NC;Cu-O>*K)+cv7Z{ zAsCE%q1kQiG#7`Xc>7QwtZX+vGZrZ5NxG+r`xX%fexL$`a0)Hgu1i>x=-hhT{&ao% z{Iw04UH6zx)^>~p$OkIl@#kvc3(9*mOjSh;LMAdzBGw{+N!8oT$_d7dq%-&R^nm}g z_>+`v$!=NR&IN>Slz%Hgu+LI0RAgT?BcEfJR1@+z&KMQGTXwDUn=F(hA+4qdLN!0Z zSzj^|aoOSc+#Hrf$Bf{7#)}M+wQcWjF(pKJfzOT0#ldA$X!W6`Cni#JQ#<@mAubgE zCm}8~%zw5z*);X%G%7R;fb@%e?)fZkN2UgY{+yUlik3K4UVj=w;Kt;8cgpxHs$S29 zojg9w@~x}m$jlp9w#*zliK-Df<{lEH@IXEYe-172m#3dgZ}8Q z%I2p@N^wYP>i&MdSsYZ#@01OVvtNB%wuR~C5_+-`hcx>%c8-T4Tu8+1g!w0Ts|sWn)UBxrulywK`! zg3jB3S%Dt+XG!*O>eruGXUe6)jJovZf$LL=Ag- z6h855d16(kOC@phzs=Esp^yw(R~%NDR0g5Q#DYC}sj;UbfWSdXWFqHO=HK?IAz+H7 zfEIt$*-(HMQD8mUDVc?NFeC|h;KAISYC|F@A|$jB24glda8NH2VBjD>n4110@k~4z zk{}UYbCHyxQ$tN2Yc&B0z`RlAtF4_Puee9l5Qb z%8I${y_r=q5O^s zx?@}oo9+j@I~bO-NWi6mfFVJ_J|}7q^ZOKVot^Bph2Fyl0b?fJZF}AtO*NK7JA8Y) zww_36A@#m)3ZV9rEq}Lay18ugOx%dG!|Xq@$e+Z4Wn-gSXY?{`#~Ui`iEDprrlhYZ zieY8f1D@50Z2d)GRo4113a|!ta9%5e*@`AovA}3n;K5qH6JkK`QAijAR}(lAVEJ@{ z)@O<8fwthT;K6fWGk^yS3c@1sYJg@02J6-fTIZzx26QIL2l||d!UMBn!6xzg6G;nn f{{MT(duU$bSp6z1G%et_62N37l_aXfjDr6Q^UuDi literal 0 HcmV?d00001 diff --git a/gopls/doc/refactor-inline.md b/gopls/doc/refactor-inline.md new file mode 100644 index 00000000000..dd857f874dd --- /dev/null +++ b/gopls/doc/refactor-inline.md @@ -0,0 +1,161 @@ + +Gopls v0.14 supports a new refactoring operation: +inlining of function calls. + +You can find it in VS Code by selecting a static call to a function or +method f and choosing the `Refactor...` command followed by `Inline +call to f`. +Other editors and LSP clients have their own idiomatic command for it; +for example, in Emacs with Eglot it is +[`M-x eglot-code-action-inline`](https://joaotavora.github.io/eglot/#index-M_002dx-eglot_002dcode_002daction_002dinline) +and in Vim with coc.nvim it is `coc-rename`. + + +![Before: select Refactor... Inline call to sum](inline-before.png) +![After: the call has been replaced by the sum logic](inline-after.png) + +Inlining replaces the call expression by a copy of the function body, +with parameters replaced by arguments. +Inlining is useful for a number of reasons. +Perhaps you want to eliminate a call to a deprecated +function such as `ioutil.ReadFile` by replacing it with a call to the +newer `os.ReadFile`; inlining will do that for you. +Or perhaps you want to copy and modify an existing function in some +way; inlining can provide a starting point. +The inlining logic also provides a building block for +other refactorings to come, such as "change signature". + +Not every call can be inlined. +Of course, the tool needs to know which function is being called, so +you can't inline a dynamic call through a function value or interface +method; but static calls to methods are fine. +Nor can you inline a call if the callee is declared in another package +and refers to non-exported parts of that package, or to [internal +packages](https://go.dev/doc/go1.4#internalpackages) that are +inaccessible to the caller. + +When inlining is possible, it's critical that the tool preserve +the original behavior of the program. +We don't want refactoring to break the build, or, worse, to introduce +subtle latent bugs. +This is especially important when inlining tools are used to perform +automated clean-ups in large code bases. +We must be able to trust the tool. +Our inliner is very careful not to make guesses or unsound +assumptions about the behavior of the code. +However, that does mean it sometimes produces a change that differs +from what someone with expert knowledge of the same code might have +written by hand. + +In the most difficult cases, especially with complex control flow, it +may not be safe to eliminate the function call at all. +For example, the behavior of a `defer` statement is intimately tied to +its enclosing function call, and `defer` is the only control +construct that can be used to handle panics, so it cannot be reduced +into simpler constructs. +So, for example, given a function f defined as: + +```go +func f(s string) { + defer fmt.Println("goodbye") + fmt.Println(s) +} +``` +a call `f("hello")` will be inlined to: +```go + func() { + defer fmt.Println("goodbye") + fmt.Println("hello") + }() +``` +Although the parameter was eliminated, the function call remains. + +An inliner is a bit like an optimizing compiler. +A compiler is considered "correct" if it doesn't change the meaning of +the program in translation from source language to target language. +An _optimizing_ compiler exploits the particulars of the input to +generate better code, where "better" usually means more efficient. +As users report inputs that cause the compiler to emit suboptimal +code, the compiler is improved to recognize more cases, or more rules, +and more exceptions to rules---but this process has no end. +Inlining is similar, except that "better" code means tidier code. +The most conservative translation provides a simple but (hopefully!) +correct foundation, on top of which endless rules, and exceptions to +rules, can embellish and improve the quality of the output. + +The following section lists some of the technical +challenges involved in sound inlining: + +- **Effects:** When replacing a parameter by its argument expression, + we must be careful not to change the effects of the call. For + example, if we call a function `func twice(x int) int { return x + x }` + with `twice(g())`, we do not want to see `g() + g()`, which would + cause g's effects to occur twice, and potentially each call might + return a different value. All effects must occur the same number of + times, and in the same order. This requires analyzing both the + arguments and the callee function to determine whether they are + "pure", whether they read variables, or whether (and when) they + update them too. The inliner will introduce a declaration such as + `var x int = g()` when it cannot prove that it is safe to substitute + the argument throughout. + +- **Constants:** If inlining always replaced a parameter by its argument + when the value is constant, some programs would no longer build + because checks previously done at run time would happen at compile time. + For example `func index(s string, i int) byte { return s[i] }` + is a valid function, but if inlining were to replace the call `index("abc", 3)` + by the expression `"abc"[3]`, the compiler will report that the + index `3` is out of bounds for the string `"abc"`. + The inliner will prevent substitution of parameters by problematic + constant arguments, again introducing a `var` declaration instead. + +- **Referential integrity:** When a parameter variable is replaced by + its argument expression, we must ensure that any names in the + argument expression continue to refer to the same thing---not to a + different declaration in the callee function body that happens to + use the same name! The inliner must replace local references such as + `Printf` by qualified references such as `fmt.Printf`, and add an + import of package `fmt` as needed. + +- **Implicit conversions:** When passing an argument to a function, it + is implicitly converted to the parameter type. + If we eliminate the parameter variable, we don't want to + lose the conversion as it may be important. + For example, in `func f(x any) { y := x; fmt.Printf("%T", &y) }` the + type of variable y is `any`, so the program prints `"*interface{}"`. + But if inlining the call `f(1)` were to produce the statement `y := + 1`, then the type of y would have changed to `int`, which could + cause a compile error or, as in this case, a bug, as the program + now prints `"*int"`. When the inliner substitutes a parameter variable + by its argument value, it may need to introduce explicit conversions + of each value to the original parameter type, such as `y := any(1)`. + +- **Last reference:** When an argument expression has no effects + and its corresponding parameter is never used, the expression + may be eliminated. However, if the expression contains the last + reference to a local variable at the caller, this may cause a compile + error because the variable is now unused! So the inliner must be + cautious about eliminating references to local variables. + +This is just a taste of the problem domain. If you're curious, the +documentation for [golang.org/x/tools/internal/refactor/inline](https://pkg.go.dev/golang.org/x/tools/internal/refactor/inline) has +more detail. All of this is to say, it's a complex problem, and we aim +for correctness first of all. We've already implemented a number of +important "tidiness optimizations" and we expect more to follow. + +Please give the inliner a try, and if you find any bugs (where the +transformation is incorrect), please do report them. We'd also like to +hear what "optimizations" you'd like to see next. From f85b3f7bcd04af51fc211dbce186ee007e0b5a66 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 12 Oct 2023 11:23:06 -0400 Subject: [PATCH 022/100] internal/refactor/inline: don't treat blanks as decls in declares() Plus, a test. Change-Id: I9d3f4729c1b8da51d771442d9c3f5909f608591e Reviewed-on: https://go-review.googlesource.com/c/tools/+/534895 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- internal/refactor/inline/inline.go | 1 + internal/refactor/inline/inline_test.go | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 618b179867b..b8418e53bdd 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -2622,6 +2622,7 @@ func declares(stmts []ast.Stmt) map[string]bool { } } } + delete(names, "_") return names } diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 189ac3f8861..3ab780e18bf 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -388,6 +388,17 @@ func TestBasics(t *testing.T) { `func _(ch chan int) { f(ch) }`, `func _(ch chan int) { <-(<-chan int)(ch) }`, }, + { + // (a regression test for unnecessary braces) + "In block elision, blank decls don't count when computing name conflicts.", + `func f(x int) { var _ = x; var _ = 3 }`, + `func _() { var _ = 1; f(2) }`, + `func _() { + var _ = 1 + var _ = 2 + var _ = 3 +}`, + }, }) } From 12b5dadef96835a9e447f2079a6cb949220c3856 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 13 Oct 2023 16:13:45 -0400 Subject: [PATCH 023/100] gopls: deprecate "tempModfile" and "expandWorkspaceToModule" settings These settings deeply affect gopls' behavior, and can't work well when set to a non-default value. More rationale is provided in the attached issues. The features controlled by these settings are not yet removed. For the v0.14.0 release, we will simply warn users who have them set. Fixes golang/go#57514 Fixes golang/go#61970 Change-Id: Ica631dcb8980eeeace9ceaec159218e6306831ae Reviewed-on: https://go-review.googlesource.com/c/tools/+/534919 LUCI-TryBot-Result: Go LUCI Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/source/options.go | 7 +++++++ gopls/internal/regtest/misc/configuration_test.go | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index ec544fc31b1..e57a3fd43c1 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -1181,6 +1181,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.VerboseWorkDoneProgress) case "tempModfile": + result.softErrorf("gopls setting \"tempModfile\" is deprecated.\nPlease comment on https://go.dev/issue/63537 if this impacts your workflow.") result.setBool(&o.TempModfile) case "showBugReports": @@ -1207,6 +1208,7 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) result.setBool(&o.NoSemanticNumber) case "expandWorkspaceToModule": + result.softErrorf("gopls setting \"expandWorkspaceToModule\" is deprecated.\nPlease comment on https://go.dev/issue/63536 if this impacts your workflow.") result.setBool(&o.ExpandWorkspaceToModule) case "experimentalPostfixCompletions": @@ -1356,6 +1358,11 @@ func (r *OptionResult) deprecated(replacement string) { r.Error = &SoftError{msg} } +// softErrorf reports a soft error related to the current option. +func (r *OptionResult) softErrorf(format string, args ...any) { + r.Error = &SoftError{fmt.Sprintf(format, args...)} +} + // unexpected reports that the current setting is not known to gopls. func (r *OptionResult) unexpected() { r.Error = fmt.Errorf("unexpected gopls setting %q", r.Name) diff --git a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go index 3f9c5b941c3..d74162e225b 100644 --- a/gopls/internal/regtest/misc/configuration_test.go +++ b/gopls/internal/regtest/misc/configuration_test.go @@ -141,6 +141,8 @@ func TestDeprecatedSettings(t *testing.T) { "experimentalUseInvalidMetadata": true, "experimentalWatchedFileDelay": "1s", "experimentalWorkspaceModule": true, + "tempModfile": true, + "expandWorkspaceToModule": false, }, ).Run(t, "", func(t *testing.T, env *Env) { env.OnceMet( @@ -148,6 +150,8 @@ func TestDeprecatedSettings(t *testing.T) { ShownMessage("experimentalWorkspaceModule"), ShownMessage("experimentalUseInvalidMetadata"), ShownMessage("experimentalWatchedFileDelay"), + ShownMessage("https://go.dev/issue/63537"), // issue to remove tempModfile + ShownMessage("https://go.dev/issue/63536"), // issue to remove expandWorkspaceToModule ) }) } From 918e96ac795f6d9de2d0b487826297420e899e3f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 14 Oct 2023 13:52:49 -0400 Subject: [PATCH 024/100] internal/refactor/inline: use binding decl with literalization Previously, literalization, the strategy of last resort, didn't make use of a binding decl even when one was available. But a binding decl can make literalization more readable as it puts the arguments before the body, their natural place, which is important especially when the body is longer. func(params) { body } (args) => func() { var params = args; body }() We don't yet attempt to do this if any named result is referenced, because the binding decl would duplicate it; teasing apart the params and results of the binding decl is left for future work. Plus tests. Change-Id: I51da3016157c1531c2d57962c4bddb0203ac0946 Reviewed-on: https://go-review.googlesource.com/c/tools/+/535456 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- internal/refactor/inline/doc.go | 7 --- internal/refactor/inline/inline.go | 43 ++++++++++++++----- internal/refactor/inline/inline_test.go | 20 ++++++++- .../inline/testdata/basic-literal.txtar | 2 +- .../refactor/inline/testdata/issue62667.txtar | 2 +- 5 files changed, 54 insertions(+), 20 deletions(-) diff --git a/internal/refactor/inline/doc.go b/internal/refactor/inline/doc.go index b13241f1ec6..6bb4cef055d 100644 --- a/internal/refactor/inline/doc.go +++ b/internal/refactor/inline/doc.go @@ -251,10 +251,6 @@ TODO(adonovan): future work: could be achieved by returning metadata alongside the result and having the client conditionally discard the change. - - Is it acceptable to skip effects that are limited to runtime - panics? Can we avoid evaluating an argument x.f - or a[i] when the corresponding parameter is unused? - - Support inlining of generic functions, replacing type parameters by their instantiations. @@ -262,9 +258,6 @@ TODO(adonovan): future work: But note that the existing algorithm makes widespread assumptions that the callee is a package-level function or method. - - Eliminate parens and braces inserted conservatively when they - are redundant. - - Eliminate explicit conversions of "untyped" literals inserted conservatively when they are redundant. For example, the conversion int32(1) is redundant when this value is used only as a diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index b8418e53bdd..53119e372ec 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -989,15 +989,32 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu } // Infallible general case: literalization. + // + // func(params) { body }(args) + // logf("strategy: literalization") + funcLit := &ast.FuncLit{ + Type: calleeDecl.Type, + Body: calleeDecl.Body, + } + + // Literalization can still make use of a binding + // decl as it gives a more natural reading order: + // + // func() { var params = args; body }() + // + // TODO(adonovan): relax the allResultsUnreferenced requirement + // by adding a parameter-only (no named results) binding decl. + if bindingDeclStmt != nil && allResultsUnreferenced { + funcLit.Type.Params.List = nil + remainingArgs = nil + funcLit.Body.List = prepend(bindingDeclStmt, funcLit.Body.List...) + } // Emit a new call to a function literal in place of // the callee name, with appropriate replacements. newCall := &ast.CallExpr{ - Fun: &ast.FuncLit{ - Type: calleeDecl.Type, - Body: calleeDecl.Body, - }, + Fun: funcLit, Ellipsis: token.NoPos, // f(slice...) is always simplified Args: remainingArgs, } @@ -1536,11 +1553,12 @@ func updateCalleeParams(calleeDecl *ast.FuncDecl, params []*parameter) { // createBindingDecl constructs a "binding decl" that implements // parameter assignment and declares any named result variables -// referenced by the callee. +// referenced by the callee. It returns nil if there were no +// unsubstituted parameters. // // It may not always be possible to create the decl (e.g. due to -// shadowing), in which case it returns nil; but if it succeeds, the -// declaration may be used by reduction strategies to relax the +// shadowing), in which case it also returns nil; but if it succeeds, +// the declaration may be used by reduction strategies to relax the // requirement that all parameters have been substituted. // // For example, a call: @@ -1682,14 +1700,19 @@ func createBindingDecl(logf func(string, ...any), caller *Caller, args []*argume } } - decl := &ast.DeclStmt{ + if len(specs) == 0 { + logf("binding decl not needed: all parameters substituted") + return nil + } + + stmt := &ast.DeclStmt{ Decl: &ast.GenDecl{ Tok: token.VAR, Specs: specs, }, } - logf("binding decl: %s", debugFormatNode(caller.Fset, decl)) - return decl + logf("binding decl: %s", debugFormatNode(caller.Fset, stmt)) + return stmt } // lookup does a symbol lookup in the lexical environment of the caller. diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 3ab780e18bf..3f797055d90 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -693,7 +693,7 @@ func TestVariadic(t *testing.T) { "Variadic cancellation (basic).", `func f(args ...any) { defer f(&args); println(args) }`, `func _(slice []any) { f(slice...) }`, - `func _(slice []any) { func(args []any) { defer f(&args); println(args) }(slice) }`, + `func _(slice []any) { func() { var args []any = slice; defer f(&args); println(args) }() }`, }, { "Variadic cancellation (literalization with parameter elimination).", @@ -797,6 +797,24 @@ func TestParameterBindingDecl(t *testing.T) { `func _(x *T) { f(x, recover()) }`, `func _(x *T) { x.g(recover()) }`, }, + { + "Literalization can make use of a binding decl (all params).", + `func f(x, y int) int { defer println(); return y + x }; func g(int) int`, + `func _() { println(f(g(1), g(2))) }`, + `func _() { println(func() int { var x, y int = g(1), g(2); defer println(); return y + x }()) }`, + }, + { + "Literalization can make use of a binding decl (some params).", + `func f(x, y int) int { z := y + x; defer println(); return z }; func g(int) int`, + `func _() { println(f(g(1), g(2))) }`, + `func _() { println(func() int { var x int = g(1); z := g(2) + x; defer println(); return z }()) }`, + }, + { + "Literalization can't yet use of a binding decl if named results.", + `func f(x, y int) (z int) { z = y + x; defer println(); return }; func g(int) int`, + `func _() { println(f(g(1), g(2))) }`, + `func _() { println(func(x int) (z int) { z = g(2) + x; defer println(); return }(g(1))) }`, + }, }) } diff --git a/internal/refactor/inline/testdata/basic-literal.txtar b/internal/refactor/inline/testdata/basic-literal.txtar index a74fbda42de..7ae640aad02 100644 --- a/internal/refactor/inline/testdata/basic-literal.txtar +++ b/internal/refactor/inline/testdata/basic-literal.txtar @@ -23,7 +23,7 @@ func add(x, y int) int { defer print(); return x + y } package a func _() { - func(x int) int { defer print(); return x + 2 }(recover().(int)) //@ inline(re"add", add1) + func() int { var x int = recover().(int); defer print(); return x + 2 }() //@ inline(re"add", add1) } func add(x, y int) int { defer print(); return x + y } diff --git a/internal/refactor/inline/testdata/issue62667.txtar b/internal/refactor/inline/testdata/issue62667.txtar index 21420e21df4..b6ff83b4bce 100644 --- a/internal/refactor/inline/testdata/issue62667.txtar +++ b/internal/refactor/inline/testdata/issue62667.txtar @@ -38,7 +38,7 @@ import ( ) func A() { - func(path string) { defer func() {}(); path0.Split(path) }(g()) //@ inline(re"Dir", result) + func() { var path string = g(); defer func() {}(); path0.Split(path) }() //@ inline(re"Dir", result) } func g() string \ No newline at end of file From 6fcd7783a125d5ab4b45f18ad6566d9f1489f748 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 3 Oct 2023 16:23:36 -0400 Subject: [PATCH 025/100] gopls/internal/lsp: add code actions to remove unused parameters Use the new inlining technology to implement a first change-signature refactoring, by rewriting the declaration to be a trivial wrapper around a declaration with modified signature, inlining the wrapper, and then performing string substitution to replace references to the synthetic delegate. This demonstrates the power of inlining, which can be seen as a more general tool for rewriting source code: express old code as new code, recognize instances of old code (in this case, calls), and inline. However, this CL was still rather difficult, primarily because of (1) the problem of manipulating syntax without breaking formatting and comments, and (2) the problem of composing multiple refactoring operations, which in general requires iterative type checking. Neither of those difficulties have general solutions: any form of nontrivial AST manipulation tends to result in an unacceptable movement of comments, and iterative type checking is difficult because it may involve an arbitrarily modified package graph, and because it is difficult to correlate references in the previous version of the package with references in the new version of the package. But in the case of removing a parameter, these problems are solvable: We can avoid most AST mangling by restricting the scope of rewriting to the function signature. We can type check the new package using the imports of the old package. We can find the next reference in the new package by counting. Fixes golang/go#63534 Change-Id: Iba5fa6b0da503b7723bea1b43fd2c4151576a672 Reviewed-on: https://go-review.googlesource.com/c/tools/+/532495 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley --- gopls/doc/commands.md | 20 + .../lsp/analysis/unusedparams/cmd/main.go | 13 + .../lsp/analysis/unusedparams/unusedparams.go | 30 +- gopls/internal/lsp/cache/check.go | 1 + gopls/internal/lsp/code_action.go | 58 +- gopls/internal/lsp/command.go | 22 + gopls/internal/lsp/command/command_gen.go | 20 + gopls/internal/lsp/command/interface.go | 11 + gopls/internal/lsp/regtest/marker.go | 5 +- gopls/internal/lsp/source/api_json.go | 6 + gopls/internal/lsp/source/change_signature.go | 572 ++++++++++++++++++ gopls/internal/lsp/source/inline.go | 15 +- gopls/internal/lsp/source/inline_all.go | 263 ++++++++ gopls/internal/lsp/source/util.go | 8 + gopls/internal/lsp/source/view.go | 52 +- .../testdata/codeaction/removeparam.txt | 264 ++++++++ .../codeaction/removeparam_formatting.txt | 55 ++ .../codeaction/removeparam_funcvalue.txt | 19 + .../codeaction/removeparam_imports.txt | 171 ++++++ internal/astutil/clone.go | 71 +++ internal/refactor/inline/inline.go | 65 +- 21 files changed, 1655 insertions(+), 86 deletions(-) create mode 100644 gopls/internal/lsp/analysis/unusedparams/cmd/main.go create mode 100644 gopls/internal/lsp/source/change_signature.go create mode 100644 gopls/internal/lsp/source/inline_all.go create mode 100644 gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt create mode 100644 gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt create mode 100644 gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt create mode 100644 gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt create mode 100644 internal/astutil/clone.go diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 833dad9a1d0..3404c91c7e5 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -84,6 +84,26 @@ Args: } ``` +### **performs a "change signature" refactoring.** +Identifier: `gopls.change_signature` + +This command is experimental, currently only supporting parameter removal. +Its signature will certainly change in the future (pun intended). + +Args: + +``` +{ + "RemoveParameter": { + "uri": string, + "range": { + "start": { ... }, + "end": { ... }, + }, + }, +} +``` + ### **Check for upgrades** Identifier: `gopls.check_upgrades` diff --git a/gopls/internal/lsp/analysis/unusedparams/cmd/main.go b/gopls/internal/lsp/analysis/unusedparams/cmd/main.go new file mode 100644 index 00000000000..fafb126ffdf --- /dev/null +++ b/gopls/internal/lsp/analysis/unusedparams/cmd/main.go @@ -0,0 +1,13 @@ +// 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 stringintconv command runs the stringintconv analyzer. +package main + +import ( + "golang.org/x/tools/go/analysis/singlechecker" + "golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams" +) + +func main() { singlechecker.Main(unusedparams.Analyzer) } diff --git a/gopls/internal/lsp/analysis/unusedparams/unusedparams.go b/gopls/internal/lsp/analysis/unusedparams/unusedparams.go index e0ef5ef8dfb..64702b2f0a6 100644 --- a/gopls/internal/lsp/analysis/unusedparams/unusedparams.go +++ b/gopls/internal/lsp/analysis/unusedparams/unusedparams.go @@ -28,11 +28,20 @@ To reduce false positives it ignores: - functions in test files - functions with empty bodies or those with just a return stmt` -var Analyzer = &analysis.Analyzer{ - Name: "unusedparams", - Doc: Doc, - Requires: []*analysis.Analyzer{inspect.Analyzer}, - Run: run, +var ( + Analyzer = &analysis.Analyzer{ + Name: "unusedparams", + Doc: Doc, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: run, + } + inspectLits bool + inspectWrappers bool +) + +func init() { + Analyzer.Flags.BoolVar(&inspectLits, "lits", true, "inspect function literals") + Analyzer.Flags.BoolVar(&inspectWrappers, "wrappers", false, "inspect functions whose body consists of a single return statement") } type paramData struct { @@ -45,7 +54,9 @@ func run(pass *analysis.Pass) (interface{}, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) nodeFilter := []ast.Node{ (*ast.FuncDecl)(nil), - (*ast.FuncLit)(nil), + } + if inspectLits { + nodeFilter = append(nodeFilter, (*ast.FuncLit)(nil)) } inspect.Preorder(nodeFilter, func(n ast.Node) { @@ -62,6 +73,7 @@ func run(pass *analysis.Pass) (interface{}, error) { if f.Recv != nil { return } + // Ignore functions in _test.go files to reduce false positives. if file := pass.Fset.File(n.Pos()); file != nil && strings.HasSuffix(file.Name(), "_test.go") { return @@ -76,8 +88,10 @@ func run(pass *analysis.Pass) (interface{}, error) { switch expr := body.List[0].(type) { case *ast.ReturnStmt: - // Ignore functions that only contain a return statement to reduce false positives. - return + if !inspectWrappers { + // Ignore functions that only contain a return statement to reduce false positives. + return + } case *ast.ExprStmt: callExpr, ok := expr.X.(*ast.CallExpr) if !ok || len(body.List) > 1 { diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index e0be99b64f5..a95c439cfa7 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -1487,6 +1487,7 @@ func typeCheckImpl(ctx context.Context, b *typeCheckBatch, inputs typeCheckInput return pkg, nil } +// TODO(golang/go#63472): this looks wrong with the new Go version syntax. var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) func doTypeCheck(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs) (*syntaxPackage, error) { diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 555131ea796..d7567488790 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -430,6 +430,26 @@ func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.P rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r) } }() + + var actions []protocol.CodeAction + + if canRemoveParameter(pkg, pgf, rng) { + cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{ + RemoveParameter: protocol.Location{ + URI: protocol.URIFromSpanURI(pgf.URI), + Range: rng, + }, + }) + if err != nil { + return nil, err + } + actions = append(actions, protocol.CodeAction{ + Title: "Refactor: remove unused parameter", + Kind: protocol.RefactorRewrite, + Command: &cmd, + }) + } + start, end, err := pgf.RangePos(rng) if err != nil { return nil, err @@ -471,7 +491,6 @@ func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.P } } - var actions []protocol.CodeAction for i := range commands { actions = append(actions, protocol.CodeAction{ Title: commands[i].Title, @@ -510,6 +529,43 @@ func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.P return actions, nil } +// canRemoveParameter reports whether we can remove the function parameter +// indicated by the given [start, end) range. +// +// This is true if: +// - [start, end) is contained within an unused field or parameter name +// - ... of a non-method function declaration. +func canRemoveParameter(pkg source.Package, pgf *source.ParsedGoFile, rng protocol.Range) bool { + info := source.FindParam(pgf, rng) + if info.Decl == nil || info.Field == nil { + return false + } + + if len(info.Field.Names) == 0 { + return true // no names => field is unused + } + if info.Name == nil { + return false // no name is indicated + } + if info.Name.Name == "_" { + return true // trivially unused + } + + obj := pkg.GetTypesInfo().Defs[info.Name] + if obj == nil { + return false // something went wrong + } + + used := false + ast.Inspect(info.Decl.Body, func(node ast.Node) bool { + if n, ok := node.(*ast.Ident); ok && pkg.GetTypesInfo().Uses[n] == obj { + used = true + } + return !used // keep going until we find a use + }) + return !used +} + // refactorInline returns inline actions available at the specified range. func refactorInline(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, fh source.FileHandle, rng protocol.Range) ([]protocol.CodeAction, error) { var commands []protocol.Command diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index f4d4a9e4ba2..8fe1ec82391 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -1210,3 +1210,25 @@ func showDocumentImpl(ctx context.Context, cli protocol.Client, url protocol.URI event.Log(ctx, fmt.Sprintf("client declined to open document %v", url)) } } + +func (c *commandHandler) ChangeSignature(ctx context.Context, args command.ChangeSignatureArgs) error { + return c.run(ctx, commandConfig{ + forURI: args.RemoveParameter.URI, + }, func(ctx context.Context, deps commandDeps) error { + // For now, gopls only supports removing unused parameters. + changes, err := source.RemoveUnusedParameter(ctx, deps.fh, args.RemoveParameter.Range, deps.snapshot) + if err != nil { + return err + } + r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ + Edit: protocol.WorkspaceEdit{ + DocumentChanges: changes, + }, + }) + if !r.Applied { + return fmt.Errorf("failed to apply edits: %v", r.FailureReason) + } + + return nil + }) +} diff --git a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go index 5dd2a9dd452..e54030ceea9 100644 --- a/gopls/internal/lsp/command/command_gen.go +++ b/gopls/internal/lsp/command/command_gen.go @@ -23,6 +23,7 @@ const ( AddImport Command = "add_import" AddTelemetryCounters Command = "add_telemetry_counters" ApplyFix Command = "apply_fix" + ChangeSignature Command = "change_signature" CheckUpgrades Command = "check_upgrades" EditGoDirective Command = "edit_go_directive" FetchVulncheckResult Command = "fetch_vulncheck_result" @@ -56,6 +57,7 @@ var Commands = []Command{ AddImport, AddTelemetryCounters, ApplyFix, + ChangeSignature, CheckUpgrades, EditGoDirective, FetchVulncheckResult, @@ -110,6 +112,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return nil, s.ApplyFix(ctx, a0) + case "gopls.change_signature": + var a0 ChangeSignatureArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return nil, s.ChangeSignature(ctx, a0) case "gopls.check_upgrades": var a0 CheckUpgradesArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -308,6 +316,18 @@ func NewApplyFixCommand(title string, a0 ApplyFixArgs) (protocol.Command, error) }, nil } +func NewChangeSignatureCommand(title string, a0 ChangeSignatureArgs) (protocol.Command, error) { + args, err := MarshalArgs(a0) + if err != nil { + return protocol.Command{}, err + } + return protocol.Command{ + Title: title, + Command: "gopls.change_signature", + Arguments: args, + }, nil +} + func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Command, error) { args, err := MarshalArgs(a0) if err != nil { diff --git a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go index c3bd921fcf1..066f16f790f 100644 --- a/gopls/internal/lsp/command/interface.go +++ b/gopls/internal/lsp/command/interface.go @@ -201,6 +201,12 @@ type Interface interface { // the user to ask if they want to enable Go telemetry uploading. If the user // responds 'Yes', the telemetry mode is set to "on". MaybePromptForTelemetry(context.Context) error + + // ChangeSignature: performs a "change signature" refactoring. + // + // This command is experimental, currently only supporting parameter removal. + // Its signature will certainly change in the future (pun intended). + ChangeSignature(context.Context, ChangeSignatureArgs) error } type RunTestsArgs struct { @@ -519,3 +525,8 @@ type AddTelemetryCountersArgs struct { Names []string // Name of counters. Values []int64 // Values added to the corresponding counters. Must be non-negative. } + +// ChangeSignatureArgs specifies a "change signature" refactoring to perform. +type ChangeSignatureArgs struct { + RemoveParameter protocol.Location +} diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index f6c1d4c6077..928f77dc40d 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -1001,6 +1001,9 @@ func formatTest(test *markerTest) ([]byte, error) { // ...followed by any new golden files. var newGoldenFiles []txtar.File for filename, data := range updatedGolden { + // TODO(rfindley): it looks like this implicitly removes trailing newlines + // from golden content. Is there any way to fix that? Perhaps we should + // just make the diff tolerant of missing newlines? newGoldenFiles = append(newGoldenFiles, txtar.File{Name: filename, Data: data}) } // Sort new golden files lexically. @@ -2047,7 +2050,7 @@ func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKi Command: action.Command.Command, Arguments: action.Command.Arguments, }); err != nil { - env.T.Fatalf("error converting command %q to edits: %v", action.Command.Command, err) + return nil, err } if err := applyDocumentChanges(env, env.Awaiter.takeDocumentChanges(), fileChanges); err != nil { diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 0fd6b07c54b..0ad9f1d3c53 100644 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -733,6 +733,12 @@ var GeneratedAPIJSON = &APIJSON{ Doc: "Applies a fix to a region of source code.", ArgDoc: "{\n\t// The fix to apply.\n\t\"Fix\": string,\n\t// The file URI for the document to fix.\n\t\"URI\": string,\n\t// The document range to scan for fixes.\n\t\"Range\": {\n\t\t\"start\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t},\n}", }, + { + Command: "gopls.change_signature", + Title: "performs a \"change signature\" refactoring.", + Doc: "This command is experimental, currently only supporting parameter removal.\nIts signature will certainly change in the future (pun intended).", + ArgDoc: "{\n\t\"RemoveParameter\": {\n\t\t\"uri\": string,\n\t\t\"range\": {\n\t\t\t\"start\": { ... },\n\t\t\t\"end\": { ... },\n\t\t},\n\t},\n}", + }, { Command: "gopls.check_upgrades", Title: "Check for upgrades", diff --git a/gopls/internal/lsp/source/change_signature.go b/gopls/internal/lsp/source/change_signature.go new file mode 100644 index 00000000000..8dfd0135950 --- /dev/null +++ b/gopls/internal/lsp/source/change_signature.go @@ -0,0 +1,572 @@ +// 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 source + +import ( + "bytes" + "context" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "go/types" + "regexp" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/lsp/safetoken" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/imports" + internalastutil "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/refactor/inline" + "golang.org/x/tools/internal/tokeninternal" + "golang.org/x/tools/internal/typesinternal" +) + +// RemoveUnusedParameter computes a refactoring to remove the parameter +// indicated by the given range, which must be contained within an unused +// parameter name or field. +// +// This operation is a work in progress. Remaining TODO: +// - Handle function assignment correctly. +// - Improve the extra newlines in output. +// - Stream type checking via ForEachPackage. +// - Avoid unnecessary additional type checking. +func RemoveUnusedParameter(ctx context.Context, fh FileHandle, rng protocol.Range, snapshot Snapshot) ([]protocol.DocumentChanges, error) { + pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) + if err != nil { + return nil, err + } + if perrors, terrors := pkg.GetParseErrors(), pkg.GetTypeErrors(); len(perrors) > 0 || len(terrors) > 0 { + var sample string + if len(perrors) > 0 { + sample = perrors[0].Error() + } else { + sample = terrors[0].Error() + } + return nil, fmt.Errorf("can't change signatures for packages with parse or type errors: (e.g. %s)", sample) + } + + info := FindParam(pgf, rng) + if info.Decl == nil { + return nil, fmt.Errorf("failed to find declaration") + } + if info.Decl.Recv != nil { + return nil, fmt.Errorf("can't change signature of methods (yet)") + } + if info.Field == nil { + return nil, fmt.Errorf("failed to find field") + } + + // Create the new declaration, which is a copy of the original decl with the + // unnecessary parameter removed. + newDecl := internalastutil.CloneNode(info.Decl) + if info.Name != nil { + names := remove(newDecl.Type.Params.List[info.FieldIndex].Names, info.NameIndex) + newDecl.Type.Params.List[info.FieldIndex].Names = names + } + if len(newDecl.Type.Params.List[info.FieldIndex].Names) == 0 { + // Unnamed, or final name was removed: in either case, remove the field. + newDecl.Type.Params.List = remove(newDecl.Type.Params.List, info.FieldIndex) + } + + // Compute inputs into building a wrapper function around the modified + // signature. + var ( + params = internalastutil.CloneNode(info.Decl.Type.Params) // "_" names will be modified + args []ast.Expr // arguments to delegate + variadic = false // whether the signature is variadic + ) + { + allNames := make(map[string]bool) // for renaming blanks + for _, fld := range params.List { + for _, n := range fld.Names { + if n.Name != "_" { + allNames[n.Name] = true + } + } + } + blanks := 0 + for i, fld := range params.List { + for j, n := range fld.Names { + if i == info.FieldIndex && j == info.NameIndex { + continue + } + if n.Name == "_" { + // Create names for blank (_) parameters so the delegating wrapper + // can refer to them. + for { + newName := fmt.Sprintf("blank%d", blanks) + blanks++ + if !allNames[newName] { + n.Name = newName + break + } + } + } + args = append(args, &ast.Ident{Name: n.Name}) + if i == len(params.List)-1 { + _, variadic = fld.Type.(*ast.Ellipsis) + } + } + } + } + + // Rewrite all referring calls. + newContent, err := rewriteCalls(ctx, signatureRewrite{ + snapshot: snapshot, + pkg: pkg, + pgf: pgf, + origDecl: info.Decl, + newDecl: newDecl, + params: params, + callArgs: args, + variadic: variadic, + }) + if err != nil { + return nil, err + } + // Finally, rewrite the original declaration. We do this after inlining all + // calls, as there may be calls in the same file as the declaration. But none + // of the inlining should have changed the location of the original + // declaration. + { + idx := findDecl(pgf.File, info.Decl) + if idx < 0 { + return nil, bug.Errorf("didn't find original decl") + } + + src, ok := newContent[pgf.URI] + if !ok { + src = pgf.Src + } + fset := tokeninternal.FileSetFor(pgf.Tok) + src, err = rewriteSignature(fset, idx, src, newDecl) + newContent[pgf.URI] = src + } + + // Translate the resulting state into document changes. + var changes []protocol.DocumentChanges + for uri, after := range newContent { + fh, err := snapshot.ReadFile(ctx, uri) + if err != nil { + return nil, err + } + before, err := fh.Content() + if err != nil { + return nil, err + } + edits := diff.Bytes(before, after) + mapper := protocol.NewMapper(uri, before) + pedits, err := ToProtocolEdits(mapper, edits) + if err != nil { + return nil, fmt.Errorf("computing edits for %s: %v", uri, err) + } + changes = append(changes, protocol.DocumentChanges{ + TextDocumentEdit: &protocol.TextDocumentEdit{ + TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ + Version: fh.Version(), + TextDocumentIdentifier: protocol.TextDocumentIdentifier{URI: protocol.URIFromSpanURI(uri)}, + }, + Edits: pedits, + }, + }) + } + return changes, nil +} + +// rewriteSignature rewrites the signature of the declIdx'th declaration in src +// to use the signature of newDecl (described by fset). +// +// TODO(rfindley): I think this operation could be generalized, for example by +// using a concept of a 'nodepath' to correlate nodes between two related +// files. +// +// Note that with its current application, rewriteSignature is expected to +// succeed. Separate bug.Errorf calls are used below (rather than one call at +// the callsite) in order to have greater precision. +func rewriteSignature(fset *token.FileSet, declIdx int, src0 []byte, newDecl *ast.FuncDecl) ([]byte, error) { + // Parse the new file0 content, to locate the original params. + file0, err := parser.ParseFile(fset, "", src0, parser.ParseComments|parser.SkipObjectResolution) + if err != nil { + return nil, bug.Errorf("re-parsing declaring file failed: %v", err) + } + decl0, _ := file0.Decls[declIdx].(*ast.FuncDecl) + // Inlining shouldn't have changed the location of any declarations, but do + // a sanity check. + if decl0 == nil || decl0.Name.Name != newDecl.Name.Name { + return nil, bug.Errorf("inlining affected declaration order: found %v, not func %s", decl0, newDecl.Name.Name) + } + opening0, closing0, err := safetoken.Offsets(fset.File(decl0.Pos()), decl0.Type.Params.Opening, decl0.Type.Params.Closing) + if err != nil { + return nil, bug.Errorf("can't find params: %v", err) + } + + // Format the modified signature and apply a textual replacement. This + // minimizes comment disruption. + formattedType := FormatNode(fset, newDecl.Type) + expr, err := parser.ParseExprFrom(fset, "", []byte(formattedType), 0) + if err != nil { + return nil, bug.Errorf("parsing modified signature: %v", err) + } + newType := expr.(*ast.FuncType) + opening1, closing1, err := safetoken.Offsets(fset.File(newType.Pos()), newType.Params.Opening, newType.Params.Closing) + if err != nil { + return nil, bug.Errorf("param offsets: %v", err) + } + newParams := formattedType[opening1 : closing1+1] + + // Splice. + var buf bytes.Buffer + buf.Write(src0[:opening0]) + buf.WriteString(newParams) + buf.Write(src0[closing0+1:]) + newSrc := buf.Bytes() + if len(file0.Imports) > 0 { + formatted, err := imports.Process("output", newSrc, nil) + if err != nil { + return nil, bug.Errorf("imports.Process failed: %v", err) + } + newSrc = formatted + } + return newSrc, nil +} + +// ParamInfo records information about a param identified by a position. +type ParamInfo struct { + Decl *ast.FuncDecl // enclosing func decl, or nil + FieldIndex int // index of Field in Decl.Type.Params, or -1 + Field *ast.Field // enclosing field of Decl, or nil + NameIndex int // index of Name in Field.Names, or nil + Name *ast.Ident // indicated name (either enclosing, or Field.Names[0] if len(Field.Names) == 1) +} + +// FindParam finds the parameter information spanned by the given range. +func FindParam(pgf *ParsedGoFile, rng protocol.Range) ParamInfo { + info := ParamInfo{FieldIndex: -1, NameIndex: -1} + start, end, err := pgf.RangePos(rng) + if err != nil { + bug.Reportf("(file=%v).RangePos(%v) failed: %v", pgf.URI, rng, err) + return info + } + + path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) + var ( + id *ast.Ident + field *ast.Field + decl *ast.FuncDecl + ) + // Find the outermost enclosing node of each kind, whether or not they match + // the semantics described in the docstring. + for _, n := range path { + switch n := n.(type) { + case *ast.Ident: + id = n + case *ast.Field: + field = n + case *ast.FuncDecl: + decl = n + } + } + // Check the conditions described in the docstring. + if decl == nil { + return info + } + info.Decl = decl + for fi, f := range decl.Type.Params.List { + if f == field { + info.FieldIndex = fi + info.Field = f + for ni, n := range f.Names { + if n == id { + info.NameIndex = ni + info.Name = n + break + } + } + if info.Name == nil && len(info.Field.Names) == 1 { + info.NameIndex = 0 + info.Name = info.Field.Names[0] + } + break + } + } + return info +} + +// signatureRewrite defines a rewritten function signature. +// +// See rewriteCalls for more details. +type signatureRewrite struct { + snapshot Snapshot + pkg Package + pgf *ParsedGoFile + origDecl, newDecl *ast.FuncDecl + params *ast.FieldList + callArgs []ast.Expr + variadic bool +} + +// rewriteCalls returns the document changes required to rewrite the +// signature of origDecl to that of newDecl. +// +// This is a rather complicated factoring of the rewrite operation, but is able +// to describe arbitrary rewrites. Specifically, rewriteCalls creates a +// synthetic copy of pkg, where the original function declaration is changed to +// be a trivial wrapper around the new declaration. params and callArgs are +// used to perform this delegation: params must have the same type as origDecl, +// but may have renamed parameters (such as is required for delegating blank +// parameters). callArgs are the arguments of the delegated call (i.e. using +// params). +// +// For example, consider removing the unused 'b' parameter below, rewriting +// +// func Foo(a, b, c, _ int) int { +// return a+c +// } +// +// To +// +// func Foo(a, c, _ int) int { +// return a+c +// } +// +// In this case, rewriteCalls is parameterized as follows: +// - origDecl is the original declaration +// - newDecl is the new declaration, which is a copy of origDecl less the 'b' +// parameter. +// - params is a new parameter list (a, b, c, blank0 int) to be used for the +// new wrapper. +// - callArgs is the argument list (a, c, blank0), to be used to call the new +// delegate. +// +// rewriting is expressed this way so that rewriteCalls can own the details +// of *how* this rewriting is performed. For example, as of writing it names +// the synthetic delegate G_o_p_l_s_foo, but the caller need not know this. +// +// By passing an entirely new declaration, rewriteCalls may be used for +// signature refactorings that may affect the function body, such as removing +// or adding return values. +func rewriteCalls(ctx context.Context, rw signatureRewrite) (map[span.URI][]byte, error) { + // tag is a unique prefix that is added to the delegated declaration. + // + // It must have a ~0% probability of causing collisions with existing names. + const tag = "G_o_p_l_s_" + + var ( + modifiedSrc []byte + modifiedFile *ast.File + modifiedDecl *ast.FuncDecl + ) + { + delegate := internalastutil.CloneNode(rw.newDecl) // clone before modifying + delegate.Name.Name = tag + delegate.Name.Name + if obj := rw.pkg.GetTypes().Scope().Lookup(delegate.Name.Name); obj != nil { + return nil, fmt.Errorf("synthetic name %q conflicts with an existing declaration", delegate.Name.Name) + } + + wrapper := internalastutil.CloneNode(rw.origDecl) + wrapper.Type.Params = rw.params + call := &ast.CallExpr{ + Fun: &ast.Ident{Name: delegate.Name.Name}, + Args: rw.callArgs, + } + if rw.variadic { + call.Ellipsis = 1 // must not be token.NoPos + } + + var stmt ast.Stmt + if delegate.Type.Results.NumFields() > 0 { + stmt = &ast.ReturnStmt{ + Results: []ast.Expr{call}, + } + } else { + stmt = &ast.ExprStmt{ + X: call, + } + } + wrapper.Body = &ast.BlockStmt{ + List: []ast.Stmt{stmt}, + } + + fset := tokeninternal.FileSetFor(rw.pgf.Tok) + var err error + modifiedSrc, err = replaceFileDecl(rw.pgf, rw.origDecl, delegate) + if err != nil { + return nil, err + } + // TODO(rfindley): we can probably get away with one fewer parse operations + // by returning the modified AST from replaceDecl. Investigate if that is + // accurate. + modifiedSrc = append(modifiedSrc, []byte("\n\n"+FormatNode(fset, wrapper))...) + modifiedFile, err = parser.ParseFile(rw.pkg.FileSet(), rw.pgf.URI.Filename(), modifiedSrc, parser.ParseComments|parser.SkipObjectResolution) + if err != nil { + return nil, err + } + modifiedDecl = modifiedFile.Decls[len(modifiedFile.Decls)-1].(*ast.FuncDecl) + } + + // Type check pkg again with the modified file, to compute the synthetic + // callee. + logf := logger(ctx, "change signature", rw.snapshot.Options().VerboseOutput) + pkg2, info, err := reTypeCheck(logf, rw.pkg, map[span.URI]*ast.File{rw.pgf.URI: modifiedFile}, false) + if err != nil { + return nil, err + } + calleeInfo, err := inline.AnalyzeCallee(logf, rw.pkg.FileSet(), pkg2, info, modifiedDecl, modifiedSrc) + if err != nil { + return nil, fmt.Errorf("analyzing callee: %v", err) + } + + post := func(got []byte) []byte { return bytes.ReplaceAll(got, []byte(tag), nil) } + return inlineAllCalls(ctx, logf, rw.snapshot, rw.pkg, rw.pgf, rw.origDecl, calleeInfo, post) +} + +// reTypeCheck re-type checks orig with new file contents defined by fileMask. +// +// It expects that any newly added imports are already present in the +// transitive imports of orig. +// +// If expectErrors is true, reTypeCheck allows errors in the new package. +// TODO(rfindley): perhaps this should be a filter to specify which errors are +// acceptable. +func reTypeCheck(logf func(string, ...any), orig Package, fileMask map[span.URI]*ast.File, expectErrors bool) (*types.Package, *types.Info, error) { + pkg := types.NewPackage(string(orig.Metadata().PkgPath), string(orig.Metadata().Name)) + info := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + Instances: make(map[*ast.Ident]types.Instance), + } + { + var files []*ast.File + for _, pgf := range orig.CompiledGoFiles() { + if mask, ok := fileMask[pgf.URI]; ok { + files = append(files, mask) + } else { + files = append(files, pgf.File) + } + } + + // Implement a BFS for imports in the transitive package graph. + // + // Note that this only works if any newly added imports are expected to be + // present among transitive imports. In general we cannot assume this to + // be the case, but in the special case of removing a parameter it works + // because any parameter types must be present in export data. + var importer func(importPath string) (*types.Package, error) + { + var ( + importsByPath = make(map[string]*types.Package) // cached imports + toSearch = []*types.Package{orig.GetTypes()} // packages to search + searched = make(map[string]bool) // path -> (false, if present in toSearch; true, if already searched) + ) + importer = func(path string) (*types.Package, error) { + if p, ok := importsByPath[path]; ok { + return p, nil + } + for len(toSearch) > 0 { + pkg := toSearch[0] + toSearch = toSearch[1:] + searched[pkg.Path()] = true + for _, p := range pkg.Imports() { + // TODO(rfindley): this is incorrect: p.Path() is a package path, + // whereas path is an import path. We can fix this by reporting any + // newly added imports from inlining, or by using the ImporterFrom + // interface and package metadata. + // + // TODO(rfindley): can't the inliner also be wrong here? It's + // possible that an import path means different things depending on + // the location. + importsByPath[p.Path()] = p + if _, ok := searched[p.Path()]; !ok { + searched[p.Path()] = false + toSearch = append(toSearch, p) + } + } + if p, ok := importsByPath[path]; ok { + return p, nil + } + } + return nil, fmt.Errorf("missing import") + } + } + cfg := &types.Config{ + Sizes: orig.Metadata().TypesSizes, + Importer: ImporterFunc(importer), + } + + // Copied from cache/check.go. + // TODO(rfindley): factor this out and fix goVersionRx. + // Set Go dialect. + if module := orig.Metadata().Module; module != nil && module.GoVersion != "" { + goVersion := "go" + module.GoVersion + // types.NewChecker panics if GoVersion is invalid. + // An unparsable mod file should probably stop us + // before we get here, but double check just in case. + if goVersionRx.MatchString(goVersion) { + typesinternal.SetGoVersion(cfg, goVersion) + } + } + if expectErrors { + cfg.Error = func(err error) { + logf("re-type checking: expected error: %v", err) + } + } + typesinternal.SetUsesCgo(cfg) + checker := types.NewChecker(cfg, orig.FileSet(), pkg, info) + if err := checker.Files(files); err != nil && !expectErrors { + return nil, nil, fmt.Errorf("type checking rewritten package: %v", err) + } + } + return pkg, info, nil +} + +// TODO(golang/go#63472): this looks wrong with the new Go version syntax. +var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) + +func remove[T any](s []T, i int) []T { + return append(s[:i], s[i+1:]...) +} + +// 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) { + i := findDecl(pgf.File, old) + if i == -1 { + return nil, bug.Errorf("didn't find old declaration") + } + start, end, err := safetoken.Offsets(pgf.Tok, old.Pos(), old.End()) + if err != nil { + return nil, err + } + var out bytes.Buffer + out.Write(pgf.Src[:start]) + fset := tokeninternal.FileSetFor(pgf.Tok) + if err := format.Node(&out, fset, new); err != nil { + return nil, bug.Errorf("formatting new node: %v", err) + } + out.Write(pgf.Src[end:]) + return out.Bytes(), nil +} + +// findDecl finds the index of decl in file.Decls. +// +// TODO: use slices.Index when it is available. +func findDecl(file *ast.File, decl ast.Decl) int { + for i, d := range file.Decls { + if d == decl { + return i + } + } + return -1 +} diff --git a/gopls/internal/lsp/source/inline.go b/gopls/internal/lsp/source/inline.go index 4e6e16f9159..da3e8e5ae0c 100644 --- a/gopls/internal/lsp/source/inline.go +++ b/gopls/internal/lsp/source/inline.go @@ -106,9 +106,7 @@ func inlineCall(ctx context.Context, snapshot Snapshot, fh FileHandle, rng proto // Users can consult the gopls event log to see // why a particular inlining strategy was chosen. - logf := func(format string, args ...any) { - event.Log(ctx, "inliner: "+fmt.Sprintf(format, args...)) - } + logf := logger(ctx, "inliner", snapshot.Options().VerboseOutput) callee, err := inline.AnalyzeCallee(logf, calleePkg.FileSet(), calleePkg.GetTypes(), calleePkg.GetTypesInfo(), calleeDecl, calleePGF.Src) if err != nil { @@ -136,3 +134,14 @@ func inlineCall(ctx context.Context, snapshot Snapshot, fh FileHandle, rng proto TextEdits: diffToTextEdits(callerPGF.Tok, diff.Bytes(callerPGF.Src, got)), }, nil } + +// TODO(adonovan): change the inliner to instead accept an io.Writer. +func logger(ctx context.Context, name string, verbose bool) func(format string, args ...any) { + if verbose { + return func(format string, args ...any) { + event.Log(ctx, name+": "+fmt.Sprintf(format, args...)) + } + } else { + return func(string, ...any) {} + } +} diff --git a/gopls/internal/lsp/source/inline_all.go b/gopls/internal/lsp/source/inline_all.go new file mode 100644 index 00000000000..d814a115d17 --- /dev/null +++ b/gopls/internal/lsp/source/inline_all.go @@ -0,0 +1,263 @@ +// 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 source + +import ( + "context" + "fmt" + "go/ast" + "go/parser" + + "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/gopls/internal/bug" + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/span" + "golang.org/x/tools/internal/refactor/inline" +) + +// inlineAllCalls inlines all calls to the original function declaration +// described by callee, returning the resulting modified file content. +// +// inlining everything is currently an expensive operation: it involves re-type +// checking every package that contains a potential call, as reported by +// References. In cases where there are multiple calls per file, inlineAllCalls +// must type check repeatedly for each additional call. +// +// The provided post processing function is applied to the resulting source +// after each transformation. This is necessary because we are using this +// function to inline synthetic wrappers for the purpose of signature +// rewriting. The delegated function has a fake name that doesn't exist in the +// snapshot, and so we can't re-type check until we replace this fake name. +// +// TODO(rfindley): this only works because removing a parameter is a very +// narrow operation. A better solution would be to allow for ad-hoc snapshots +// that expose the full machinery of real snapshots: minimal invalidation, +// batched type checking, etc. Then we could actually rewrite the declaring +// package in this snapshot (and so 'post' would not be necessary), and could +// robustly re-type check for the purpose of iterative inlining, even if the +// inlined code pulls in new imports that weren't present in export data. +// +// 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 Snapshot, pkg Package, pgf *ParsedGoFile, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[span.URI][]byte, error) { + // Collect references. + var refs []protocol.Location + { + funcPos, err := pgf.Mapper.PosPosition(pgf.Tok, origDecl.Name.NamePos) + if err != nil { + return nil, err + } + fh, err := snapshot.ReadFile(ctx, pgf.URI) + if err != nil { + return nil, err + } + refs, err = References(ctx, snapshot, fh, funcPos, false) + if err != nil { + return nil, fmt.Errorf("finding references to rewrite: %v", err) + } + } + + // Type-check the narrowest package containing each reference. + // TODO(rfindley): we should expose forEachPackage in order to operate in + // parallel and to reduce peak memory for this operation. + var ( + pkgForRef = make(map[protocol.Location]PackageID) + pkgs = make(map[PackageID]Package) + ) + { + needPkgs := make(map[PackageID]struct{}) + for _, ref := range refs { + md, err := NarrowestMetadataForFile(ctx, snapshot, ref.URI.SpanURI()) + if err != nil { + return nil, fmt.Errorf("finding ref metadata: %v", err) + } + pkgForRef[ref] = md.ID + needPkgs[md.ID] = struct{}{} + } + var pkgIDs []PackageID + for id := range needPkgs { // TODO: use maps.Keys once it is available to us + pkgIDs = append(pkgIDs, id) + } + + refPkgs, err := snapshot.TypeCheck(ctx, pkgIDs...) + if err != nil { + return nil, fmt.Errorf("type checking reference packages: %v", err) + } + + for _, p := range refPkgs { + pkgs[p.Metadata().ID] = p + } + } + + // Organize calls by top file declaration. Calls within a single file may + // affect each other, as the inlining edit may affect the surrounding scope + // or imports Therefore, when inlining subsequent calls in the same + // declaration, we must re-type check. + + type fileCalls struct { + pkg Package + pgf *ParsedGoFile + calls []*ast.CallExpr + } + + refsByFile := make(map[span.URI]*fileCalls) + for _, ref := range refs { + refpkg := pkgs[pkgForRef[ref]] + pgf, err := refpkg.File(ref.URI.SpanURI()) + if err != nil { + return nil, bug.Errorf("finding %s in %s: %v", ref.URI, refpkg.Metadata().ID, err) + } + start, end, err := pgf.RangePos(ref.Range) + if err != nil { + return nil, bug.Errorf("RangePos(ref): %v", err) + } + + // Look for the surrounding call expression. + var ( + name *ast.Ident + call *ast.CallExpr + ) + path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) + name, _ = path[0].(*ast.Ident) + if _, ok := path[1].(*ast.SelectorExpr); ok { + call, _ = path[2].(*ast.CallExpr) + } else { + call, _ = path[1].(*ast.CallExpr) + } + if name == nil || call == nil { + // TODO(rfindley): handle this case with eta-abstraction: + // a reference to the target function f in a non-call position + // use(f) + // is replaced by + // use(func(...) { f(...) }) + return nil, fmt.Errorf("cannot inline: found non-call function reference %v", ref) + } + // Sanity check. + if obj := refpkg.GetTypesInfo().ObjectOf(name); obj == nil || + obj.Name() != origDecl.Name.Name || + obj.Pkg() == nil || + obj.Pkg().Path() != string(pkg.Metadata().PkgPath) { + return nil, bug.Errorf("cannot inline: corrupted reference %v", ref) + } + + callInfo, ok := refsByFile[ref.URI.SpanURI()] + if !ok { + callInfo = &fileCalls{ + pkg: refpkg, + pgf: pgf, + } + refsByFile[ref.URI.SpanURI()] = callInfo + } + callInfo.calls = append(callInfo.calls, call) + } + + // Inline each call within the same decl in sequence, re-typechecking after + // each one. If there is only a single call within the decl, we can avoid + // additional type checking. + // + // Assumption: inlining does not affect the package scope, so we can operate + // on separate files independently. + result := make(map[span.URI][]byte) + for uri, callInfo := range refsByFile { + var ( + calls = callInfo.calls + fset = callInfo.pkg.FileSet() + tpkg = callInfo.pkg.GetTypes() + tinfo = callInfo.pkg.GetTypesInfo() + file = callInfo.pgf.File + content = callInfo.pgf.Src + ) + currentCall := 0 + for currentCall < len(calls) { + caller := &inline.Caller{ + Fset: fset, + Types: tpkg, + Info: tinfo, + File: file, + Call: calls[currentCall], + Content: content, + } + var err error + content, err = inline.Inline(logf, caller, callee) + if err != nil { + return nil, fmt.Errorf("inlining failed: %v", err) + } + if post != nil { + content = post(content) + } + if len(calls) <= 1 { + // No need to re-type check, as we've inlined all calls. + break + } + + // TODO(rfindley): develop a theory of "trivial" inlining, which are + // inlinings that don't require re-type checking. + // + // In principle, if the inlining only involves replacing one call with + // another, the scope of the caller is unchanged and there is no need to + // type check again before inlining subsequent calls (edits should not + // overlap, and should not affect each other semantically). However, it + // feels sufficiently complicated that, to be safe, this optimization is + // deferred until later. + + file, err = parser.ParseFile(fset, uri.Filename(), content, parser.ParseComments|parser.SkipObjectResolution) + if err != nil { + return nil, bug.Errorf("inlined file failed to parse: %v", err) + } + + // After inlining one call with a removed parameter, the package will + // fail to type check due to "not enough arguments". Therefore, we must + // allow type errors here. + // + // Assumption: the resulting type errors do not affect the correctness of + // subsequent inlining, because invalid arguments to a call do not affect + // anything in the surrounding scope. + // + // TODO(rfindley): improve this. + tpkg, tinfo, err = reTypeCheck(logf, callInfo.pkg, map[span.URI]*ast.File{uri: file}, true) + if err != nil { + return nil, bug.Errorf("type checking after inlining failed: %v", err) + } + + // Collect calls to the target function in the modified declaration. + var calls2 []*ast.CallExpr + ast.Inspect(file, func(n ast.Node) bool { + if call, ok := n.(*ast.CallExpr); ok { + fn := typeutil.StaticCallee(tinfo, call) + if fn != nil && fn.Pkg().Path() == string(pkg.Metadata().PkgPath) && fn.Name() == origDecl.Name.Name { + calls2 = append(calls2, call) + } + } + return true + }) + + // If the number of calls has increased, this process will never cease. + // If the number of calls has decreased, assume that inlining removed a + // call. + // If the number of calls didn't change, assume that inlining replaced + // a call, and move on to the next. + // + // Assumption: we're inlining a call that has at most one recursive + // reference (which holds for signature rewrites). + // + // TODO(rfindley): this isn't good enough. We should be able to support + // inlining all existing calls even if they increase calls. How do we + // correlate the before and after syntax? + switch { + case len(calls2) > len(calls): + return nil, fmt.Errorf("inlining increased calls %d->%d, possible recursive call? content:\n%s", len(calls), len(calls2), content) + case len(calls2) < len(calls): + calls = calls2 + case len(calls2) == len(calls): + calls = calls2 + currentCall++ + } + } + + result[callInfo.pgf.URI] = content + } + return result, nil +} diff --git a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go index d0ecd50ac6b..2cbce615962 100644 --- a/gopls/internal/lsp/source/util.go +++ b/gopls/internal/lsp/source/util.go @@ -119,6 +119,8 @@ func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) { func FormatNode(fset *token.FileSet, n ast.Node) string { var buf strings.Builder if err := printer.Fprint(&buf, fset, n); err != nil { + // TODO(rfindley): we should use bug.Reportf here. + // We encounter this during completion.resolveInvalid. return "" } return buf.String() @@ -531,3 +533,9 @@ func embeddedIdent(x ast.Expr) *ast.Ident { } return nil } + +// An importFunc is an implementation of the single-method +// types.Importer interface based on a function value. +type ImporterFunc func(path string) (*types.Package, error) + +func (f ImporterFunc) Import(path string) (*types.Package, error) { return f(path) } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index f4a81922b2b..d501e04ce00 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -284,24 +284,44 @@ func SnapshotLabels(snapshot Snapshot) []label.Label { return []label.Label{tag.Snapshot.Of(snapshot.SequenceID()), tag.Directory.Of(snapshot.View().Folder())} } -// NarrowestPackageForFile is a convenience function that selects the -// narrowest non-ITV package to which this file belongs, type-checks -// it in the requested mode (full or workspace), and returns it, along -// with the parse tree of that file. +// NarrowestPackageForFile is a convenience function that selects the narrowest +// non-ITV package to which this file belongs, type-checks it in the requested +// mode (full or workspace), and returns it, along with the parse tree of that +// file. // -// The "narrowest" package is the one with the fewest number of files -// that includes the given file. This solves the problem of test -// variants, as the test will have more files than the non-test package. -// (Historically the preference was a parameter but widest was almost -// never needed.) +// The "narrowest" package is the one with the fewest number of files that +// includes the given file. This solves the problem of test variants, as the +// test will have more files than the non-test package. // -// An intermediate test variant (ITV) package has identical source -// to a regular package but resolves imports differently. -// gopls should never need to type-check them. +// An intermediate test variant (ITV) package has identical source to a regular +// package but resolves imports differently. gopls should never need to +// type-check them. // -// Type-checking is expensive. Call snapshot.ParseGo if all you need -// is a parse tree, or snapshot.MetadataForFile if you only need metadata. +// 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 Snapshot, uri span.URI) (Package, *ParsedGoFile, error) { + return selectPackageForFile(ctx, snapshot, uri, func(metas []*Metadata) *Metadata { return metas[0] }) +} + +// WidestPackageForFile is a convenience function that selects the widest +// non-ITV package to which this file belongs, type-checks it in the requested +// mode (full or workspace), and returns it, along with the parse tree of that +// file. +// +// The "widest" package is the one with the most number of files that includes +// the given file. Which is the test variant if one exists. +// +// An intermediate test variant (ITV) package has identical source to a regular +// package but resolves imports differently. gopls should never need to +// type-check them. +// +// 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 Snapshot, uri span.URI) (Package, *ParsedGoFile, error) { + return selectPackageForFile(ctx, snapshot, uri, func(metas []*Metadata) *Metadata { return metas[len(metas)-1] }) +} + +func selectPackageForFile(ctx context.Context, snapshot Snapshot, uri span.URI, selector func([]*Metadata) *Metadata) (Package, *ParsedGoFile, error) { metas, err := snapshot.MetadataForFile(ctx, uri) if err != nil { return nil, nil, err @@ -310,8 +330,8 @@ func NarrowestPackageForFile(ctx context.Context, snapshot Snapshot, uri span.UR if len(metas) == 0 { return nil, nil, fmt.Errorf("no package metadata for file %s", uri) } - narrowest := metas[0] - pkgs, err := snapshot.TypeCheck(ctx, narrowest.ID) + md := selector(metas) + pkgs, err := snapshot.TypeCheck(ctx, md.ID) if err != nil { return nil, nil, err } diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt new file mode 100644 index 00000000000..c7f8d546ac0 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt @@ -0,0 +1,264 @@ +This test exercises the refactoring to remove unused parameters. + +-- go.mod -- +module unused.mod + +go 1.18 + +-- a/a.go -- +package a + +func A(x, unused int) int { //@codeaction("refactor.rewrite", "unused", "unused", a) + return x +} + +-- @a/a/a.go -- +package a + +func A(x int) int { //@codeaction("refactor.rewrite", "unused", "unused", a) + return x +} + +-- a/a2.go -- +package a + +func _() { + A(1, 2) +} + +-- a/a_test.go -- +package a + +func _() { + A(1, 2) +} + +-- a/a_x_test.go -- +package a_test + +import "unused.mod/a" + +func _() { + a.A(1, 2) +} + +-- b/b.go -- +package b + +import "unused.mod/a" + +func f() int { + return 1 +} + +func g() int { + return 2 +} + +func _() { + a.A(f(), 1) +} + +-- @a/a/a2.go -- +package a + +func _() { + A(1) +} +-- @a/a/a_test.go -- +package a + +func _() { + A(1) +} +-- @a/a/a_x_test.go -- +package a_test + +import "unused.mod/a" + +func _() { + a.A(1) +} +-- @a/b/b.go -- +package b + +import "unused.mod/a" + +func f() int { + return 1 +} + +func g() int { + return 2 +} + +func _() { + a.A(f()) +} +-- field/field.go -- +package field + +func Field(x int, field int) { //@codeaction("refactor.rewrite", "int", "int", field) +} + +func _() { + Field(1, 2) +} +-- @field/field/field.go -- +package field + +func Field(field int) { //@codeaction("refactor.rewrite", "int", "int", field) +} + +func _() { + Field(2) +} +-- ellipsis/ellipsis.go -- +package ellipsis + +func Ellipsis(...any) { //@codeaction("refactor.rewrite", "any", "any", ellipsis) +} + +func _() { + // TODO(rfindley): investigate the broken formatting resulting from these inlinings. + Ellipsis() + Ellipsis(1) + Ellipsis(1, 2) + Ellipsis(1, f(), g()) + Ellipsis(h()) + Ellipsis(i()...) +} + +func f() int +func g() int +func h() (int, int) +func i() []any + +-- @ellipsis/ellipsis/ellipsis.go -- +package ellipsis + +func Ellipsis() { //@codeaction("refactor.rewrite", "any", "any", ellipsis) +} + +func _() { + // TODO(rfindley): investigate the broken formatting resulting from these inlinings. + Ellipsis() + Ellipsis() + Ellipsis() + + var _ []any = []any{1, f(), g()} + Ellipsis() + + func(_ ...any) { + Ellipsis() + }(h()) + + var _ []any = i() + Ellipsis() + +} + +func f() int +func g() int +func h() (int, int) +func i() []any +-- ellipsis2/ellipsis2.go -- +package ellipsis2 + +func Ellipsis2(_, _ int, rest ...int) { //@codeaction("refactor.rewrite", "_", "_", ellipsis2) +} + +func _() { + Ellipsis2(1,2,3) + Ellipsis2(h()) + Ellipsis2(1,2, []int{3, 4}...) +} + +func h() (int, int) + +-- @ellipsis2/ellipsis2/ellipsis2.go -- +package ellipsis2 + +func Ellipsis2(_ int, rest ...int) { //@codeaction("refactor.rewrite", "_", "_", ellipsis2) +} + +func _() { + Ellipsis2(2, []int{3}...) + func(_, blank0 int, rest ...int) { + Ellipsis2(blank0, rest...) + }(h()) + Ellipsis2(2, []int{3, 4}...) +} + +func h() (int, int) +-- overlapping/overlapping.go -- +package overlapping + +func Overlapping(i int) int { //@codeaction("refactor.rewrite", re"(i) int", re"(i) int", overlapping) + return 0 +} + +func _() { + x := Overlapping(Overlapping(0)) + _ = x +} +-- @overlapping/overlapping/overlapping.go -- +package overlapping + +func Overlapping() int { //@codeaction("refactor.rewrite", re"(i) int", re"(i) int", overlapping) + return 0 +} + +func _() { + x := func(_ int) int { + return Overlapping() + }(Overlapping()) + _ = x +} +-- effects/effects.go -- +package effects + +func effects(x, y int) int { //@codeaction("refactor.rewrite", "y", "y", effects) + return x +} + +func f() int +func g() int + +func _() { + effects(f(), g()) + effects(f(), g()) +} +-- @effects/effects/effects.go -- +package effects + +func effects(x int) int { //@codeaction("refactor.rewrite", "y", "y", effects) + return x +} + +func f() int +func g() int + +func _() { + + var x, _ int = f(), g() + effects(x) + + { + var x, _ int = f(), g() + effects(x) + } +} +-- recursive/recursive.go -- +package recursive + +func Recursive(x int) int { //@codeaction("refactor.rewrite", "x", "x", recursive) + return Recursive(1) +} + +-- @recursive/recursive/recursive.go -- +package recursive + +func Recursive() int { //@codeaction("refactor.rewrite", "x", "x", recursive) + return Recursive() +} diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt new file mode 100644 index 00000000000..39f3ddbf121 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt @@ -0,0 +1,55 @@ +This test exercises behavior of change signature refactoring with respect to +comments. + +Currently, inline comments around arguments or parameters are dropped, which is +probably acceptable. Fixing this is likely intractible without fixing comment +representation in the AST. + +-- go.mod -- +module unused.mod + +go 1.18 + +-- a/a.go -- +package a + +// A doc comment. +func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("refactor.rewrite", "unused", "unused", a) + // about to return + return x // returning + // just returned +} + +// This function makes calls. +func _() { + // about to call + A(one() /* used arg */, 2 /* unused arg */) // calling + // just called +} + +func one() int { + // I should be unaffected! + return 1 +} + +-- @a/a/a.go -- +package a + +// A doc comment. +func A(x int) int { //@codeaction("refactor.rewrite", "unused", "unused", a) + // about to return + return x // returning + // just returned +} + +// This function makes calls. +func _() { + // about to call + A(one()) // calling + // just called +} + +func one() int { + // I should be unaffected! + return 1 +} diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt new file mode 100644 index 00000000000..417318497d7 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt @@ -0,0 +1,19 @@ +This test exercises change signature refactoring handling of function values. + +TODO(rfindley): use a literalization strategy to allow these references. + +-- go.mod -- +module unused.mod + +go 1.18 + +-- a/a.go -- +package a + +func A(x, unused int) int { //@codeactionerr("refactor.rewrite", "unused", "unused", re"non-call function reference") + return x +} + +func _() { + _ = A +} diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt new file mode 100644 index 00000000000..8a09cb97452 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt @@ -0,0 +1,171 @@ +This test checks the behavior of removing a parameter with respect to various +import scenarios. + +-- go.mod -- +module mod.test + +go 1.21 + + +-- a/a1.go -- +package a + +import "mod.test/b" + +func _() { + b.B(<-b.Chan, <-b.Chan) +} + +-- a/a2.go -- +package a + +import "mod.test/b" + +func _() { + b.B(<-b.Chan, <-b.Chan) + b.B(<-b.Chan, <-b.Chan) +} + +-- a/a3.go -- +package a + +import "mod.test/b" + +func _() { + b.B(<-b.Chan, <-b.Chan) +} + +func _() { + b.B(<-b.Chan, <-b.Chan) +} + +-- a/a4.go -- +package a + +// TODO(rfindley/adonovan): inlining here adds an additional import of +// mod.test/b. Can we do better? +import ( + . "mod.test/b" +) + +func _() { + B(<-Chan, <-Chan) +} + +-- b/b.go -- +package b + +import "mod.test/c" + +var Chan chan c.C + +func B(x, y c.C) { //@codeaction("refactor.rewrite", "x", "x", b) +} + +-- c/c.go -- +package c + +type C int + +-- d/d.go -- +package d + +// Removing the parameter should remove this import. +import "mod.test/c" + +func D(x c.C) { //@codeaction("refactor.rewrite", "x", "x", d) +} + +func _() { + D(1) +} + +-- @b/a/a1.go -- +package a + +import ( + "mod.test/b" + c "mod.test/c" +) + +func _() { + + var _ c.C = <-b.Chan + b.B(<-b.Chan) + +} +-- @b/a/a2.go -- +package a + +import ( + "mod.test/b" + c "mod.test/c" +) + +func _() { + + var _ c.C = <-b.Chan + b.B(<-b.Chan) + + var _ c.C = <-b.Chan + b.B(<-b.Chan) + +} +-- @b/a/a3.go -- +package a + +import ( + "mod.test/b" + c "mod.test/c" +) + +func _() { + + var _ c.C = <-b.Chan + b.B(<-b.Chan) + +} + +func _() { + + var _ c.C = <-b.Chan + b.B(<-b.Chan) + +} +-- @b/a/a4.go -- +package a + +// TODO(rfindley/adonovan): inlining here adds an additional import of +// mod.test/b. Can we do better? +import ( + . "mod.test/b" + b "mod.test/b" + c "mod.test/c" +) + +func _() { + + var _ c.C = <-Chan + b.B(<-Chan) + +} +-- @b/b/b.go -- +package b + +import "mod.test/c" + +var Chan chan c.C + +func B(y c.C) { //@codeaction("refactor.rewrite", "x", "x", b) +} +-- @d/d/d.go -- +package d + +// Removing the parameter should remove this import. + +func D() { //@codeaction("refactor.rewrite", "x", "x", d) +} + +func _() { + D() +} diff --git a/internal/astutil/clone.go b/internal/astutil/clone.go new file mode 100644 index 00000000000..d5ee82c58b2 --- /dev/null +++ b/internal/astutil/clone.go @@ -0,0 +1,71 @@ +// 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 astutil + +import ( + "go/ast" + "reflect" +) + +// CloneNode returns a deep copy of a Node. +// It omits pointers to ast.{Scope,Object} variables. +func CloneNode[T ast.Node](n T) T { + return cloneNode(n).(T) +} + +func cloneNode(n ast.Node) ast.Node { + var clone func(x reflect.Value) reflect.Value + set := func(dst, src reflect.Value) { + src = clone(src) + if src.IsValid() { + dst.Set(src) + } + } + clone = func(x reflect.Value) reflect.Value { + switch x.Kind() { + case reflect.Ptr: + if x.IsNil() { + return x + } + // Skip fields of types potentially involved in cycles. + switch x.Interface().(type) { + case *ast.Object, *ast.Scope: + return reflect.Zero(x.Type()) + } + y := reflect.New(x.Type().Elem()) + set(y.Elem(), x.Elem()) + return y + + case reflect.Struct: + y := reflect.New(x.Type()).Elem() + for i := 0; i < x.Type().NumField(); i++ { + set(y.Field(i), x.Field(i)) + } + return y + + case reflect.Slice: + if x.IsNil() { + return x + } + y := reflect.MakeSlice(x.Type(), x.Len(), x.Cap()) + for i := 0; i < x.Len(); i++ { + set(y.Index(i), x.Index(i)) + } + return y + + case reflect.Interface: + y := reflect.New(x.Type()).Elem() + set(y, x.Elem()) + return y + + case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer: + panic(x) // unreachable in AST + + default: + return x // bool, string, number + } + } + return clone(reflect.ValueOf(n)).Interface().(ast.Node) +} diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 53119e372ec..53adfbc579c 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -21,6 +21,7 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/imports" + internalastutil "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/typeparams" ) @@ -487,8 +488,12 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu if samePkg { // Caller and callee are in same package. // Check caller has not shadowed the decl. - found := caller.lookup(obj.Name) // can't fail - if !isPkgLevel(found) { + // + // This may fail if the callee is "fake", such as for signature + // refactoring where the callee is modified to be a trivial wrapper + // around the refactored signature. + found := caller.lookup(obj.Name) + if found != nil && !isPkgLevel(found) { return nil, fmt.Errorf("cannot inline because %q is shadowed in caller by a %s (line %d)", obj.Name, objectKind(found), caller.Fset.PositionFor(found.Pos(), false).Line) @@ -1323,7 +1328,7 @@ next: logf("replacing parameter %q by argument %q", param.info.Name, debugFormatNode(caller.Fset, arg.expr)) for _, ref := range param.info.Refs { - replaceCalleeID(ref, cloneNode(arg.expr).(ast.Expr)) + replaceCalleeID(ref, internalastutil.CloneNode(arg.expr).(ast.Expr)) } params[i] = nil // substituted args[i] = nil // substituted @@ -2340,60 +2345,6 @@ func replaceNode(root ast.Node, from, to ast.Node) { } } -// cloneNode returns a deep copy of a Node. -// It omits pointers to ast.{Scope,Object} variables. -func cloneNode(n ast.Node) ast.Node { - var clone func(x reflect.Value) reflect.Value - set := func(dst, src reflect.Value) { - src = clone(src) - if src.IsValid() { - dst.Set(src) - } - } - clone = func(x reflect.Value) reflect.Value { - switch x.Kind() { - case reflect.Ptr: - if x.IsNil() { - return x - } - // Skip fields of types potentially involved in cycles. - switch x.Interface().(type) { - case *ast.Object, *ast.Scope: - return reflect.Zero(x.Type()) - } - y := reflect.New(x.Type().Elem()) - set(y.Elem(), x.Elem()) - return y - - case reflect.Struct: - y := reflect.New(x.Type()).Elem() - for i := 0; i < x.Type().NumField(); i++ { - set(y.Field(i), x.Field(i)) - } - return y - - case reflect.Slice: - y := reflect.MakeSlice(x.Type(), x.Len(), x.Cap()) - for i := 0; i < x.Len(); i++ { - set(y.Index(i), x.Index(i)) - } - return y - - case reflect.Interface: - y := reflect.New(x.Type()).Elem() - set(y, x.Elem()) - return y - - case reflect.Array, reflect.Chan, reflect.Func, reflect.Map, reflect.UnsafePointer: - panic(x) // unreachable in AST - - default: - return x // bool, string, number - } - } - return clone(reflect.ValueOf(n)).Interface().(ast.Node) -} - // clearPositions destroys token.Pos information within the tree rooted at root, // as positions in callee trees may cause caller comments to be emitted prematurely. // From f744e4be4bf26afd2ea076abd8d68f63e1587ba9 Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 5 Oct 2023 14:41:16 -0700 Subject: [PATCH 026/100] go/ssa: propagate goversions in ssa Propagate goversions on functions and initializers. Updates golang/go#63374 Change-Id: I67b25b65fd83888fa27818aee3570b8bf4c09508 Reviewed-on: https://go-review.googlesource.com/c/tools/+/533239 TryBot-Result: Gopher Robot Run-TryBot: Tim King Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/ssa/builder.go | 18 ++++++-- go/ssa/builder_go122_test.go | 88 ++++++++++++++++++++++++++++++++++++ go/ssa/create.go | 33 +++++++++----- go/ssa/func.go | 1 + go/ssa/instantiate.go | 13 +++--- go/ssa/source.go | 26 +++++++++++ go/ssa/ssa.go | 12 +++-- go/ssa/ssautil/load.go | 1 + go/ssa/versions_go122.go | 21 +++++++++ go/ssa/wrappers.go | 8 +++- 10 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 go/ssa/builder_go122_test.go create mode 100644 go/ssa/versions_go122.go diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 0e49537d00a..9e8d12e072c 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -647,7 +647,8 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { typeparams: fn.typeparams, // share the parent's type parameters. typeargs: fn.typeargs, // share the parent's type arguments. info: fn.info, - subst: fn.subst, // share the parent's type substitutions. + subst: fn.subst, // share the parent's type substitutions. + goversion: fn.goversion, // share the parent's goversion } fn.AnonFuncs = append(fn.AnonFuncs, fn2) b.created.Add(fn2) @@ -2516,11 +2517,18 @@ func (p *Package) build() { if len(p.info.InitOrder) > 0 && len(p.files) == 0 { panic("no source files provided for package. cannot initialize globals") } + for _, varinit := range p.info.InitOrder { if init.Prog.mode&LogSource != 0 { fmt.Fprintf(os.Stderr, "build global initializer %v @ %s\n", varinit.Lhs, p.Prog.Fset.Position(varinit.Rhs.Pos())) } + // Initializers for global vars are evaluated in dependency + // order, but may come from arbitrary files of the package + // with different versions, so we transiently update + // init.goversion for each one. (Since init is a synthetic + // function it has no syntax of its own that needs a version.) + init.goversion = p.initVersion[varinit.Rhs] if len(varinit.Lhs) == 1 { // 1:1 initialization: var x, y = a(), b() var lval lvalue @@ -2541,6 +2549,7 @@ func (p *Package) build() { } } } + init.goversion = "" // The rest of the init function is synthetic. No syntax => no goversion. // Call all of the declared init() functions in source order. for _, file := range p.files { @@ -2585,8 +2594,11 @@ func (p *Package) build() { b.needsRuntimeTypes() // Add all of the runtime type information. May CREATE Functions. } - p.info = nil // We no longer need ASTs or go/types deductions. - p.created = nil // We no longer need created functions. + // We no longer need transient information: ASTs or go/types deductions. + p.info = nil + p.created = nil + p.files = nil + p.initVersion = nil if p.Prog.mode&SanityCheckFunctions != 0 { sanityCheckPackage(p) diff --git a/go/ssa/builder_go122_test.go b/go/ssa/builder_go122_test.go new file mode 100644 index 00000000000..21930d649a1 --- /dev/null +++ b/go/ssa/builder_go122_test.go @@ -0,0 +1,88 @@ +// 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.22 +// +build go1.22 + +package ssa_test + +import ( + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func TestMultipleGoversions(t *testing.T) { + var contents = map[string]string{ + "post.go": ` + //go:build go1.22 + package p + + var distinct = func(l []int) []*int { + var r []*int + for i := range l { + r = append(r, &i) + } + return r + }(l) + `, + "pre.go": ` + package p + + var l = []int{0, 0, 0} + + var same = func(l []int) []*int { + var r []*int + for i := range l { + r = append(r, &i) + } + return r + }(l) + `, + } + + fset := token.NewFileSet() + var files []*ast.File + for _, fname := range []string{"post.go", "pre.go"} { + file, err := parser.ParseFile(fset, fname, contents[fname], 0) + if err != nil { + t.Fatal(err) + } + files = append(files, file) + } + + pkg := types.NewPackage("p", "") + conf := &types.Config{Importer: nil, GoVersion: "go1.21"} + p, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + fns := ssautil.AllFunctions(p.Prog) + names := make(map[string]*ssa.Function) + for fn := range fns { + names[fn.String()] = fn + } + for _, item := range []struct{ name, wantSyn, wantPos string }{ + {"p.init", "package initializer", "-"}, + {"p.init$1", "", "post.go:5:17"}, + {"p.init$2", "", "pre.go:6:13"}, + } { + fn := names[item.name] + if fn == nil { + t.Fatalf("Could not find function named %q in package %s", item.name, p) + } + if fn.Synthetic != item.wantSyn { + t.Errorf("Function %q.Syntethic=%q. expected %q", fn, fn.Synthetic, item.wantSyn) + } + if got := fset.Position(fn.Pos()).String(); got != item.wantPos { + t.Errorf("Function %q.Pos()=%q. expected %q", fn, got, item.wantPos) + } + } +} diff --git a/go/ssa/create.go b/go/ssa/create.go index 1bf88c83e76..90cb9bb2e94 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -47,9 +47,10 @@ func NewProgram(fset *token.FileSet, mode BuilderMode) *Program { // typechecker object obj. // // For objects from Go source code, syntax is the associated syntax -// tree (for funcs and vars only); it will be used during the build +// tree (for funcs and vars only) and goversion defines the +// appropriate interpretation; they will be used during the build // phase. -func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { +func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion string) { name := obj.Name() switch obj := obj.(type) { case *types.Builtin: @@ -108,6 +109,7 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { Prog: pkg.Prog, typeparams: tparams, info: pkg.info, + goversion: goversion, } pkg.created.Add(fn) if syntax == nil { @@ -130,7 +132,7 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { // membersFromDecl populates package pkg with members for each // typechecker object (var, func, const or type) associated with the // specified decl. -func membersFromDecl(pkg *Package, decl ast.Decl) { +func membersFromDecl(pkg *Package, decl ast.Decl, goversion string) { switch decl := decl.(type) { case *ast.GenDecl: // import, const, type or var switch decl.Tok { @@ -138,16 +140,19 @@ func membersFromDecl(pkg *Package, decl ast.Decl) { for _, spec := range decl.Specs { for _, id := range spec.(*ast.ValueSpec).Names { if !isBlankIdent(id) { - memberFromObject(pkg, pkg.info.Defs[id], nil) + memberFromObject(pkg, pkg.info.Defs[id], nil, "") } } } case token.VAR: for _, spec := range decl.Specs { + for _, rhs := range spec.(*ast.ValueSpec).Values { + pkg.initVersion[rhs] = goversion + } for _, id := range spec.(*ast.ValueSpec).Names { if !isBlankIdent(id) { - memberFromObject(pkg, pkg.info.Defs[id], spec) + memberFromObject(pkg, pkg.info.Defs[id], spec, goversion) } } } @@ -156,7 +161,7 @@ func membersFromDecl(pkg *Package, decl ast.Decl) { for _, spec := range decl.Specs { id := spec.(*ast.TypeSpec).Name if !isBlankIdent(id) { - memberFromObject(pkg, pkg.info.Defs[id], nil) + memberFromObject(pkg, pkg.info.Defs[id], nil, "") } } } @@ -164,7 +169,7 @@ func membersFromDecl(pkg *Package, decl ast.Decl) { case *ast.FuncDecl: id := decl.Name if !isBlankIdent(id) { - memberFromObject(pkg, pkg.info.Defs[id], decl) + memberFromObject(pkg, pkg.info.Defs[id], decl, goversion) } } } @@ -197,8 +202,10 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * Members: make(map[string]Member), objects: make(map[types.Object]Member), Pkg: pkg, - info: info, // transient (CREATE and BUILD phases) - files: files, // transient (CREATE and BUILD phases) + // transient values (CREATE and BUILD phases) + info: info, + files: files, + initVersion: make(map[ast.Expr]string), } // Add init() function. @@ -209,6 +216,7 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * Pkg: p, Prog: prog, info: p.info, + goversion: "", // See Package.build() for details. } p.Members[p.init.name] = p.init p.created.Add(p.init) @@ -218,8 +226,9 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * if len(files) > 0 { // Go source package. for _, file := range files { + goversion := goversionOf(p, file) for _, decl := range file.Decls { - membersFromDecl(p, decl) + membersFromDecl(p, decl, goversion) } } } else { @@ -229,11 +238,11 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * scope := p.Pkg.Scope() for _, name := range scope.Names() { obj := scope.Lookup(name) - memberFromObject(p, obj, nil) + memberFromObject(p, obj, nil, "") if obj, ok := obj.(*types.TypeName); ok { if named, ok := obj.Type().(*types.Named); ok { for i, n := 0, named.NumMethods(); i < n; i++ { - memberFromObject(p, named.Method(i), nil) + memberFromObject(p, named.Method(i), nil, "") } } } diff --git a/go/ssa/func.go b/go/ssa/func.go index 38c3e31baff..7b374442e95 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -324,6 +324,7 @@ func (f *Function) finishBody() { f.namedResults = nil // (used by lifting) f.info = nil f.subst = nil + f.goversion = "" numberRegisters(f) // uses f.namedRegisters } diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index 38249dea26d..94c7b6411bc 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -32,8 +32,8 @@ type instanceSet struct { fn *Function // fn.typeparams.Len() > 0 and len(fn.typeargs) == 0. instances map[*typeList]*Function // canonical type arguments to an instance. syntax *ast.FuncDecl // fn.syntax copy for instantiating after fn is done. nil on synthetic packages. - info *types.Info // fn.pkg.info copy for building after fn is done.. nil on synthetic packages. - + info *types.Info // fn.pkg.info copy for building after fn is done. nil on synthetic packages. + goversion string // goversion to build syntax with. // TODO(taking): Consider ways to allow for clearing syntax and info when done building. // May require a public API change as MethodValue can request these be built after prog.Build() is done. } @@ -66,9 +66,10 @@ func (prog *Program) createInstanceSet(fn *Function) { if _, ok := prog.instances[fn]; !ok { prog.instances[fn] = &instanceSet{ - fn: fn, - syntax: syntax, - info: fn.info, + fn: fn, + syntax: syntax, + info: fn.info, + goversion: fn.goversion, } } } @@ -169,8 +170,8 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa typeargs: targs, info: insts.info, // on synthetic packages info is nil. subst: subst, + goversion: insts.goversion, } - cr.Add(instance) insts.instances[key] = instance return instance diff --git a/go/ssa/source.go b/go/ssa/source.go index 9c900e3aab1..487abcf814d 100644 --- a/go/ssa/source.go +++ b/go/ssa/source.go @@ -11,6 +11,7 @@ package ssa // the originating syntax, as specified. import ( + "fmt" "go/ast" "go/token" "go/types" @@ -131,6 +132,31 @@ func findNamedFunc(pkg *Package, pos token.Pos) *Function { return nil } +// goversionOf returns the goversion of a node in the package +// where the node is either a function declaration or the initial +// value of a package level variable declaration. +func goversionOf(p *Package, file *ast.File) string { + if p.info == nil { + return "" + } + + // TODO(taking): Update to the following when internal/versions available: + // return versions.Lang(versions.FileVersions(p.info, file)) + return fileVersions(file) +} + +// TODO(taking): Remove when internal/versions is available. +var fileVersions = func(file *ast.File) string { return "" } + +// parses a goXX.YY version or returns a negative version on an error. +// TODO(taking): Switch to a permanent solution when internal/versions is submitted. +func parseGoVersion(x string) (major, minor int) { + if _, err := fmt.Sscanf(x, "go%d.%d", &major, &minor); err != nil || major < 0 || minor < 0 { + return -1, -1 + } + return +} + // ValueForExpr returns the SSA Value that corresponds to non-constant // expression e. // diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index bd42f2e0a90..04b7157deb4 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -57,11 +57,12 @@ type Package struct { // The following fields are set transiently, then cleared // after building. - buildOnce sync.Once // ensures package building occurs once - ninit int32 // number of init functions - info *types.Info // package type information - files []*ast.File // package ASTs - created creator // members created as a result of building this package (includes declared functions, wrappers) + buildOnce sync.Once // ensures package building occurs once + ninit int32 // number of init functions + info *types.Info // package type information + files []*ast.File // package ASTs + created creator // members created as a result of building this package (includes declared functions, wrappers) + initVersion map[ast.Expr]string // goversion to use for each global var init expr } // A Member is a member of a Go package, implemented by *NamedConst, @@ -338,6 +339,7 @@ type Function struct { lblocks map[types.Object]*lblock // labelled blocks info *types.Info // *types.Info to build from. nil for wrappers. subst *subster // non-nil => expand generic body using this type substitution of ground types + goversion string // Go version of syntax (NB: init is special) } // BasicBlock represents an SSA basic block. diff --git a/go/ssa/ssautil/load.go b/go/ssa/ssautil/load.go index 96d69a20a17..281ad10f52d 100644 --- a/go/ssa/ssautil/load.go +++ b/go/ssa/ssautil/load.go @@ -147,6 +147,7 @@ func BuildPackage(tc *types.Config, fset *token.FileSet, pkg *types.Package, fil Selections: make(map[*ast.SelectorExpr]*types.Selection), } typeparams.InitInstanceInfo(info) + // versions.InitFileVersions(info) // TODO(taking): Enable when internal/versions is available. if err := types.NewChecker(tc, fset, pkg, info).Files(files); err != nil { return nil, nil, err } diff --git a/go/ssa/versions_go122.go b/go/ssa/versions_go122.go new file mode 100644 index 00000000000..b74165a8e32 --- /dev/null +++ b/go/ssa/versions_go122.go @@ -0,0 +1,21 @@ +// 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.22 +// +build go1.22 + +package ssa + +import ( + "go/ast" +) + +func init() { + fileVersions = func(file *ast.File) string { + if maj, min := parseGoVersion(file.GoVersion); maj >= 0 && min >= 0 { + return file.GoVersion + } + return "" + } +} diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index 123ea6858aa..f71c27ddc2f 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -72,7 +72,9 @@ func makeWrapper(prog *Program, sel *selection, cr *creator) *Function { Synthetic: description, Prog: prog, pos: obj.Pos(), - info: nil, // info is not set on wrappers. + // wrappers have no syntax + info: nil, + goversion: "", } cr.Add(fn) fn.startBody() @@ -200,7 +202,9 @@ func makeBound(prog *Program, obj *types.Func, cr *creator) *Function { Synthetic: description, Prog: prog, pos: obj.Pos(), - info: nil, // info is not set on wrappers. + // wrappers have no syntax + info: nil, + goversion: "", } cr.Add(fn) From 8a71c399c094758304fa198a6921d86f1fb9bbf6 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 16 Oct 2023 12:23:06 -0400 Subject: [PATCH 027/100] gopls/internal/lsp/source: abort change signature on overlapping calls The heuristic used to identify remaining uninlined calls by counting fails when the inlining operation changes the order of "original" calls, as may be the case with overlapping calls, following CL 535456. I spent around an hour trying to fix this properly (by tracking calls that were "just" inlined), but it's too hard because we must also track calls that had previously been inlined. It needs more theory and thought. For now, remove support for overlapping calls, as in all other cases the top-to-bottom counting heuristic should still work. For golang/go#63534 Change-Id: I59fe4aa5d20e56b0eb9dcbc01d29dd2ece082c17 Reviewed-on: https://go-review.googlesource.com/c/tools/+/535795 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Robert Findley --- gopls/internal/lsp/source/inline_all.go | 12 ++++++++++++ .../marker/testdata/codeaction/removeparam.txt | 14 +------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/gopls/internal/lsp/source/inline_all.go b/gopls/internal/lsp/source/inline_all.go index d814a115d17..848b5f7cc08 100644 --- a/gopls/internal/lsp/source/inline_all.go +++ b/gopls/internal/lsp/source/inline_all.go @@ -9,6 +9,7 @@ import ( "fmt" "go/ast" "go/parser" + "go/types" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/types/typeutil" @@ -170,6 +171,17 @@ func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot Sna file = callInfo.pgf.File content = callInfo.pgf.Src ) + + // Check for overlapping calls (such as Foo(Foo())). We can't handle these + // because inlining may change the source order of the inner call with + // respect to the inlined outer call, and so the heuristic we use to find + // the next call (counting from top-to-bottom) does not work. + for i := range calls { + if i > 0 && calls[i-1].End() > calls[i].Pos() { + return nil, fmt.Errorf("%s: can't inline overlapping call %s", uri, types.ExprString(calls[i-1])) + } + } + currentCall := 0 for currentCall < len(calls) { caller := &inline.Caller{ diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt index c7f8d546ac0..2509ce6c538 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt @@ -194,7 +194,7 @@ func h() (int, int) -- overlapping/overlapping.go -- package overlapping -func Overlapping(i int) int { //@codeaction("refactor.rewrite", re"(i) int", re"(i) int", overlapping) +func Overlapping(i int) int { //@codeactionerr("refactor.rewrite", re"(i) int", re"(i) int", re"overlapping") return 0 } @@ -202,19 +202,7 @@ func _() { x := Overlapping(Overlapping(0)) _ = x } --- @overlapping/overlapping/overlapping.go -- -package overlapping - -func Overlapping() int { //@codeaction("refactor.rewrite", re"(i) int", re"(i) int", overlapping) - return 0 -} -func _() { - x := func(_ int) int { - return Overlapping() - }(Overlapping()) - _ = x -} -- effects/effects.go -- package effects From 61bb3e97fcf98f5a728f15569ab206a4cefb86ad Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 13 Oct 2023 13:46:27 -0400 Subject: [PATCH 028/100] internal/refactor/inline: less hacky solution for eliding braces Joint with Alan Donovan (CL 535455) :) For golang/go#63534 Change-Id: Ia4fdc617edf6699da285f56670a51efac1817834 Reviewed-on: https://go-review.googlesource.com/c/tools/+/534918 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- .../testdata/codeaction/removeparam.txt | 6 ---- .../codeaction/removeparam_imports.txt | 11 -------- internal/refactor/inline/inline.go | 28 ++++++++----------- .../inline/testdata/import-shadow.txtar | 4 +-- .../refactor/inline/testdata/issue63298.txtar | 4 +-- .../refactor/inline/testdata/method.txtar | 4 +-- .../inline/testdata/multistmt-body.txtar | 4 +-- .../refactor/inline/testdata/tailcall.txtar | 4 +-- 8 files changed, 17 insertions(+), 48 deletions(-) diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt index 2509ce6c538..7caa660babe 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt @@ -145,17 +145,13 @@ func _() { Ellipsis() Ellipsis() Ellipsis() - var _ []any = []any{1, f(), g()} Ellipsis() - func(_ ...any) { Ellipsis() }(h()) - var _ []any = i() Ellipsis() - } func f() int @@ -228,10 +224,8 @@ func f() int func g() int func _() { - var x, _ int = f(), g() effects(x) - { var x, _ int = f(), g() effects(x) diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt index 8a09cb97452..d616fe21769 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt @@ -89,10 +89,8 @@ import ( ) func _() { - var _ c.C = <-b.Chan b.B(<-b.Chan) - } -- @b/a/a2.go -- package a @@ -103,13 +101,10 @@ import ( ) func _() { - var _ c.C = <-b.Chan b.B(<-b.Chan) - var _ c.C = <-b.Chan b.B(<-b.Chan) - } -- @b/a/a3.go -- package a @@ -120,17 +115,13 @@ import ( ) func _() { - var _ c.C = <-b.Chan b.B(<-b.Chan) - } func _() { - var _ c.C = <-b.Chan b.B(<-b.Chan) - } -- @b/a/a4.go -- package a @@ -144,10 +135,8 @@ import ( ) func _() { - var _ c.C = <-Chan b.B(<-Chan) - } -- @b/b/b.go -- package b diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 53adfbc579c..a1319ce2d80 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -183,15 +183,19 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte, // Precise comment handling would make this a // non-issue. Formatting wouldn't really need a // FileSet at all. - mark := out.Len() - if err := format.Node(&out, caller.Fset, res.new); err != nil { - return nil, err - } if elideBraces { - // Overwrite unnecessary {...} braces with spaces. - // TODO(adonovan): less hacky solution. - out.Bytes()[mark] = ' ' - out.Bytes()[out.Len()-1] = ' ' + for i, stmt := range res.new.(*ast.BlockStmt).List { + if i > 0 { + out.WriteByte('\n') + } + if err := format.Node(&out, caller.Fset, stmt); err != nil { + return nil, err + } + } + } else { + if err := format.Node(&out, caller.Fset, res.new); err != nil { + return nil, err + } } out.Write(caller.Content[end:]) const mode = parser.ParseComments | parser.SkipObjectResolution | parser.AllErrors @@ -902,9 +906,6 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu // The body may use defer, arbitrary control flow, and // multiple returns. // - // TODO(adonovan): omit the braces if the sets of - // names in the two blocks are disjoint. - // // TODO(adonovan): add a strategy for a 'void tail // call', i.e. a call statement prior to an (explicit // or implicit) return. @@ -942,8 +943,6 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu // - all parameters and result vars can be eliminated // or replaced by a binding decl, // - caller ExprStmt is in unrestricted statement context. - // - // If there is only a single statement, the braces are omitted. if stmt := callStmt(caller.path, true); stmt != nil && (!needBindingDecl || bindingDeclStmt != nil) && !callee.HasDefer && @@ -956,9 +955,6 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu if needBindingDecl { body.List = prepend(bindingDeclStmt, body.List...) } - if len(body.List) == 1 { // FIXME do this opt later - repl = body.List[0] // singleton: omit braces - } res.old = stmt res.new = repl return res, nil diff --git a/internal/refactor/inline/testdata/import-shadow.txtar b/internal/refactor/inline/testdata/import-shadow.txtar index 4188a52375d..9d1abdb9e95 100644 --- a/internal/refactor/inline/testdata/import-shadow.txtar +++ b/internal/refactor/inline/testdata/import-shadow.txtar @@ -87,10 +87,8 @@ import ( var x b.T func A(b int) { - b0.One() - b0.Two() - //@ inline(re"F", fresult) + b0.Two() //@ inline(re"F", fresult) } -- d/d.go -- diff --git a/internal/refactor/inline/testdata/issue63298.txtar b/internal/refactor/inline/testdata/issue63298.txtar index e355e8e64d9..990ebcd879a 100644 --- a/internal/refactor/inline/testdata/issue63298.txtar +++ b/internal/refactor/inline/testdata/issue63298.txtar @@ -45,8 +45,6 @@ import ( ) func _() { - b.B() b0.B() - -} \ No newline at end of file +} diff --git a/internal/refactor/inline/testdata/method.txtar b/internal/refactor/inline/testdata/method.txtar index b141b09d707..92343edd840 100644 --- a/internal/refactor/inline/testdata/method.txtar +++ b/internal/refactor/inline/testdata/method.txtar @@ -104,10 +104,8 @@ func (T) h() int { return 1 } func _() { var ptr *T - var _ T = *ptr - _ = 1 - //@ inline(re"h", h) + _ = 1 //@ inline(re"h", h) } -- a/i.go -- diff --git a/internal/refactor/inline/testdata/multistmt-body.txtar b/internal/refactor/inline/testdata/multistmt-body.txtar index 6bd0108e1fe..77027191bd4 100644 --- a/internal/refactor/inline/testdata/multistmt-body.txtar +++ b/internal/refactor/inline/testdata/multistmt-body.txtar @@ -54,10 +54,8 @@ package a func _() { a := 1 - z := 1 - print(a + 2 + z) - //@ inline(re"f", out2) + print(a + 2 + z) //@ inline(re"f", out2) } -- a/a3.go -- diff --git a/internal/refactor/inline/testdata/tailcall.txtar b/internal/refactor/inline/testdata/tailcall.txtar index 53b6de367dd..ccfe9f4d866 100644 --- a/internal/refactor/inline/testdata/tailcall.txtar +++ b/internal/refactor/inline/testdata/tailcall.txtar @@ -36,7 +36,6 @@ start: package a func _() int { - total := 0 start: for i := 1; i <= 2; i++ { @@ -47,8 +46,7 @@ start: return -1 } } - return total - //@ inline(re"sum", sum) + return total //@ inline(re"sum", sum) } func sum(lo, hi int) int { From 53e637bd2e51acc93186a106d6575bae81994de7 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 13 Oct 2023 14:25:25 -0400 Subject: [PATCH 029/100] internal/refactor/inline: improve check for last uses of free vars This showed up when I used the inliner for removing parameters: we should be more precise about detecting the last use of a local var. A TODO is left to make this perfect, which will be done in a subsequent CL. For golang/go#63534 Change-Id: Id5c753f3e7ae51e07db1d29a59e82e51c6d5952c Reviewed-on: https://go-review.googlesource.com/c/tools/+/535335 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- internal/refactor/inline/inline.go | 38 +++++++++++++++++++++---- internal/refactor/inline/inline_test.go | 30 +++++++++++++++++++ 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index a1319ce2d80..8a6d7779d87 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -1258,11 +1258,11 @@ next: // remove the last reference to a caller local var. if caller.enclosingFunc != nil { for free := range arg.freevars { - if v, ok := caller.lookup(free).(*types.Var); ok && within(v.Pos(), caller.enclosingFunc.Body) { - // TODO(adonovan): be more precise and check that v - // is indeed referenced only by call arguments. - // Better: proceed, but blank out its declaration as needed. - logf("keeping param %q: arg contains perhaps the last reference to possible caller local %v @ %v", + // TODO(rfindley): we can get this 100% right by looking for + // references among other arguments which have non-zero references + // within the callee. + if v, ok := caller.lookup(free).(*types.Var); ok && within(v.Pos(), caller.enclosingFunc.Body) && !isUsedOutsideCall(caller, v) { + logf("keeping param %q: arg contains perhaps the last reference to caller local %v @ %v", param.info.Name, v, caller.Fset.PositionFor(v.Pos(), false)) continue next } @@ -1332,6 +1332,34 @@ next: } } +// isUsedOutsideCall reports whether v is used outside of caller.Call, within +// the body of caller.enclosingFunc. +func isUsedOutsideCall(caller *Caller, v *types.Var) bool { + used := false + ast.Inspect(caller.enclosingFunc.Body, func(n ast.Node) bool { + if n == caller.Call { + return false + } + switch n := n.(type) { + case *ast.Ident: + if use := caller.Info.Uses[n]; use == v { + used = true + } + case *ast.FuncType: + // All params are used. + for _, fld := range n.Params.List { + for _, n := range fld.Names { + if def := caller.Info.Defs[n]; def == v { + used = true + } + } + } + } + return !used // keep going until we find a use + }) + return used +} + // checkFalconConstraints checks whether constant arguments // are safe to substitute (e.g. s[i] -> ""[0] is not safe.) // diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 3f797055d90..99cc0de8230 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -594,6 +594,36 @@ func TestSubstitution(t *testing.T) { `func _() { var local int; f(local) }`, `func _() { var local int; _ = local }`, }, + { + "Arguments that are used are detected", + `func f(int) {}`, + `func _() { var local int; _ = local; f(local) }`, + `func _() { var local int; _ = local }`, + }, + { + "Arguments that are used are detected", + `func f(x, y int) { print(x) }`, + `func _() { var z int; f(z, z) }`, + `func _() { + var z int + var _ int = z + print(z) +}`, + }, + { + "Function parameters are always used", + `func f(int) {}`, + `func _() { + func(local int) { + f(local) + }(1) +}`, + `func _() { + func(local int) { + + }(1) +}`, + }, { "Regression test for detection of shadowing in nested functions.", `func f(x int) { _ = func() { y := 1; print(y); print(x) } }`, From 43c41b5e57d49f710e65d5257b71c2dc3a433b2e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 16 Oct 2023 13:17:19 -0400 Subject: [PATCH 030/100] internal/refactor/inline: reify implicit return conversions This change fixes a bug that caused func() error { return nil }() to be reduced to nil in some cases, because the expr-context reduction strategy was abusing safeReturn, which is only appropriate for tailcalls. (That function has been renamed for clarity.) The safeReturn condition in the expr-context strategy has been pushed down to the leaves, allowing it to be relaxed in some cases (e.g. simple tail calls) by materializing conversions. Also, tests. There is more work to do to cleanly factor the reification of implicit return conversions across strategies, and to do it only when actually necessary. Change-Id: I775554a0a3d1348f8dbd9930904edd819f7c3839 Reviewed-on: https://go-review.googlesource.com/c/tools/+/535796 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- internal/refactor/inline/inline.go | 36 ++++++++++++++++++++----- internal/refactor/inline/inline_test.go | 18 ++++++++++++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 8a6d7779d87..be161610882 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -760,6 +760,16 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu needBindingDecl := !allResultsUnreferenced || exists(params, func(i int, p *parameter) bool { return p != nil }) + // The two strategies below overlap for a tail call of {return exprs}: + // The expr-context reduction is nice because it keeps the + // caller's return stmt and merely switches its operand, + // without introducing a new block, but it doesn't work with + // implicit return conversions. + // + // TODO(adonovan): unify these cases more cleanly, allowing return- + // operand replacement and implicit conversions, by adding + // conversions around each return operand (if not a spread return). + // Special case: call to { return exprs }. // // Reduces to: @@ -776,8 +786,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu // callee's body expression, suitably substituted. if len(calleeDecl.Body.List) == 1 && is[*ast.ReturnStmt](calleeDecl.Body.List[0]) && - len(calleeDecl.Body.List[0].(*ast.ReturnStmt).Results) > 0 && // not a bare return - safeReturn(caller, calleeSymbol, callee) { + len(calleeDecl.Body.List[0].(*ast.ReturnStmt).Results) > 0 { // not a bare return results := calleeDecl.Body.List[0].(*ast.ReturnStmt).Results context := callContext(caller.path) @@ -839,11 +848,24 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu if callee.NumResults == 1 { logf("strategy: reduce expr-context call to { return expr }") + // (includes some simple tail-calls) + + // Make implicit return conversion explicit. + if callee.TrivialReturns < callee.TotalReturns { + results[0] = convert(calleeDecl.Type.Results.List[0].Type, results[0]) + } res.old = caller.Call res.new = results[0] - } else { + return res, nil + + } else if callee.TrivialReturns == callee.TotalReturns { logf("strategy: reduce spread-context call to { return expr }") + // There is no general way to reify conversions in a spread + // return, hence the requirement above. + // + // TODO(adonovan): allow this reduction when no + // conversion is required by the context. // The call returns multiple results but is // not a standalone call statement. It must @@ -880,8 +902,8 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu default: return nil, fmt.Errorf("internal error: unexpected context %T for spread call", context) } + return res, nil } - return res, nil } } @@ -911,7 +933,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu // or implicit) return. if ret, ok := callContext(caller.path).(*ast.ReturnStmt); ok && len(ret.Results) == 1 && - safeReturn(caller, calleeSymbol, callee) && + tailCallSafeReturn(caller, calleeSymbol, callee) && !callee.HasBareReturn && (!needBindingDecl || bindingDeclStmt != nil) && !hasLabelConflict(caller.path, callee.Labels) && @@ -2624,9 +2646,9 @@ func declares(stmts []ast.Stmt) map[string]bool { return names } -// safeReturn reports whether the callee's return statements may be safely +// tailCallSafeReturn reports whether the callee's return statements may be safely // used to return from the function enclosing the caller (which must exist). -func safeReturn(caller *Caller, calleeSymbol *types.Func, callee *gobCallee) bool { +func tailCallSafeReturn(caller *Caller, calleeSymbol *types.Func, callee *gobCallee) bool { // It is safe if all callee returns involve only trivial conversions. if callee.TrivialReturns == callee.TotalReturns { return true diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 99cc0de8230..525be74ea60 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -397,6 +397,16 @@ func TestBasics(t *testing.T) { var _ = 1 var _ = 2 var _ = 3 +}`, + }, + { + // (a regression test for a missing conversion) + "Implicit return conversions are inserted in expr-context reduction.", + `func f(x int) error { return nil }`, + `func _() { if err := f(0); err != nil {} }`, + `func _() { + if err := error(nil); err != nil { + } }`, }, }) @@ -673,7 +683,7 @@ func TestTailCallStrategy(t *testing.T) { "Tail call with non-trivial return conversion (caller.sig != callee.sig).", `func f() error { return E{} }; type E struct{error}`, `func _() any { return f() }`, - `func _() any { return func() error { return E{} }() }`, + `func _() any { return error(E{}) }`, }, }) } @@ -714,6 +724,12 @@ func TestSpreadCalls(t *testing.T) { `func _() (int, error) { return f() }`, `func _() (int, error) { return 0, nil }`, }, + { + "Implicit return conversions defeat reduction of spread returns, for now.", + `func f(x int) (_, _ error) { return nil, nil }`, + `func _() { _, _ = f(0) }`, + `func _() { _, _ = func() (_, _ error) { return nil, nil }() }`, + }, }) } From 7df9d5f9fad03a519de4e1803354649306fb5eb1 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 16 Oct 2023 15:17:04 -0400 Subject: [PATCH 031/100] gopls/internal/lsp: fix signature crash on error.Error Fixes golang/go#63578 Change-Id: Id79993c2e4553af1f92923ff6fe481ee00a5edc7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/535875 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/lsp/source/signature_help.go | 21 +++++++++++++++---- .../marker/testdata/completion/testy.txt | 4 ++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go index 1420fc3ee15..dc45322b864 100644 --- a/gopls/internal/lsp/source/signature_help.go +++ b/gopls/internal/lsp/source/signature_help.go @@ -13,6 +13,7 @@ import ( "strings" "golang.org/x/tools/go/ast/astutil" + "golang.org/x/tools/gopls/internal/bug" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/internal/event" ) @@ -74,9 +75,22 @@ FindCall: obj = pkg.GetTypesInfo().ObjectOf(t.Sel) } - // Handle builtin functions separately. - if obj, ok := obj.(*types.Builtin); ok { - return builtinSignature(ctx, snapshot, callExpr, obj.Name(), pos) + // Built-in? + if obj != nil && !obj.Pos().IsValid() { + // built-in function? + if obj, ok := obj.(*types.Builtin); ok { + return builtinSignature(ctx, snapshot, callExpr, obj.Name(), pos) + } + + // error.Error? + if fn, ok := obj.(*types.Func); ok && fn.Name() == "Error" { + return &protocol.SignatureInformation{ + Label: "Error()", + Documentation: stringToSigInfoDocumentation("Error returns the error message.", snapshot.Options()), + }, 0, nil + } + + return nil, 0, bug.Errorf("call to unexpected built-in %v (%T)", obj, obj) } // Get the type information for the function being called. @@ -137,7 +151,6 @@ func builtinSignature(ctx context.Context, snapshot Snapshot, callExpr *ast.Call Documentation: stringToSigInfoDocumentation(sig.doc, snapshot.Options()), Parameters: paramInfo, }, activeParam, nil - } func activeParameter(callExpr *ast.CallExpr, numParams int, variadic bool, pos token.Pos) (activeParam int) { diff --git a/gopls/internal/regtest/marker/testdata/completion/testy.txt b/gopls/internal/regtest/marker/testdata/completion/testy.txt index f26b3ae1b1f..1fbc7a272e3 100644 --- a/gopls/internal/regtest/marker/testdata/completion/testy.txt +++ b/gopls/internal/regtest/marker/testdata/completion/testy.txt @@ -55,3 +55,7 @@ func _() { _ = snippets.X(nil) //@signature("nil", "X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias") var _ sig.Alias } + +func issue63578(err error) { + err.Error() //@signature(")", "Error()") +} From 8ed111356bf28822200c086df2976ac577994888 Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 12 Oct 2023 15:40:16 -0700 Subject: [PATCH 032/100] go/ssa: add support for range-over-int Adds support for ssa to range over int types. Fixes golang/go#63373 Change-Id: I451b175d2c62958e022f1b6489971f1af32b19e0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/535035 Reviewed-by: Alan Donovan Run-TryBot: Tim King LUCI-TryBot-Result: Go LUCI TryBot-Result: Gopher Robot --- go/ssa/builder.go | 129 ++++++++++++++++++------- go/ssa/builder_generic_test.go | 83 +++++++++------- go/ssa/builder_go122_test.go | 99 ++++++++++++++++++- go/ssa/interp/interp_go122_test.go | 31 ++++++ go/ssa/interp/testdata/rangeoverint.go | 86 +++++++++++++++++ internal/testenv/testenv.go | 29 ++++++ 6 files changed, 387 insertions(+), 70 deletions(-) create mode 100644 go/ssa/interp/interp_go122_test.go create mode 100644 go/ssa/interp/testdata/rangeoverint.go diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 9e8d12e072c..c66a6c55079 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -1784,16 +1784,16 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { // forStmt emits to fn code for the for statement s, optionally // labelled by label. func (b *builder) forStmt(fn *Function, s *ast.ForStmt, label *lblock) { - // ...init... - // jump loop + // ...init... + // jump loop // loop: - // if cond goto body else done + // if cond goto body else done // body: - // ...body... - // jump post - // post: (target of continue) - // ...post... - // jump loop + // ...body... + // jump post + // post: (target of continue) + // ...post... + // jump loop // done: (target of break) if s.Init != nil { b.stmt(fn, s.Init) @@ -1841,17 +1841,17 @@ func (b *builder) forStmt(fn *Function, s *ast.ForStmt, label *lblock) { // forPos is the position of the "for" token. func (b *builder) rangeIndexed(fn *Function, x Value, tv types.Type, pos token.Pos) (k, v Value, loop, done *BasicBlock) { // - // length = len(x) - // index = -1 - // loop: (target of continue) - // index++ - // if index < length goto body else done + // length = len(x) + // index = -1 + // loop: (target of continue) + // index++ + // if index < length goto body else done // body: - // k = index - // v = x[index] - // ...body... - // jump loop - // done: (target of break) + // k = index + // v = x[index] + // ...body... + // jump loop + // done: (target of break) // Determine number of iterations. var length Value @@ -1936,16 +1936,16 @@ func (b *builder) rangeIndexed(fn *Function, x Value, tv types.Type, pos token.P // if the respective component is not wanted. func (b *builder) rangeIter(fn *Function, x Value, tk, tv types.Type, pos token.Pos) (k, v Value, loop, done *BasicBlock) { // - // it = range x + // it = range x // loop: (target of continue) - // okv = next it (ok, key, value) - // ok = extract okv #0 - // if ok goto body else done + // okv = next it (ok, key, value) + // ok = extract okv #0 + // if ok goto body else done // body: - // k = extract okv #1 - // v = extract okv #2 - // ...body... - // jump loop + // k = extract okv #1 + // v = extract okv #2 + // ...body... + // jump loop // done: (target of break) // @@ -1998,13 +1998,13 @@ func (b *builder) rangeIter(fn *Function, x Value, tk, tv types.Type, pos token. func (b *builder) rangeChan(fn *Function, x Value, tk types.Type, pos token.Pos) (k Value, loop, done *BasicBlock) { // // loop: (target of continue) - // ko = <-x (key, ok) - // ok = extract ko #1 - // if ok goto body else done + // ko = <-x (key, ok) + // ok = extract ko #1 + // if ok goto body else done // body: - // k = extract ko #0 - // ... - // goto loop + // k = extract ko #0 + // ...body... + // goto loop // done: (target of break) loop = fn.newBasicBlock("rangechan.loop") @@ -2031,6 +2031,57 @@ func (b *builder) rangeChan(fn *Function, x Value, tk types.Type, pos token.Pos) return } +// rangeInt emits to fn the header for a range loop with an integer operand. +// tk is the key value's type, or nil if the k result is not wanted. +// pos is the position of the "for" token. +func (b *builder) rangeInt(fn *Function, x Value, tk types.Type, pos token.Pos) (k Value, loop, done *BasicBlock) { + // + // iter = 0 + // if 0 < x goto body else done + // loop: (target of continue) + // iter++ + // if iter < x goto body else done + // body: + // k = x + // ...body... + // jump loop + // done: (target of break) + + if isUntyped(x.Type()) { + x = emitConv(fn, x, tInt) + } + + T := x.Type() + iter := fn.addLocal(T, token.NoPos) + // x may be unsigned. Avoid initializing x to -1. + + body := fn.newBasicBlock("rangeint.body") + done = fn.newBasicBlock("rangeint.done") + emitIf(fn, emitCompare(fn, token.LSS, zeroConst(T), x, token.NoPos), body, done) + + loop = fn.newBasicBlock("rangeint.loop") + fn.currentBlock = loop + + incr := &BinOp{ + Op: token.ADD, + X: emitLoad(fn, iter), + Y: emitConv(fn, vOne, T), + } + incr.setType(T) + emitStore(fn, iter, fn.emit(incr), pos) + emitIf(fn, emitCompare(fn, token.LSS, incr, x, token.NoPos), body, done) + fn.currentBlock = body + + if tk != nil { + // Integer types (int, uint8, etc.) are named and + // we know that k is assignable to x when tk != nil. + // This implies tk and T are identical so no conversion is needed. + k = emitLoad(fn, iter) + } + + return +} + // rangeStmt emits to fn code for the range statement s, optionally // labelled by label. func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { @@ -2068,9 +2119,21 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { case *types.Chan: k, loop, done = b.rangeChan(fn, x, tk, s.For) - case *types.Map, *types.Basic: // string + case *types.Map: k, v, loop, done = b.rangeIter(fn, x, tk, tv, s.For) + case *types.Basic: + switch { + case rt.Info()&types.IsString != 0: + k, v, loop, done = b.rangeIter(fn, x, tk, tv, s.For) + + case rt.Info()&types.IsInteger != 0: + k, loop, done = b.rangeInt(fn, x, tk, s.For) + + default: + panic("Cannot range over basic type: " + rt.String()) + } + default: panic("Cannot range over: " + rt.String()) } diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go index 8ddf898efd9..9f58349badc 100644 --- a/go/ssa/builder_generic_test.go +++ b/go/ssa/builder_generic_test.go @@ -515,48 +515,19 @@ func TestGenericBodies(t *testing.T) { p := prog.Package(lprog.Package(pkgname).Pkg) p.Build() - // Collect calls to the builtin print function. - probes := make(map[*ssa.CallCommon]*ssa.Function) - for _, mem := range p.Members { - if fn, ok := mem.(*ssa.Function); ok { - for _, bb := range fn.Blocks { - for _, i := range bb.Instrs { - if i, ok := i.(ssa.CallInstruction); ok { - call := i.Common() - if b, ok := call.Value.(*ssa.Builtin); ok && b.Name() == "print" { - probes[i.Common()] = fn - } - } - } - } - } - } - // Collect all notes in f, i.e. comments starting with "//@ types". notes, err := expect.ExtractGo(prog.Fset, f) if err != nil { t.Errorf("expect.ExtractGo: %v", err) } - // Matches each probe with a note that has the same line. - sameLine := func(x, y token.Pos) bool { - xp := prog.Fset.Position(x) - yp := prog.Fset.Position(y) - return xp.Filename == yp.Filename && xp.Line == yp.Line - } - expectations := make(map[*ssa.CallCommon]*expect.Note) + // Collect calls to the builtin print function. + probes := callsTo(p, "print") + expectations := matchNotes(prog.Fset, notes, probes) + for call := range probes { - var match *expect.Note - for _, note := range notes { - if note.Name == "types" && sameLine(call.Pos(), note.Pos) { - match = note // first match is good enough. - break - } - } - if match != nil { - expectations[call] = match - } else { - t.Errorf("Unmatched probe: %v", call) + if expectations[call] == nil { + t.Errorf("Unmatched call: %v", call) } } @@ -575,6 +546,48 @@ func TestGenericBodies(t *testing.T) { } } +// callsTo finds all calls to an SSA value named fname, +// and returns a map from each call site to its enclosing function. +func callsTo(p *ssa.Package, fname string) map[*ssa.CallCommon]*ssa.Function { + callsites := make(map[*ssa.CallCommon]*ssa.Function) + for _, mem := range p.Members { + if fn, ok := mem.(*ssa.Function); ok { + for _, bb := range fn.Blocks { + for _, i := range bb.Instrs { + if i, ok := i.(ssa.CallInstruction); ok { + call := i.Common() + if call.Value.Name() == fname { + callsites[call] = fn + } + } + } + } + } + } + return callsites +} + +// matchNodes returns a mapping from call sites (found by callsTo) +// to the first "//@ note" comment on the same line. +func matchNotes(fset *token.FileSet, notes []*expect.Note, calls map[*ssa.CallCommon]*ssa.Function) map[*ssa.CallCommon]*expect.Note { + // Matches each probe with a note that has the same line. + sameLine := func(x, y token.Pos) bool { + xp := fset.Position(x) + yp := fset.Position(y) + return xp.Filename == yp.Filename && xp.Line == yp.Line + } + expectations := make(map[*ssa.CallCommon]*expect.Note) + for call := range calls { + for _, note := range notes { + if sameLine(call.Pos(), note.Pos) { + expectations[call] = note + break // first match is good enough. + } + } + } + return expectations +} + // TestInstructionString tests serializing instructions via Instruction.String(). func TestInstructionString(t *testing.T) { if !typeparams.Enabled { diff --git a/go/ssa/builder_go122_test.go b/go/ssa/builder_go122_test.go index 21930d649a1..a57e6a5ba24 100644 --- a/go/ssa/builder_go122_test.go +++ b/go/ssa/builder_go122_test.go @@ -8,14 +8,17 @@ package ssa_test import ( + "fmt" "go/ast" "go/parser" "go/token" "go/types" "testing" + "golang.org/x/tools/go/expect" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" ) func TestMultipleGoversions(t *testing.T) { @@ -61,7 +64,7 @@ func TestMultipleGoversions(t *testing.T) { conf := &types.Config{Importer: nil, GoVersion: "go1.21"} p, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Fatalf("unexpected error: %v", err) } fns := ssautil.AllFunctions(p.Prog) @@ -79,10 +82,102 @@ func TestMultipleGoversions(t *testing.T) { t.Fatalf("Could not find function named %q in package %s", item.name, p) } if fn.Synthetic != item.wantSyn { - t.Errorf("Function %q.Syntethic=%q. expected %q", fn, fn.Synthetic, item.wantSyn) + t.Errorf("Function %q.Synthetic=%q. expected %q", fn, fn.Synthetic, item.wantSyn) } if got := fset.Position(fn.Pos()).String(); got != item.wantPos { t.Errorf("Function %q.Pos()=%q. expected %q", fn, got, item.wantPos) } } } + +const rangeOverIntSrc = ` +package p + +type I uint8 + +func noKey(x int) { + for range x { + // does not crash + } +} + +func untypedConstantOperand() { + for i := range 10 { + print(i) /*@ types("int")*/ + } +} + +func unsignedOperand(x uint64) { + for i := range x { + print(i) /*@ types("uint64")*/ + } +} + +func namedOperand(x I) { + for i := range x { + print(i) /*@ types("p.I")*/ + } +} + +func typeparamOperand[T int](x T) { + for i := range x { + print(i) /*@ types("T")*/ + } +} + +func assignment(x I) { + var k I + for k = range x { + print(k) /*@ types("p.I")*/ + } +} +` + +// TestRangeOverInt tests that, in a range-over-int (#61405), +// the type of each range var v (identified by print(v) calls) +// has the expected type. +func TestRangeOverInt(t *testing.T) { + testenv.NeedsGoExperiment(t, "range") + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", rangeOverIntSrc, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + + pkg := types.NewPackage("p", "") + conf := &types.Config{} + p, _, err := ssautil.BuildPackage(conf, fset, pkg, []*ast.File{f}, ssa.SanityCheckFunctions) + if err != nil { + t.Fatal(err) + } + + // Collect all notes in f, i.e. comments starting with "//@ types". + notes, err := expect.ExtractGo(fset, f) + if err != nil { + t.Fatal(err) + } + + // Collect calls to the built-in print function. + probes := callsTo(p, "print") + expectations := matchNotes(fset, notes, probes) + + for call := range probes { + if expectations[call] == nil { + t.Errorf("Unmatched call: %v @ %s", call, fset.Position(call.Pos())) + } + } + + // Check each expectation. + for call, note := range expectations { + var args []string + for _, a := range call.Args { + args = append(args, a.Type().String()) + } + if got, want := fmt.Sprint(args), fmt.Sprint(note.Args); got != want { + at := fset.Position(call.Pos()) + t.Errorf("%s: arguments to print had types %s, want %s", at, got, want) + logFunction(t, probes[call]) + } + } +} diff --git a/go/ssa/interp/interp_go122_test.go b/go/ssa/interp/interp_go122_test.go new file mode 100644 index 00000000000..3d808df97b7 --- /dev/null +++ b/go/ssa/interp/interp_go122_test.go @@ -0,0 +1,31 @@ +// 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 interp_test + +// Utilities from interp_test.go require go1.19. + +import ( + "log" + "os" + "path/filepath" + "testing" + + "golang.org/x/tools/internal/testenv" +) + +// TestExperimentRange tests files in testdata with GOEXPERIMENT=range set. +func TestExperimentRange(t *testing.T) { + testenv.NeedsGoExperiment(t, "range") + + // TODO: Is cwd actually needed here? + goroot := makeGoroot(t) + cwd, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + run(t, filepath.Join(cwd, "testdata", "rangeoverint.go"), goroot) +} diff --git a/go/ssa/interp/testdata/rangeoverint.go b/go/ssa/interp/testdata/rangeoverint.go new file mode 100644 index 00000000000..9a02d829764 --- /dev/null +++ b/go/ssa/interp/testdata/rangeoverint.go @@ -0,0 +1,86 @@ +package main + +// Range over integers. + +// Currently requires 1.22 and GOEXPERIMENT=range. + +import "fmt" + +func f() { + s := "AB" + for range 5 { + s += s + } + if s != "ABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABAB" { + panic(s) + } + + var t []int + for i := range 10 { + t = append(t, i) + } + if got, want := fmt.Sprint(t), "[0 1 2 3 4 5 6 7 8 9]"; got != want { + panic(got) + } + + var u []uint + for i := range uint(3) { + u = append(u, i) + } + if got, want := fmt.Sprint(u), "[0 1 2]"; got != want { + panic(got) + } + + for i := range 0 { + panic(i) + } + + for i := range int(-1) { + panic(i) + } + + for _, test := range []struct { + x int + b, c bool + want string + }{ + {-1, false, false, "[-123 -123]"}, + {0, false, false, "[-123 -123]"}, + {1, false, false, "[-123 0 333 333]"}, + {2, false, false, "[-123 0 333 1 333 333]"}, + {2, false, true, "[-123 0 222 1 222 222]"}, + {2, true, false, "[-123 0 111 111]"}, + {3, false, false, "[-123 0 333 1 333 2 333 333]"}, + } { + got := fmt.Sprint(valueSequence(test.x, test.b, test.c)) + if got != test.want { + panic(fmt.Sprint(test, got)) + } + } +} + +// valueSequence returns a sequence of the values of i. +// b causes an early break and c causes a continue. +func valueSequence(x int, b, c bool) []int { + var vals []int + var i int = -123 + vals = append(vals, i) + for i = range x { + vals = append(vals, i) + if b { + i = 111 + vals = append(vals, i) + break + } else if c { + i = 222 + vals = append(vals, i) + continue + } + i = 333 + vals = append(vals, i) + } + vals = append(vals, i) + return vals +} + +func main() { f() } diff --git a/internal/testenv/testenv.go b/internal/testenv/testenv.go index 4d29ebe7f72..88de3da05d2 100644 --- a/internal/testenv/testenv.go +++ b/internal/testenv/testenv.go @@ -447,3 +447,32 @@ func NeedsLocalXTools(t testing.TB) { t.Skipf("skipping test: %s module path is %q, not %q", modFilePath, modulePath, want) } } + +// NeedsGoExperiment skips t if the current process environment does not +// have a GOEXPERIMENT flag set. +func NeedsGoExperiment(t testing.TB, flag string) { + t.Helper() + + goexp := os.Getenv("GOEXPERIMENT") + set := false + for _, f := range strings.Split(goexp, ",") { + if f == "" { + continue + } + if f == "none" { + // GOEXPERIMENT=none disables all experiment flags. + set = false + break + } + val := true + if strings.HasPrefix(f, "no") { + f, val = f[2:], false + } + if f == flag { + set = val + } + } + if !set { + t.Skipf("skipping test: flag %q is not set in GOEXPERIMENT=%q", flag, goexp) + } +} From 99bbd3c91b3cd24de502fa0805f5802666cb2096 Mon Sep 17 00:00:00 2001 From: Tim King Date: Tue, 17 Oct 2023 10:37:47 -0700 Subject: [PATCH 033/100] go/callgraph/vta: use core type for struct fields Fixes golang/go#63146 Change-Id: Iafcd171b727ecf8bab04fdd2dbc06823574050df Reviewed-on: https://go-review.googlesource.com/c/tools/+/536035 LUCI-TryBot-Result: Go LUCI Run-TryBot: Tim King TryBot-Result: Gopher Robot Reviewed-by: Zvonimir Pavlinovic --- go/callgraph/vta/graph.go | 6 ++--- go/callgraph/vta/testdata/src/issue63146.go | 26 +++++++++++++++++++++ go/callgraph/vta/vta_test.go | 1 + 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 go/callgraph/vta/testdata/src/issue63146.go diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index 2537123f4c4..4d1d5254c6e 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -106,12 +106,12 @@ type field struct { } func (f field) Type() types.Type { - s := f.StructType.Underlying().(*types.Struct) + s := typeparams.CoreType(f.StructType).(*types.Struct) return s.Field(f.index).Type() } func (f field) String() string { - s := f.StructType.Underlying().(*types.Struct) + s := typeparams.CoreType(f.StructType).(*types.Struct) return fmt.Sprintf("Field(%v:%s)", f.StructType, s.Field(f.index).Name()) } @@ -434,7 +434,7 @@ func (b *builder) field(f *ssa.Field) { } func (b *builder) fieldAddr(f *ssa.FieldAddr) { - t := f.X.Type().Underlying().(*types.Pointer).Elem() + t := typeparams.CoreType(f.X.Type()).(*types.Pointer).Elem() // Since we are getting pointer to a field, make a bidirectional edge. fnode := field{StructType: t, index: f.Field} diff --git a/go/callgraph/vta/testdata/src/issue63146.go b/go/callgraph/vta/testdata/src/issue63146.go new file mode 100644 index 00000000000..6c809c4a608 --- /dev/null +++ b/go/callgraph/vta/testdata/src/issue63146.go @@ -0,0 +1,26 @@ +package test + +type embedded struct{} + +type S struct{ embedded } + +func (_ S) M() {} + +type C interface { + M() + S +} + +func G[T C]() { + t := T{embedded{}} + t.M() +} + +func F() { + G[S]() +} + +// WANT: +// F: G[testdata.S]() -> G[testdata.S] +// G[testdata.S]: (S).M(t2) -> S.M +// S.M: (testdata.S).M(t1) -> S.M diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go index 47962e3c531..69f218172a1 100644 --- a/go/callgraph/vta/vta_test.go +++ b/go/callgraph/vta/vta_test.go @@ -127,6 +127,7 @@ func TestVTACallGraphGenerics(t *testing.T) { files := []string{ "testdata/src/arrays_generics.go", "testdata/src/callgraph_generics.go", + "testdata/src/issue63146.go", } for _, file := range files { t.Run(file, func(t *testing.T) { From cdf1b5eb510057060188b56aec5b1f20d7f85703 Mon Sep 17 00:00:00 2001 From: Ian Lance Taylor Date: Tue, 17 Oct 2023 17:45:44 -0700 Subject: [PATCH 034/100] cmd/present: drop NaCl reference from docs Change-Id: Ic0b04040ab869c69898a0b7b7b17d1fe624aa4cc Reviewed-on: https://go-review.googlesource.com/c/tools/+/536135 LUCI-TryBot-Result: Go LUCI Reviewed-by: Cuong Manh Le Reviewed-by: Than McIntosh Auto-Submit: Ian Lance Taylor Reviewed-by: Ian Lance Taylor --- cmd/present/doc.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/present/doc.go b/cmd/present/doc.go index 654553507a9..a5065f06f10 100644 --- a/cmd/present/doc.go +++ b/cmd/present/doc.go @@ -8,9 +8,6 @@ presents slide and article files from the current directory. It may be run as a stand-alone command or an App Engine app. -The setup of the Go version of NaCl is documented at: -https://golang.org/wiki/NativeClient - To use with App Engine, copy the files in the tools/cmd/present directory to the root of your application and create an app.yaml file similar to this: From 71f6a46884ab564cd242dadf6fa16f76fe0517dd Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Tue, 17 Oct 2023 13:55:35 -0400 Subject: [PATCH 035/100] cmd/bundle: drop old +build lines The +build to go:build syntax conversion has progressed enough that it's fine to write only the new syntax now. All supported (and many unsupported) Go releases understand it. For golang/go#41184. For golang/go#60268. Change-Id: I8c1600577a21f4c7c89123302ca976d881a69841 Reviewed-on: https://go-review.googlesource.com/c/tools/+/536075 Auto-Submit: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Reviewed-by: Dmitri Shuralyov --- cmd/bundle/main.go | 1 - cmd/bundle/testdata/out.golden | 1 - 2 files changed, 2 deletions(-) diff --git a/cmd/bundle/main.go b/cmd/bundle/main.go index a5c426d8f88..fa73eb83a0a 100644 --- a/cmd/bundle/main.go +++ b/cmd/bundle/main.go @@ -228,7 +228,6 @@ func bundle(src, dst, dstpkg, prefix, buildTags string) ([]byte, error) { var out bytes.Buffer if buildTags != "" { fmt.Fprintf(&out, "//go:build %s\n", buildTags) - fmt.Fprintf(&out, "// +build %s\n\n", buildTags) } fmt.Fprintf(&out, "// Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT.\n") diff --git a/cmd/bundle/testdata/out.golden b/cmd/bundle/testdata/out.golden index a8f0cfeb280..c6f536e643e 100644 --- a/cmd/bundle/testdata/out.golden +++ b/cmd/bundle/testdata/out.golden @@ -1,5 +1,4 @@ //go:build tag -// +build tag // Code generated by golang.org/x/tools/cmd/bundle. DO NOT EDIT. // $ bundle From 6360c0b192333f8823d04b62e7cc83d56d03ea9c Mon Sep 17 00:00:00 2001 From: Viktor Blomqvist Date: Sun, 15 Oct 2023 19:52:30 +0200 Subject: [PATCH 036/100] gopls/internal/lsp/source: find linkname directives without parsing Avoid full source code parsing when looking for linkname directives to hover over or go-to-defintion from. Change-Id: I348a907fea3dcb15430095f5207bc3eaed28f6b6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/535516 Reviewed-by: Benny Siegert Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/source/definition.go | 2 +- gopls/internal/lsp/source/hover.go | 2 +- gopls/internal/lsp/source/linkname.go | 59 ++++++++++--------------- 3 files changed, 25 insertions(+), 38 deletions(-) diff --git a/gopls/internal/lsp/source/definition.go b/gopls/internal/lsp/source/definition.go index dd3feda70a2..60f101899b1 100644 --- a/gopls/internal/lsp/source/definition.go +++ b/gopls/internal/lsp/source/definition.go @@ -60,7 +60,7 @@ func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position } // Handle the case where the cursor is in a linkname directive. - locations, err := LinknameDefinition(ctx, snapshot, fh, position) + locations, err := LinknameDefinition(ctx, snapshot, pgf.Mapper, position) if !errors.Is(err, ErrNoLinkname) { return locations, err } diff --git a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go index 95317833489..3c58074aff0 100644 --- a/gopls/internal/lsp/source/hover.go +++ b/gopls/internal/lsp/source/hover.go @@ -131,7 +131,7 @@ func hover(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Po // Handle linkname directive by overriding what to look for. var linkedRange *protocol.Range // range referenced by linkname directive, or nil - if pkgPath, name, offset := parseLinkname(ctx, snapshot, fh, pp); pkgPath != "" && name != "" { + if pkgPath, name, offset := parseLinkname(pgf.Mapper, pp); pkgPath != "" && name != "" { // rng covering 2nd linkname argument: pkgPath.name. rng, err := pgf.PosRange(pgf.Tok.Pos(offset), pgf.Tok.Pos(offset+len(pkgPath)+len(".")+len(name))) if err != nil { diff --git a/gopls/internal/lsp/source/linkname.go b/gopls/internal/lsp/source/linkname.go index 84890a6e3ea..5a727e5c194 100644 --- a/gopls/internal/lsp/source/linkname.go +++ b/gopls/internal/lsp/source/linkname.go @@ -21,10 +21,10 @@ import ( // As such it indicates that other definitions could be worth checking. var ErrNoLinkname = errors.New("no linkname directive found") -// LinknameDefinition finds the definition of the linkname directive in fh at pos. +// LinknameDefinition finds the definition of the linkname directive in m at pos. // If there is no linkname directive at pos, returns ErrNoLinkname. -func LinknameDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, from protocol.Position) ([]protocol.Location, error) { - pkgPath, name, _ := parseLinkname(ctx, snapshot, fh, from) +func LinknameDefinition(ctx context.Context, snapshot Snapshot, m *protocol.Mapper, from protocol.Position) ([]protocol.Location, error) { + pkgPath, name, _ := parseLinkname(m, from) if pkgPath == "" { return nil, ErrNoLinkname } @@ -44,27 +44,34 @@ func LinknameDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, f // If successful, it returns // - package path referenced // - object name referenced -// - byte offset in fh of the start of the link target +// - byte offset in mapped file of the start of the link target // of the linkname directives 2nd argument. // // If the position is not in the second argument of a go:linkname directive, // or parsing fails, it returns "", "", 0. -func parseLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) (pkgPath, name string, targetOffset int) { - // TODO(adonovan): opt: parsing isn't necessary here. - // We're only looking for a line comment. - pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) +func parseLinkname(m *protocol.Mapper, pos protocol.Position) (pkgPath, name string, targetOffset int) { + lineStart, err := m.PositionOffset(protocol.Position{Line: pos.Line, Character: 0}) if err != nil { return "", "", 0 } - - offset, err := pgf.Mapper.PositionOffset(pos) + lineEnd, err := m.PositionOffset(protocol.Position{Line: pos.Line + 1, Character: 0}) if err != nil { return "", "", 0 } + directive := string(m.Content[lineStart:lineEnd]) + // (Assumes no leading spaces.) + if !strings.HasPrefix(directive, "//go:linkname") { + return "", "", 0 + } + // Sometimes source code (typically tests) has another + // comment after the directive, trim that away. + if i := strings.LastIndex(directive, "//"); i != 0 { + directive = strings.TrimSpace(directive[:i]) + } + // Looking for pkgpath in '//go:linkname f pkgpath.g'. // (We ignore 1-arg linkname directives.) - directive, end := findLinknameAtOffset(pgf, offset) parts := strings.Fields(directive) if len(parts) != 3 { return "", "", 0 @@ -72,6 +79,11 @@ func parseLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr // Inside 2nd arg [start, end]? // (Assumes no trailing spaces.) + offset, err := m.PositionOffset(pos) + if err != nil { + return "", "", 0 + } + end := lineStart + len(directive) start := end - len(parts[2]) if !(start <= offset && offset <= end) { return "", "", 0 @@ -87,31 +99,6 @@ func parseLinkname(ctx context.Context, snapshot Snapshot, fh FileHandle, pos pr return linkname[:dot], linkname[dot+1:], start } -// findLinknameAtOffset returns the first linkname directive on line and its end offset. -// Returns "", 0 if the offset is not in a linkname directive. -func findLinknameAtOffset(pgf *ParsedGoFile, offset int) (string, int) { - for _, grp := range pgf.File.Comments { - for _, com := range grp.List { - if strings.HasPrefix(com.Text, "//go:linkname") { - p := safetoken.Position(pgf.Tok, com.Pos()) - - // Sometimes source code (typically tests) has another - // comment after the directive, trim that away. - text := com.Text - if i := strings.LastIndex(text, "//"); i != 0 { - text = strings.TrimSpace(text[:i]) - } - - end := p.Offset + len(text) - if p.Offset <= offset && offset < end { - return text, end - } - } - } - } - return "", 0 -} - // 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 Snapshot, pkgPath PackagePath, name string) (Package, *ParsedGoFile, token.Pos, error) { From 5185da162248158fcdd2058c0f9f82a5e4b97620 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 20 Oct 2023 17:52:42 -0400 Subject: [PATCH 037/100] internal/refactor/inline: avoid redundant import names added by inlining When analyzing callee objects, keep track of their package names so that we don't have to assign explicit names to new imports when there is no conflict with their implicit name. Fixes golang/go#63613 Change-Id: I5b8dc9c514e434fb4262cab321933a28729bbd76 Reviewed-on: https://go-review.googlesource.com/c/tools/+/536755 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- .../codeaction/removeparam_imports.txt | 10 +-- internal/refactor/inline/callee.go | 18 ++++-- internal/refactor/inline/inline.go | 61 +++++++++++-------- .../refactor/inline/testdata/crosspkg.txtar | 31 +++++++++- .../refactor/inline/testdata/dotimport.txtar | 2 +- .../refactor/inline/testdata/issue63298.txtar | 2 +- .../inline/testdata/revdotimport.txtar | 2 +- 7 files changed, 84 insertions(+), 42 deletions(-) diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt index d616fe21769..1a483d2525c 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt @@ -85,7 +85,7 @@ package a import ( "mod.test/b" - c "mod.test/c" + "mod.test/c" ) func _() { @@ -97,7 +97,7 @@ package a import ( "mod.test/b" - c "mod.test/c" + "mod.test/c" ) func _() { @@ -111,7 +111,7 @@ package a import ( "mod.test/b" - c "mod.test/c" + "mod.test/c" ) func _() { @@ -129,9 +129,9 @@ package a // TODO(rfindley/adonovan): inlining here adds an additional import of // mod.test/b. Can we do better? import ( + "mod.test/b" . "mod.test/b" - b "mod.test/b" - c "mod.test/c" + "mod.test/c" ) func _() { diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go index dc74eab4e1a..c9a7ea0c8f2 100644 --- a/internal/refactor/inline/callee.go +++ b/internal/refactor/inline/callee.go @@ -59,9 +59,12 @@ type freeRef struct { // An object abstracts a free types.Object referenced by the callee. Gob-serializable. type object struct { - Name string // Object.Name() - Kind string // one of {var,func,const,type,pkgname,nil,builtin} - PkgPath string // pkgpath of object (or of imported package if kind="pkgname") + Name string // Object.Name() + Kind string // one of {var,func,const,type,pkgname,nil,builtin} + PkgPath string // path of object's package (or imported package if kind="pkgname") + PkgName string // name of object's package (or imported package if kind="pkgname") + // TODO(rfindley): should we also track LocalPkgName here? Do we want to + // preserve the local package name? ValidPos bool // Object.Pos().IsValid() Shadow map[string]bool // names shadowed at one of the object's refs } @@ -192,15 +195,18 @@ func AnalyzeCallee(logf func(string, ...any), fset *token.FileSet, pkg *types.Pa objidx, ok := freeObjIndex[obj] if !ok { objidx = len(freeObjIndex) - var pkgpath string - if pkgname, ok := obj.(*types.PkgName); ok { - pkgpath = pkgname.Imported().Path() + var pkgpath, pkgname string + if pn, ok := obj.(*types.PkgName); ok { + pkgpath = pn.Imported().Path() + pkgname = pn.Imported().Name() } else if obj.Pkg() != nil { pkgpath = obj.Pkg().Path() + pkgname = obj.Pkg().Name() } freeObjs = append(freeObjs, object{ Name: obj.Name(), Kind: objectKind(obj), + PkgName: pkgname, PkgPath: pkgpath, ValidPos: obj.Pos().IsValid(), }) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index be161610882..06f64013c79 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -222,13 +222,13 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte, importDecl = &ast.GenDecl{Tok: token.IMPORT} f.Decls = prepend[ast.Decl](importDecl, f.Decls...) } - for _, spec := range res.newImports { + for _, imp := range res.newImports { // Check that the new imports are accessible. - path, _ := strconv.Unquote(spec.Path.Value) + path, _ := strconv.Unquote(imp.spec.Path.Value) if !canImport(caller.Types.Path(), path) { return nil, fmt.Errorf("can't inline function %v as its body refers to inaccessible package %q", callee, path) } - importDecl.Specs = append(importDecl.Specs, spec) + importDecl.Specs = append(importDecl.Specs, imp.spec) } } @@ -300,8 +300,13 @@ func Inline(logf func(string, ...any), caller *Caller, callee *Callee) ([]byte, return newSrc, nil } +type newImport struct { + pkgName string + spec *ast.ImportSpec +} + type result struct { - newImports []*ast.ImportSpec + newImports []newImport old, new ast.Node // e.g. replace call expr by callee function body expression } @@ -387,14 +392,14 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu } // localImportName returns the local name for a given imported package path. - var newImports []*ast.ImportSpec - localImportName := func(path string, shadows map[string]bool) string { + var newImports []newImport + localImportName := func(obj *object) string { // Does an import exist? - for _, name := range importMap[path] { + for _, name := range importMap[obj.PkgPath] { // Check that either the import preexisted, // or that it was newly added (no PkgName) but is not shadowed, // either in the callee (shadows) or caller (caller.lookup). - if !shadows[name] { + if !obj.Shadow[name] { found := caller.lookup(name) if is[*types.PkgName](found) || found == nil { return name @@ -404,7 +409,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu newlyAdded := func(name string) bool { for _, new := range newImports { - if new.Name.Name == name { + if new.pkgName == name { return true } } @@ -419,29 +424,32 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu // // "init" is not a legal PkgName. // - // TODO(adonovan): preserve the PkgName used - // in the original source, or, for a dot import, - // use the package's declared name. - base := pathpkg.Base(path) + // TODO(rfindley): is it worth preserving local package names for callee + // imports? Are they likely to be better or worse than the name we choose + // here? + base := obj.PkgName name := base - for n := 0; shadows[name] || caller.lookup(name) != nil || newlyAdded(name) || name == "init"; n++ { + for n := 0; obj.Shadow[name] || caller.lookup(name) != nil || newlyAdded(name) || name == "init"; n++ { name = fmt.Sprintf("%s%d", base, n) } - // TODO(adonovan): don't use a renaming import - // unless the local name differs from either - // the package name or the last segment of path. - // This requires that we tabulate (path, declared name, local name) - // triples for each package referenced by the callee. - logf("adding import %s %q", name, path) - newImports = append(newImports, &ast.ImportSpec{ - Name: makeIdent(name), + logf("adding import %s %q", name, obj.PkgPath) + spec := &ast.ImportSpec{ Path: &ast.BasicLit{ Kind: token.STRING, - Value: strconv.Quote(path), + Value: strconv.Quote(obj.PkgPath), }, + } + // Use explicit pkgname (out of necessity) when it differs from the declared name, + // or (for good style) when it differs from base(pkgpath). + if name != obj.PkgName || name != pathpkg.Base(obj.PkgPath) { + spec.Name = makeIdent(name) + } + newImports = append(newImports, newImport{ + pkgName: name, + spec: spec, }) - importMap[path] = append(importMap[path], name) + importMap[obj.PkgPath] = append(importMap[obj.PkgPath], name) return name } @@ -471,8 +479,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu var newName ast.Expr if obj.Kind == "pkgname" { // Use locally appropriate import, creating as needed. - newName = makeIdent(localImportName(obj.PkgPath, obj.Shadow)) // imported package - + newName = makeIdent(localImportName(&obj)) // imported package } else if !obj.ValidPos { // Built-in function, type, or value (e.g. nil, zero): // check not shadowed at caller. @@ -515,7 +522,7 @@ func inline(logf func(string, ...any), caller *Caller, callee *gobCallee) (*resu // Form a qualified identifier, pkg.Name. if qualify { - pkgName := localImportName(obj.PkgPath, obj.Shadow) + pkgName := localImportName(&obj) newName = &ast.SelectorExpr{ X: makeIdent(pkgName), Sel: makeIdent(obj.Name), diff --git a/internal/refactor/inline/testdata/crosspkg.txtar b/internal/refactor/inline/testdata/crosspkg.txtar index 7c0704be819..e0744f99043 100644 --- a/internal/refactor/inline/testdata/crosspkg.txtar +++ b/internal/refactor/inline/testdata/crosspkg.txtar @@ -22,22 +22,30 @@ func A() { fmt.Println() b.B1() //@ inline(re"B1", b1result) b.B2() //@ inline(re"B2", b2result) + b.B3() //@ inline(re"B3", b3result) } -- b/b.go -- package b import "testdata/c" +import "testdata/d" import "fmt" func B1() { c.C() } func B2() { fmt.Println() } +func B3() { e.E() } // (note that "testdata/d" points to package e) -- c/c.go -- package c func C() {} +-- d/d.go -- +package e // <- this package name intentionally mismatches the path + +func E() {} + -- b1result -- package a @@ -46,7 +54,7 @@ package a import ( "fmt" "testdata/b" - c "testdata/c" + "testdata/c" ) // Nor this one. @@ -55,6 +63,7 @@ func A() { fmt.Println() c.C() //@ inline(re"B1", b1result) b.B2() //@ inline(re"B2", b2result) + b.B3() //@ inline(re"B3", b3result) } -- b2result -- @@ -73,4 +82,24 @@ func A() { fmt.Println() b.B1() //@ inline(re"B1", b1result) fmt.Println() //@ inline(re"B2", b2result) + b.B3() //@ inline(re"B3", b3result) +} +-- b3result -- +package a + +// This comment does not migrate. + +import ( + "fmt" + "testdata/b" + e "testdata/d" +) + +// Nor this one. + +func A() { + fmt.Println() + b.B1() //@ inline(re"B1", b1result) + b.B2() //@ inline(re"B2", b2result) + e.E() //@ inline(re"B3", b3result) } diff --git a/internal/refactor/inline/testdata/dotimport.txtar b/internal/refactor/inline/testdata/dotimport.txtar index 8ca5f05cda7..644398b1df0 100644 --- a/internal/refactor/inline/testdata/dotimport.txtar +++ b/internal/refactor/inline/testdata/dotimport.txtar @@ -29,7 +29,7 @@ func _() { package c import ( - a "testdata/a" + "testdata/a" ) func _() { diff --git a/internal/refactor/inline/testdata/issue63298.txtar b/internal/refactor/inline/testdata/issue63298.txtar index 990ebcd879a..cc556c90ecd 100644 --- a/internal/refactor/inline/testdata/issue63298.txtar +++ b/internal/refactor/inline/testdata/issue63298.txtar @@ -38,7 +38,7 @@ func B() {} package a import ( - b "testdata/b" + "testdata/b" b0 "testdata/another/b" //@ inline(re"a2", result) diff --git a/internal/refactor/inline/testdata/revdotimport.txtar b/internal/refactor/inline/testdata/revdotimport.txtar index 3838793754d..f33304f9da3 100644 --- a/internal/refactor/inline/testdata/revdotimport.txtar +++ b/internal/refactor/inline/testdata/revdotimport.txtar @@ -32,8 +32,8 @@ func _() { package c import ( + "testdata/a" . "testdata/a" - a "testdata/a" ) func _() { From 02048e6c24c4b42259768e08e3fad4fc9e2882d3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 23 Oct 2023 12:43:59 -0400 Subject: [PATCH 038/100] go/packages: document that types.Sizes may be nil This is not a behavior change. Change-Id: Ia70c01ff0893b2274b44fb0f2043dbcb6afb71c8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537116 Reviewed-by: David Chase Reviewed-by: Cuong Manh Le LUCI-TryBot-Result: Go LUCI --- go/packages/packages.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/go/packages/packages.go b/go/packages/packages.go index ece0e7c603e..3b6e58a5056 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -373,6 +373,7 @@ type Package struct { TypesInfo *types.Info // TypesSizes provides the effective size function for types in TypesInfo. + // It may be nil if, for example, the compiler/architecture pair is not known. TypesSizes types.Sizes // forTest is the package under test, if any. @@ -553,7 +554,7 @@ type loaderPackage struct { type loader struct { pkgs map[string]*loaderPackage Config - sizes types.Sizes + sizes types.Sizes // nil => unknown parseCache map[string]*parseValue parseCacheMu sync.Mutex exportMu sync.Mutex // enforces mutual exclusion of exportdata operations @@ -1042,7 +1043,7 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { IgnoreFuncBodies: ld.Mode&NeedDeps == 0 && !lpkg.initial, Error: appendError, - Sizes: ld.sizes, + Sizes: ld.sizes, // may be nil } if lpkg.Module != nil && lpkg.Module.GoVersion != "" { typesinternal.SetGoVersion(tc, "go"+lpkg.Module.GoVersion) From 5ab57de36f1d218bc840744dcfb6f16def8fb25e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 23 Oct 2023 18:04:15 -0400 Subject: [PATCH 039/100] go/packages: ensure that types.Sizes is correct This change ensures that types.Sizes information can be computed from compiler+GOARCH if it is needed; if not, it causes the Load to fail. This fixes a crash in gopls whereby a bad GOARCH causes the types.Sizes to be silently nil, in violation of the Packages.TypesSizes contract. Gopls would then dereference this nil. The problem only manifests with a file=foo.go query, as this suppresses go list's usual eager check for valid GOOS/GOARCH during its build tag computation. gopls relies on the file=... mode as a fall back. Note that this change reverts my earlier doc change today that allowed TypesSizes to be nil if unknown. This was the wrong fix, as it creates both a nil-dereference hazard (the original bug), plus, if the nil Sizes is fed to go/types.Config, it would trigger the default (wrong) size computation. (This is less significant to gopls because in file=... mode, size errors are the least of your type-error worries, but still...) Plus, a test. Also: simplify two trivially true conditions in the packages.Load control flow. Fixes golang/go#63701 Fixes golang/vscode-go#3021 Change-Id: I8d519d0d8a8c7bce6b3206a8116a150d37e74e45 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537118 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- go/packages/packages.go | 80 +++++++++++++++++--------------- go/packages/packages_test.go | 28 +++++++++++ gopls/internal/lsp/cache/load.go | 4 ++ 3 files changed, 75 insertions(+), 37 deletions(-) diff --git a/go/packages/packages.go b/go/packages/packages.go index 3b6e58a5056..40af0adc461 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -258,13 +258,21 @@ type driverResponse struct { // proceeding with further analysis. The PrintErrors function is // provided for convenient display of all errors. func Load(cfg *Config, patterns ...string) ([]*Package, error) { - l := newLoader(cfg) - response, err := defaultDriver(&l.Config, patterns...) + ld := newLoader(cfg) + response, err := defaultDriver(&ld.Config, patterns...) if err != nil { return nil, err } - l.sizes = types.SizesFor(response.Compiler, response.Arch) - return l.refine(response) + + // If type size information is needed but unavailable. + // reject the whole Load since the error is the same for every package. + ld.sizes = types.SizesFor(response.Compiler, response.Arch) + if ld.sizes == nil && ld.Config.Mode&(NeedTypes|NeedTypesSizes|NeedTypesInfo) != 0 { + return nil, fmt.Errorf("can't determine type sizes for compiler %q on GOARCH %q", + response.Compiler, response.Arch) + } + + return ld.refine(response) } // defaultDriver is a driver that implements go/packages' fallback behavior. @@ -373,7 +381,6 @@ type Package struct { TypesInfo *types.Info // TypesSizes provides the effective size function for types in TypesInfo. - // It may be nil if, for example, the compiler/architecture pair is not known. TypesSizes types.Sizes // forTest is the package under test, if any. @@ -554,7 +561,7 @@ type loaderPackage struct { type loader struct { pkgs map[string]*loaderPackage Config - sizes types.Sizes // nil => unknown + sizes types.Sizes // non-nil if needed by mode parseCache map[string]*parseValue parseCacheMu sync.Mutex exportMu sync.Mutex // enforces mutual exclusion of exportdata operations @@ -679,7 +686,7 @@ func (ld *loader) refine(response *driverResponse) ([]*Package, error) { } } - // Materialize the import graph. + // Materialize the import graph (if NeedImports). const ( white = 0 // new @@ -697,9 +704,8 @@ func (ld *loader) refine(response *driverResponse) ([]*Package, error) { // visit returns whether the package needs src or has a transitive // dependency on a package that does. These are the only packages // for which we load source code. - var stack []*loaderPackage + var stack, srcPkgs []*loaderPackage var visit func(lpkg *loaderPackage) bool - var srcPkgs []*loaderPackage visit = func(lpkg *loaderPackage) bool { switch lpkg.color { case black: @@ -710,35 +716,34 @@ func (ld *loader) refine(response *driverResponse) ([]*Package, error) { lpkg.color = grey stack = append(stack, lpkg) // push stubs := lpkg.Imports // the structure form has only stubs with the ID in the Imports - // If NeedImports isn't set, the imports fields will all be zeroed out. - if ld.Mode&NeedImports != 0 { - lpkg.Imports = make(map[string]*Package, len(stubs)) - for importPath, ipkg := range stubs { - var importErr error - imp := ld.pkgs[ipkg.ID] - if imp == nil { - // (includes package "C" when DisableCgo) - importErr = fmt.Errorf("missing package: %q", ipkg.ID) - } else if imp.color == grey { - importErr = fmt.Errorf("import cycle: %s", stack) - } - if importErr != nil { - if lpkg.importErrors == nil { - lpkg.importErrors = make(map[string]error) - } - lpkg.importErrors[importPath] = importErr - continue + lpkg.Imports = make(map[string]*Package, len(stubs)) + for importPath, ipkg := range stubs { + var importErr error + imp := ld.pkgs[ipkg.ID] + if imp == nil { + // (includes package "C" when DisableCgo) + importErr = fmt.Errorf("missing package: %q", ipkg.ID) + } else if imp.color == grey { + importErr = fmt.Errorf("import cycle: %s", stack) + } + if importErr != nil { + if lpkg.importErrors == nil { + lpkg.importErrors = make(map[string]error) } + lpkg.importErrors[importPath] = importErr + continue + } - if visit(imp) { - lpkg.needsrc = true - } - lpkg.Imports[importPath] = imp.Package + if visit(imp) { + lpkg.needsrc = true } + lpkg.Imports[importPath] = imp.Package } if lpkg.needsrc { srcPkgs = append(srcPkgs, lpkg) } + // NeedTypeSizes causes TypeSizes to be set even + // on packages for which types aren't needed. if ld.Mode&NeedTypesSizes != 0 { lpkg.TypesSizes = ld.sizes } @@ -758,17 +763,18 @@ func (ld *loader) refine(response *driverResponse) ([]*Package, error) { for _, lpkg := range initial { visit(lpkg) } - } - if ld.Mode&NeedImports != 0 && ld.Mode&NeedTypes != 0 { - for _, lpkg := range srcPkgs { + + if ld.Mode&NeedTypes != 0 { // Complete type information is required for the // immediate dependencies of each source package. - for _, ipkg := range lpkg.Imports { - imp := ld.pkgs[ipkg.ID] - imp.needtypes = true + for _, lpkg := range srcPkgs { + for _, ipkg := range lpkg.Imports { + ld.pkgs[ipkg.ID].needtypes = true + } } } } + // Load type data and syntax if needed, starting at // the initial packages (roots of the import DAG). if ld.Mode&NeedTypes != 0 || ld.Mode&NeedSyntax != 0 { diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 60fdf9fbadd..4fb7f0bdcef 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -1217,6 +1217,34 @@ func testSizes(t *testing.T, exporter packagestest.Exporter) { } } +// This is a regression test for the root cause of +// github.com/golang/vscode-go/issues/3021. +// If types are needed (any of NeedTypes{,Info,Sizes} +// and the types.Sizes cannot be obtained (e.g. due to a bad GOARCH) +// then the Load operation must fail. It must not return a nil +// TypesSizes, or use the default (wrong) size. +// +// We use a file=... query because it suppresses the bad-GOARCH check +// that the go command would otherwise perform eagerly. +// (Gopls relies on this as a fallback.) +func TestNeedTypeSizesWithBadGOARCH(t *testing.T) { + testAllOrModulesParallel(t, func(t *testing.T, exporter packagestest.Exporter) { + exported := packagestest.Export(t, exporter, []packagestest.Module{{ + Name: "testdata", + Files: map[string]interface{}{"a/a.go": `package a`}}}) + defer exported.Cleanup() + + exported.Config.Mode = packages.NeedTypesSizes // or {,Info,Sizes} + exported.Config.Env = append(exported.Config.Env, "GOARCH=286") + _, err := packages.Load(exported.Config, "file=./a/a.go") + got := fmt.Sprint(err) + want := "can't determine type sizes" + if !strings.Contains(got, want) { + t.Errorf("Load error %q does not contain substring %q", got, want) + } + }) +} + // TestContainsFallbackSticks ensures that when there are both contains and non-contains queries // the decision whether to fallback to the pre-1.11 go list sticks across both sets of calls to // go list. diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 331e0e7ebc7..0ac4e5d9c06 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -413,6 +413,10 @@ func buildMetadata(updates map[PackageID]*source.Metadata, pkg *packages.Package return } + if pkg.TypesSizes == nil { + panic(id + ".TypeSizes is nil") + } + // Recreate the metadata rather than reusing it to avoid locking. m := &source.Metadata{ ID: id, From 6da1917f63062d776de512b0c12d76a785a7b64f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 23 Oct 2023 12:36:26 -0400 Subject: [PATCH 040/100] go/packages: remove pre-go1.16 overlay support Change-Id: Ia2540c34bee72ad10078e07045fa6dc575478fca Reviewed-on: https://go-review.googlesource.com/c/tools/+/537115 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/packages/golist.go | 74 ----- go/packages/golist_overlay.go | 492 ---------------------------------- 2 files changed, 566 deletions(-) diff --git a/go/packages/golist.go b/go/packages/golist.go index 1f1eade0ac8..c1292b30f3e 100644 --- a/go/packages/golist.go +++ b/go/packages/golist.go @@ -208,62 +208,6 @@ extractQueries: } } - // Only use go/packages' overlay processing if we're using a Go version - // below 1.16. Otherwise, go list handles it. - if goVersion, err := state.getGoVersion(); err == nil && goVersion < 16 { - modifiedPkgs, needPkgs, err := state.processGolistOverlay(response) - if err != nil { - return nil, err - } - - var containsCandidates []string - if len(containFiles) > 0 { - containsCandidates = append(containsCandidates, modifiedPkgs...) - containsCandidates = append(containsCandidates, needPkgs...) - } - if err := state.addNeededOverlayPackages(response, needPkgs); err != nil { - return nil, err - } - // Check candidate packages for containFiles. - if len(containFiles) > 0 { - for _, id := range containsCandidates { - pkg, ok := response.seenPackages[id] - if !ok { - response.addPackage(&Package{ - ID: id, - Errors: []Error{{ - Kind: ListError, - Msg: fmt.Sprintf("package %s expected but not seen", id), - }}, - }) - continue - } - for _, f := range containFiles { - for _, g := range pkg.GoFiles { - if sameFile(f, g) { - response.addRoot(id) - } - } - } - } - } - // Add root for any package that matches a pattern. This applies only to - // packages that are modified by overlays, since they are not added as - // roots automatically. - for _, pattern := range restPatterns { - match := matchPattern(pattern) - for _, pkgID := range modifiedPkgs { - pkg, ok := response.seenPackages[pkgID] - if !ok { - continue - } - if match(pkg.PkgPath) { - response.addRoot(pkg.ID) - } - } - } - } - sizeswg.Wait() if sizeserr != nil { return nil, sizeserr @@ -271,24 +215,6 @@ extractQueries: return response.dr, nil } -func (state *golistState) addNeededOverlayPackages(response *responseDeduper, pkgs []string) error { - if len(pkgs) == 0 { - return nil - } - dr, err := state.createDriverResponse(pkgs...) - if err != nil { - return err - } - for _, pkg := range dr.Packages { - response.addPackage(pkg) - } - _, needPkgs, err := state.processGolistOverlay(response) - if err != nil { - return err - } - return state.addNeededOverlayPackages(response, needPkgs) -} - func (state *golistState) runContainsQueries(response *responseDeduper, queries []string) error { for _, query := range queries { // TODO(matloob): Do only one query per directory. diff --git a/go/packages/golist_overlay.go b/go/packages/golist_overlay.go index 9576b472f9c..d823c474ad3 100644 --- a/go/packages/golist_overlay.go +++ b/go/packages/golist_overlay.go @@ -6,314 +6,11 @@ package packages import ( "encoding/json" - "fmt" - "go/parser" - "go/token" - "os" "path/filepath" - "regexp" - "sort" - "strconv" - "strings" "golang.org/x/tools/internal/gocommand" ) -// processGolistOverlay provides rudimentary support for adding -// files that don't exist on disk to an overlay. The results can be -// sometimes incorrect. -// TODO(matloob): Handle unsupported cases, including the following: -// - determining the correct package to add given a new import path -func (state *golistState) processGolistOverlay(response *responseDeduper) (modifiedPkgs, needPkgs []string, err error) { - havePkgs := make(map[string]string) // importPath -> non-test package ID - needPkgsSet := make(map[string]bool) - modifiedPkgsSet := make(map[string]bool) - - pkgOfDir := make(map[string][]*Package) - for _, pkg := range response.dr.Packages { - // This is an approximation of import path to id. This can be - // wrong for tests, vendored packages, and a number of other cases. - havePkgs[pkg.PkgPath] = pkg.ID - dir, err := commonDir(pkg.GoFiles) - if err != nil { - return nil, nil, err - } - if dir != "" { - pkgOfDir[dir] = append(pkgOfDir[dir], pkg) - } - } - - // If no new imports are added, it is safe to avoid loading any needPkgs. - // Otherwise, it's hard to tell which package is actually being loaded - // (due to vendoring) and whether any modified package will show up - // in the transitive set of dependencies (because new imports are added, - // potentially modifying the transitive set of dependencies). - var overlayAddsImports bool - - // If both a package and its test package are created by the overlay, we - // need the real package first. Process all non-test files before test - // files, and make the whole process deterministic while we're at it. - var overlayFiles []string - for opath := range state.cfg.Overlay { - overlayFiles = append(overlayFiles, opath) - } - sort.Slice(overlayFiles, func(i, j int) bool { - iTest := strings.HasSuffix(overlayFiles[i], "_test.go") - jTest := strings.HasSuffix(overlayFiles[j], "_test.go") - if iTest != jTest { - return !iTest // non-tests are before tests. - } - return overlayFiles[i] < overlayFiles[j] - }) - for _, opath := range overlayFiles { - contents := state.cfg.Overlay[opath] - base := filepath.Base(opath) - dir := filepath.Dir(opath) - var pkg *Package // if opath belongs to both a package and its test variant, this will be the test variant - var testVariantOf *Package // if opath is a test file, this is the package it is testing - var fileExists bool - isTestFile := strings.HasSuffix(opath, "_test.go") - pkgName, ok := extractPackageName(opath, contents) - if !ok { - // Don't bother adding a file that doesn't even have a parsable package statement - // to the overlay. - continue - } - // If all the overlay files belong to a different package, change the - // package name to that package. - maybeFixPackageName(pkgName, isTestFile, pkgOfDir[dir]) - nextPackage: - for _, p := range response.dr.Packages { - if pkgName != p.Name && p.ID != "command-line-arguments" { - continue - } - for _, f := range p.GoFiles { - if !sameFile(filepath.Dir(f), dir) { - continue - } - // Make sure to capture information on the package's test variant, if needed. - if isTestFile && !hasTestFiles(p) { - // TODO(matloob): Are there packages other than the 'production' variant - // of a package that this can match? This shouldn't match the test main package - // because the file is generated in another directory. - testVariantOf = p - continue nextPackage - } else if !isTestFile && hasTestFiles(p) { - // We're examining a test variant, but the overlaid file is - // a non-test file. Because the overlay implementation - // (currently) only adds a file to one package, skip this - // package, so that we can add the file to the production - // variant of the package. (https://golang.org/issue/36857 - // tracks handling overlays on both the production and test - // variant of a package). - continue nextPackage - } - if pkg != nil && p != pkg && pkg.PkgPath == p.PkgPath { - // We have already seen the production version of the - // for which p is a test variant. - if hasTestFiles(p) { - testVariantOf = pkg - } - } - pkg = p - if filepath.Base(f) == base { - fileExists = true - } - } - } - // The overlay could have included an entirely new package or an - // ad-hoc package. An ad-hoc package is one that we have manually - // constructed from inadequate `go list` results for a file= query. - // It will have the ID command-line-arguments. - if pkg == nil || pkg.ID == "command-line-arguments" { - // Try to find the module or gopath dir the file is contained in. - // Then for modules, add the module opath to the beginning. - pkgPath, ok, err := state.getPkgPath(dir) - if err != nil { - return nil, nil, err - } - if !ok { - break - } - var forTest string // only set for x tests - isXTest := strings.HasSuffix(pkgName, "_test") - if isXTest { - forTest = pkgPath - pkgPath += "_test" - } - id := pkgPath - if isTestFile { - if isXTest { - id = fmt.Sprintf("%s [%s.test]", pkgPath, forTest) - } else { - id = fmt.Sprintf("%s [%s.test]", pkgPath, pkgPath) - } - } - if pkg != nil { - // TODO(rstambler): We should change the package's path and ID - // here. The only issue is that this messes with the roots. - } else { - // Try to reclaim a package with the same ID, if it exists in the response. - for _, p := range response.dr.Packages { - if reclaimPackage(p, id, opath, contents) { - pkg = p - break - } - } - // Otherwise, create a new package. - if pkg == nil { - pkg = &Package{ - PkgPath: pkgPath, - ID: id, - Name: pkgName, - Imports: make(map[string]*Package), - } - response.addPackage(pkg) - havePkgs[pkg.PkgPath] = id - // Add the production package's sources for a test variant. - if isTestFile && !isXTest && testVariantOf != nil { - pkg.GoFiles = append(pkg.GoFiles, testVariantOf.GoFiles...) - pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, testVariantOf.CompiledGoFiles...) - // Add the package under test and its imports to the test variant. - pkg.forTest = testVariantOf.PkgPath - for k, v := range testVariantOf.Imports { - pkg.Imports[k] = &Package{ID: v.ID} - } - } - if isXTest { - pkg.forTest = forTest - } - } - } - } - if !fileExists { - pkg.GoFiles = append(pkg.GoFiles, opath) - // TODO(matloob): Adding the file to CompiledGoFiles can exhibit the wrong behavior - // if the file will be ignored due to its build tags. - pkg.CompiledGoFiles = append(pkg.CompiledGoFiles, opath) - modifiedPkgsSet[pkg.ID] = true - } - imports, err := extractImports(opath, contents) - if err != nil { - // Let the parser or type checker report errors later. - continue - } - for _, imp := range imports { - // TODO(rstambler): If the package is an x test and the import has - // a test variant, make sure to replace it. - if _, found := pkg.Imports[imp]; found { - continue - } - overlayAddsImports = true - id, ok := havePkgs[imp] - if !ok { - var err error - id, err = state.resolveImport(dir, imp) - if err != nil { - return nil, nil, err - } - } - pkg.Imports[imp] = &Package{ID: id} - // Add dependencies to the non-test variant version of this package as well. - if testVariantOf != nil { - testVariantOf.Imports[imp] = &Package{ID: id} - } - } - } - - // toPkgPath guesses the package path given the id. - toPkgPath := func(sourceDir, id string) (string, error) { - if i := strings.IndexByte(id, ' '); i >= 0 { - return state.resolveImport(sourceDir, id[:i]) - } - return state.resolveImport(sourceDir, id) - } - - // Now that new packages have been created, do another pass to determine - // the new set of missing packages. - for _, pkg := range response.dr.Packages { - for _, imp := range pkg.Imports { - if len(pkg.GoFiles) == 0 { - return nil, nil, fmt.Errorf("cannot resolve imports for package %q with no Go files", pkg.PkgPath) - } - pkgPath, err := toPkgPath(filepath.Dir(pkg.GoFiles[0]), imp.ID) - if err != nil { - return nil, nil, err - } - if _, ok := havePkgs[pkgPath]; !ok { - needPkgsSet[pkgPath] = true - } - } - } - - if overlayAddsImports { - needPkgs = make([]string, 0, len(needPkgsSet)) - for pkg := range needPkgsSet { - needPkgs = append(needPkgs, pkg) - } - } - modifiedPkgs = make([]string, 0, len(modifiedPkgsSet)) - for pkg := range modifiedPkgsSet { - modifiedPkgs = append(modifiedPkgs, pkg) - } - return modifiedPkgs, needPkgs, err -} - -// resolveImport finds the ID of a package given its import path. -// In particular, it will find the right vendored copy when in GOPATH mode. -func (state *golistState) resolveImport(sourceDir, importPath string) (string, error) { - env, err := state.getEnv() - if err != nil { - return "", err - } - if env["GOMOD"] != "" { - return importPath, nil - } - - searchDir := sourceDir - for { - vendorDir := filepath.Join(searchDir, "vendor") - exists, ok := state.vendorDirs[vendorDir] - if !ok { - info, err := os.Stat(vendorDir) - exists = err == nil && info.IsDir() - state.vendorDirs[vendorDir] = exists - } - - if exists { - vendoredPath := filepath.Join(vendorDir, importPath) - if info, err := os.Stat(vendoredPath); err == nil && info.IsDir() { - // We should probably check for .go files here, but shame on anyone who fools us. - path, ok, err := state.getPkgPath(vendoredPath) - if err != nil { - return "", err - } - if ok { - return path, nil - } - } - } - - // We know we've hit the top of the filesystem when we Dir / and get /, - // or C:\ and get C:\, etc. - next := filepath.Dir(searchDir) - if next == searchDir { - break - } - searchDir = next - } - return importPath, nil -} - -func hasTestFiles(p *Package) bool { - for _, f := range p.GoFiles { - if strings.HasSuffix(f, "_test.go") { - return true - } - } - return false -} - // determineRootDirs returns a mapping from absolute directories that could // contain code to their corresponding import path prefixes. func (state *golistState) determineRootDirs() (map[string]string, error) { @@ -384,192 +81,3 @@ func (state *golistState) determineRootDirsGOPATH() (map[string]string, error) { } return m, nil } - -func extractImports(filename string, contents []byte) ([]string, error) { - f, err := parser.ParseFile(token.NewFileSet(), filename, contents, parser.ImportsOnly) // TODO(matloob): reuse fileset? - if err != nil { - return nil, err - } - var res []string - for _, imp := range f.Imports { - quotedPath := imp.Path.Value - path, err := strconv.Unquote(quotedPath) - if err != nil { - return nil, err - } - res = append(res, path) - } - return res, nil -} - -// reclaimPackage attempts to reuse a package that failed to load in an overlay. -// -// If the package has errors and has no Name, GoFiles, or Imports, -// then it's possible that it doesn't yet exist on disk. -func reclaimPackage(pkg *Package, id string, filename string, contents []byte) bool { - // TODO(rstambler): Check the message of the actual error? - // It differs between $GOPATH and module mode. - if pkg.ID != id { - return false - } - if len(pkg.Errors) != 1 { - return false - } - if pkg.Name != "" || pkg.ExportFile != "" { - return false - } - if len(pkg.GoFiles) > 0 || len(pkg.CompiledGoFiles) > 0 || len(pkg.OtherFiles) > 0 { - return false - } - if len(pkg.Imports) > 0 { - return false - } - pkgName, ok := extractPackageName(filename, contents) - if !ok { - return false - } - pkg.Name = pkgName - pkg.Errors = nil - return true -} - -func extractPackageName(filename string, contents []byte) (string, bool) { - // TODO(rstambler): Check the message of the actual error? - // It differs between $GOPATH and module mode. - f, err := parser.ParseFile(token.NewFileSet(), filename, contents, parser.PackageClauseOnly) // TODO(matloob): reuse fileset? - if err != nil { - return "", false - } - return f.Name.Name, true -} - -// commonDir returns the directory that all files are in, "" if files is empty, -// or an error if they aren't in the same directory. -func commonDir(files []string) (string, error) { - seen := make(map[string]bool) - for _, f := range files { - seen[filepath.Dir(f)] = true - } - if len(seen) > 1 { - return "", fmt.Errorf("files (%v) are in more than one directory: %v", files, seen) - } - for k := range seen { - // seen has only one element; return it. - return k, nil - } - return "", nil // no files -} - -// It is possible that the files in the disk directory dir have a different package -// name from newName, which is deduced from the overlays. If they all have a different -// package name, and they all have the same package name, then that name becomes -// the package name. -// It returns true if it changes the package name, false otherwise. -func maybeFixPackageName(newName string, isTestFile bool, pkgsOfDir []*Package) { - names := make(map[string]int) - for _, p := range pkgsOfDir { - names[p.Name]++ - } - if len(names) != 1 { - // some files are in different packages - return - } - var oldName string - for k := range names { - oldName = k - } - if newName == oldName { - return - } - // We might have a case where all of the package names in the directory are - // the same, but the overlay file is for an x test, which belongs to its - // own package. If the x test does not yet exist on disk, we may not yet - // have its package name on disk, but we should not rename the packages. - // - // We use a heuristic to determine if this file belongs to an x test: - // The test file should have a package name whose package name has a _test - // suffix or looks like "newName_test". - maybeXTest := strings.HasPrefix(oldName+"_test", newName) || strings.HasSuffix(newName, "_test") - if isTestFile && maybeXTest { - return - } - for _, p := range pkgsOfDir { - p.Name = newName - } -} - -// This function is copy-pasted from -// https://github.com/golang/go/blob/9706f510a5e2754595d716bd64be8375997311fb/src/cmd/go/internal/search/search.go#L360. -// It should be deleted when we remove support for overlays from go/packages. -// -// NOTE: This does not handle any ./... or ./ style queries, as this function -// doesn't know the working directory. -// -// matchPattern(pattern)(name) reports whether -// name matches pattern. Pattern is a limited glob -// pattern in which '...' means 'any string' and there -// is no other special syntax. -// Unfortunately, there are two special cases. Quoting "go help packages": -// -// First, /... at the end of the pattern can match an empty string, -// so that net/... matches both net and packages in its subdirectories, like net/http. -// Second, any slash-separated pattern element containing a wildcard never -// participates in a match of the "vendor" element in the path of a vendored -// package, so that ./... does not match packages in subdirectories of -// ./vendor or ./mycode/vendor, but ./vendor/... and ./mycode/vendor/... do. -// Note, however, that a directory named vendor that itself contains code -// is not a vendored package: cmd/vendor would be a command named vendor, -// and the pattern cmd/... matches it. -func matchPattern(pattern string) func(name string) bool { - // Convert pattern to regular expression. - // The strategy for the trailing /... is to nest it in an explicit ? expression. - // The strategy for the vendor exclusion is to change the unmatchable - // vendor strings to a disallowed code point (vendorChar) and to use - // "(anything but that codepoint)*" as the implementation of the ... wildcard. - // This is a bit complicated but the obvious alternative, - // namely a hand-written search like in most shell glob matchers, - // is too easy to make accidentally exponential. - // Using package regexp guarantees linear-time matching. - - const vendorChar = "\x00" - - if strings.Contains(pattern, vendorChar) { - return func(name string) bool { return false } - } - - re := regexp.QuoteMeta(pattern) - re = replaceVendor(re, vendorChar) - switch { - case strings.HasSuffix(re, `/`+vendorChar+`/\.\.\.`): - re = strings.TrimSuffix(re, `/`+vendorChar+`/\.\.\.`) + `(/vendor|/` + vendorChar + `/\.\.\.)` - case re == vendorChar+`/\.\.\.`: - re = `(/vendor|/` + vendorChar + `/\.\.\.)` - case strings.HasSuffix(re, `/\.\.\.`): - re = strings.TrimSuffix(re, `/\.\.\.`) + `(/\.\.\.)?` - } - re = strings.ReplaceAll(re, `\.\.\.`, `[^`+vendorChar+`]*`) - - reg := regexp.MustCompile(`^` + re + `$`) - - return func(name string) bool { - if strings.Contains(name, vendorChar) { - return false - } - return reg.MatchString(replaceVendor(name, vendorChar)) - } -} - -// replaceVendor returns the result of replacing -// non-trailing vendor path elements in x with repl. -func replaceVendor(x, repl string) string { - if !strings.Contains(x, "vendor") { - return x - } - elem := strings.Split(x, "/") - for i := 0; i < len(elem)-1; i++ { - if elem[i] == "vendor" { - elem[i] = repl - } - } - return strings.Join(elem, "/") -} From b82788e58cfaf02ac9464d5d98d96a7f98abd4ef Mon Sep 17 00:00:00 2001 From: Viktor Blomqvist Date: Tue, 17 Oct 2023 20:22:49 +0200 Subject: [PATCH 041/100] gopls/internal/lsp: add semantic highlighting for go: directives Highlight known go:directives in their surrounding comment. Unknown or misspelled directives are skipped to signal to the end user that something is off. Closes golang/go#63538 Change-Id: Idf926106600b734ce2076366798acef0051181d6 Reviewed-on: https://go-review.googlesource.com/c/tools/+/536915 Auto-Submit: Robert Findley Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/semantic.go | 53 +++++++++++++++++ .../regtest/misc/semantictokens_test.go | 57 +++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go index 825e654c2cc..5adf1cc4449 100644 --- a/gopls/internal/lsp/semantic.go +++ b/gopls/internal/lsp/semantic.go @@ -143,6 +143,10 @@ func (e *encoded) semantics() { } for _, cg := range f.Comments { for _, c := range cg.List { + if strings.HasPrefix(c.Text, "//go:") { + e.godirective(c) + continue + } if !strings.Contains(c.Text, "\n") { e.token(c.Pos(), len(c.Text), tokComment, nil) continue @@ -997,3 +1001,52 @@ var ( "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", } ) + +var godirectives = map[string]struct{}{ + // https://pkg.go.dev/cmd/compile + "noescape": {}, + "uintptrescapes": {}, + "noinline": {}, + "norace": {}, + "nosplit": {}, + "linkname": {}, + + // https://pkg.go.dev/go/build + "build": {}, + "binary-only-package": {}, + "embed": {}, +} + +// Tokenize godirective at the start of the comment c, if any, and the surrounding comment. +// If there is any failure, emits the entire comment as a tokComment token. +// Directives are highlighted as-is, even if used incorrectly. Typically there are +// dedicated analyzers that will warn about misuse. +func (e *encoded) godirective(c *ast.Comment) { + // First check if '//go:directive args...' is a valid directive. + directive, args, _ := strings.Cut(c.Text, " ") + kind, _ := stringsCutPrefix(directive, "//go:") + if _, ok := godirectives[kind]; !ok { + // Unknown go: directive. + e.token(c.Pos(), len(c.Text), tokComment, nil) + return + } + + // Make the 'go:directive' part stand out, the rest is comments. + e.token(c.Pos(), len("//"), tokComment, nil) + + directiveStart := c.Pos() + token.Pos(len("//")) + e.token(directiveStart, len(directive[len("//"):]), tokNamespace, nil) + + if len(args) > 0 { + tailStart := c.Pos() + token.Pos(len(directive)+len(" ")) + e.token(tailStart, len(args), tokComment, nil) + } +} + +// Go 1.20 strings.CutPrefix. +func stringsCutPrefix(s, prefix string) (after string, found bool) { + if !strings.HasPrefix(s, prefix) { + return s, false + } + return s[len(prefix):], true +} diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index d50ceb029fe..5f243a158af 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -145,3 +145,60 @@ func New[K int, V any]() Smap[K, V] { } }) } + +func TestSemanticGoDirectives(t *testing.T) { + src := ` +-- go.mod -- +module example.com + +go 1.19 +-- main.go -- +package foo + +//go:linkname now time.Now +func now() + +//go:noinline +func foo() {} + +// Mentioning go:noinline should not tokenize. + +//go:notadirective +func bar() {} +` + want := []fake.SemanticToken{ + {Token: "package", TokenType: "keyword"}, + {Token: "foo", TokenType: "namespace"}, + + {Token: "//", TokenType: "comment"}, + {Token: "go:linkname", TokenType: "namespace"}, + {Token: "now time.Now", TokenType: "comment"}, + {Token: "func", TokenType: "keyword"}, + {Token: "now", TokenType: "function", Mod: "definition"}, + + {Token: "//", TokenType: "comment"}, + {Token: "go:noinline", TokenType: "namespace"}, + {Token: "func", TokenType: "keyword"}, + {Token: "foo", TokenType: "function", Mod: "definition"}, + + {Token: "// Mentioning go:noinline should not tokenize.", TokenType: "comment"}, + + {Token: "//go:notadirective", TokenType: "comment"}, + {Token: "func", TokenType: "keyword"}, + {Token: "bar", TokenType: "function", Mod: "definition"}, + } + + WithOptions( + Modes(Default), + Settings{"semanticTokens": true}, + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go") + if err != nil { + t.Fatal(err) + } + if x := cmp.Diff(want, seen); x != "" { + t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) + } + }) +} From 7c4d876fc53f19fc0fb3741895b581faf665e27e Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Tue, 24 Oct 2023 15:17:47 -0400 Subject: [PATCH 042/100] gopls/internal/lsp/source: remove unused parameters (cleanup) Remove or _-out unused parameters in the source package. This change was generated using the new code action. Change-Id: I36bbe28efbdf160cd094fdcd0b4d18fb34666e03 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537475 Reviewed-by: Alan Donovan Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/code_action.go | 2 +- gopls/internal/lsp/source/code_lens.go | 18 +++++++++--------- gopls/internal/lsp/source/extract.go | 23 +++++++++++------------ gopls/internal/lsp/source/fix.go | 2 +- gopls/internal/lsp/source/inlay_hint.go | 2 +- gopls/internal/lsp/source/options.go | 10 +++++----- gopls/internal/lsp/source/rename.go | 12 ++++++------ 7 files changed, 34 insertions(+), 35 deletions(-) diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index d7567488790..23856fbb595 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -674,7 +674,7 @@ func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd } func goTest(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) { - fns, err := source.TestsAndBenchmarks(ctx, snapshot, pkg, pgf) + fns, err := source.TestsAndBenchmarks(pkg, pgf) if err != nil { return nil, err } diff --git a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go index aad1a5a3bd7..c46bbad68fe 100644 --- a/gopls/internal/lsp/source/code_lens.go +++ b/gopls/internal/lsp/source/code_lens.go @@ -42,7 +42,7 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p if err != nil { return nil, err } - fns, err := TestsAndBenchmarks(ctx, snapshot, pkg, pgf) + fns, err := TestsAndBenchmarks(pkg, pgf) if err != nil { return nil, err } @@ -88,18 +88,18 @@ func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]p return codeLens, nil } -type testFn struct { +type TestFn struct { Name string Rng protocol.Range } -type testFns struct { - Tests []testFn - Benchmarks []testFn +type TestFns struct { + Tests []TestFn + Benchmarks []TestFn } -func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, pkg Package, pgf *ParsedGoFile) (testFns, error) { - var out testFns +func TestsAndBenchmarks(pkg Package, pgf *ParsedGoFile) (TestFns, error) { + var out TestFns if !strings.HasSuffix(pgf.URI.Filename(), "_test.go") { return out, nil @@ -117,11 +117,11 @@ func TestsAndBenchmarks(ctx context.Context, snapshot Snapshot, pkg Package, pgf } if matchTestFunc(fn, pkg, testRe, "T") { - out.Tests = append(out.Tests, testFn{fn.Name.Name, rng}) + out.Tests = append(out.Tests, TestFn{fn.Name.Name, rng}) } if matchTestFunc(fn, pkg, benchmarkRe, "B") { - out.Benchmarks = append(out.Benchmarks, testFn{fn.Name.Name, rng}) + out.Benchmarks = append(out.Benchmarks, TestFn{fn.Name.Name, rng}) } } diff --git a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go index 2231ae9ed02..0e062bd9eae 100644 --- a/gopls/internal/lsp/source/extract.go +++ b/gopls/internal/lsp/source/extract.go @@ -36,14 +36,14 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file // TODO: stricter rules for selectorExpr. case *ast.BasicLit, *ast.CompositeLit, *ast.IndexExpr, *ast.SliceExpr, *ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr: - lhsName, _ := generateAvailableIdentifier(expr.Pos(), file, path, pkg, info, "x", 0) + lhsName, _ := generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", 0) lhsNames = append(lhsNames, lhsName) case *ast.CallExpr: tup, ok := info.TypeOf(expr).(*types.Tuple) if !ok { // If the call expression only has one return value, we can treat it the // same as our standard extract variable case. - lhsName, _ := generateAvailableIdentifier(expr.Pos(), file, path, pkg, info, "x", 0) + lhsName, _ := generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", 0) lhsNames = append(lhsNames, lhsName) break } @@ -51,7 +51,7 @@ func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file for i := 0; i < tup.Len(); i++ { // Generate a unique variable for each return value. var lhsName string - lhsName, idx = generateAvailableIdentifier(expr.Pos(), file, path, pkg, info, "x", idx) + lhsName, idx = generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", idx) lhsNames = append(lhsNames, lhsName) } default: @@ -142,7 +142,7 @@ func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast. // generateAvailableIdentifier adjusts the new function name until there are no collisions in scope. // Possible collisions include other function and variable names. Returns the next index to check for prefix. -func generateAvailableIdentifier(pos token.Pos, file *ast.File, path []ast.Node, pkg *types.Package, info *types.Info, prefix string, idx int) (string, int) { +func generateAvailableIdentifier(pos token.Pos, path []ast.Node, pkg *types.Package, info *types.Info, prefix string, idx int) (string, int) { scopes := CollectScopes(info, path, pos) scopes = append(scopes, pkg.Scope()) return generateIdentifier(idx, prefix, func(name string) bool { @@ -485,8 +485,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte // signature of the extracted function as described above. Adjust all of // the return statements in the extracted function to reflect this change in // signature. - if err := adjustReturnStatements(returnTypes, seenVars, fset, file, - pkg, extractedBlock); err != nil { + if err := adjustReturnStatements(returnTypes, seenVars, file, pkg, extractedBlock); err != nil { return nil, err } } @@ -494,7 +493,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte // statements in the selection. Update the type signature of the extracted // function and construct the if statement that will be inserted in the enclosing // function. - retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, fset, start, hasNonNestedReturn) + retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, start, hasNonNestedReturn) if err != nil { return nil, err } @@ -529,7 +528,7 @@ func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte funName = name } else { name = "newFunction" - funName, _ = generateAvailableIdentifier(start, file, path, pkg, info, name, 0) + funName, _ = generateAvailableIdentifier(start, path, pkg, info, name, 0) } extractedFunCall := generateFuncCall(hasNonNestedReturn, hasReturnValues, params, append(returns, getNames(retVars)...), funName, sym, receiverName) @@ -1154,12 +1153,12 @@ func parseBlockStmt(fset *token.FileSet, src []byte) (*ast.BlockStmt, error) { // signature of the extracted function. We prepare names, signatures, and "zero values" that // represent the new variables. We also use this information to construct the if statement that // is inserted below the call to the extracted function. -func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.Node, file *ast.File, info *types.Info, fset *token.FileSet, pos token.Pos, hasNonNestedReturns bool) ([]*returnVariable, *ast.IfStmt, error) { +func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.Node, file *ast.File, info *types.Info, pos token.Pos, hasNonNestedReturns bool) ([]*returnVariable, *ast.IfStmt, error) { var retVars []*returnVariable var cond *ast.Ident if !hasNonNestedReturns { // Generate information for the added bool value. - name, _ := generateAvailableIdentifier(pos, file, path, pkg, info, "shouldReturn", 0) + name, _ := generateAvailableIdentifier(pos, path, pkg, info, "shouldReturn", 0) cond = &ast.Ident{Name: name} retVars = append(retVars, &returnVariable{ name: cond, @@ -1181,7 +1180,7 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast. return nil, nil, fmt.Errorf("nil AST expression") } var name string - name, idx = generateAvailableIdentifier(pos, file, path, pkg, info, "returnValue", idx) + name, idx = generateAvailableIdentifier(pos, path, pkg, info, "returnValue", idx) retVars = append(retVars, &returnVariable{ name: ast.NewIdent(name), decl: &ast.Field{Type: expr}, @@ -1205,7 +1204,7 @@ func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast. // adjustReturnStatements adds "zero values" of the given types to each return statement // in the given AST node. -func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]ast.Expr, fset *token.FileSet, file *ast.File, pkg *types.Package, extractedBlock *ast.BlockStmt) error { +func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]ast.Expr, file *ast.File, pkg *types.Package, extractedBlock *ast.BlockStmt) error { var zeroVals []ast.Expr // Create "zero values" for each type. for _, returnType := range returnTypes { diff --git a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go index 7a715a8ff5a..f024de03949 100644 --- a/gopls/internal/lsp/source/fix.go +++ b/gopls/internal/lsp/source/fix.go @@ -156,7 +156,7 @@ func fixedByImportingEmbed(diag *Diagnostic) bool { } // addEmbedImport adds a missing embed "embed" import with blank name. -func addEmbedImport(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { +func addEmbedImport(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) if err != nil { return nil, nil, fmt.Errorf("narrow pkg: %w", err) diff --git a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go index f75cd3621e3..9a77d16a093 100644 --- a/gopls/internal/lsp/source/inlay_hint.go +++ b/gopls/internal/lsp/source/inlay_hint.go @@ -306,7 +306,7 @@ func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *typ return hints } -func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { compLit, ok := node.(*ast.CompositeLit) if !ok { return nil diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index e57a3fd43c1..44bc7737f9a 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -1695,7 +1695,7 @@ var parBreakRE = regexp.MustCompile("\n{2,}") func collectEnums(opt *OptionJSON) string { var b strings.Builder - write := func(name, doc string, index, len int) { + write := func(name, doc string) { if doc != "" { unbroken := parBreakRE.ReplaceAllString(doc, "\\\n") fmt.Fprintf(&b, "* %s\n", strings.TrimSpace(unbroken)) @@ -1705,13 +1705,13 @@ func collectEnums(opt *OptionJSON) string { } if len(opt.EnumValues) > 0 && opt.Type == "enum" { b.WriteString("\nMust be one of:\n\n") - for i, val := range opt.EnumValues { - write(val.Value, val.Doc, i, len(opt.EnumValues)) + for _, val := range opt.EnumValues { + write(val.Value, val.Doc) } } else if len(opt.EnumKeys.Keys) > 0 && shouldShowEnumKeysInSettings(opt.Name) { b.WriteString("\nCan contain any of:\n\n") - for i, val := range opt.EnumKeys.Keys { - write(val.Name, val.Doc, i, len(opt.EnumKeys.Keys)) + for _, val := range opt.EnumKeys.Keys { + write(val.Name, val.Doc) } } return b.String() diff --git a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go index ad6184966f4..17ab2cda815 100644 --- a/gopls/internal/lsp/source/rename.go +++ b/gopls/internal/lsp/source/rename.go @@ -387,7 +387,7 @@ func renameOrdinary(ctx context.Context, snapshot Snapshot, f FileHandle, pp pro for obj := range targets { objects = append(objects, obj) } - editMap, _, err := renameObjects(ctx, snapshot, newName, pkg, objects...) + editMap, _, err := renameObjects(newName, pkg, objects...) return editMap, err } @@ -445,7 +445,7 @@ func renameOrdinary(ctx context.Context, snapshot Snapshot, f FileHandle, pp pro // Apply the renaming to the (initial) object. declPkgPath := PackagePath(obj.Pkg().Path()) - return renameExported(ctx, snapshot, pkgs, declPkgPath, declObjPath, newName) + return renameExported(pkgs, declPkgPath, declObjPath, newName) } // funcOrigin is a go1.18-portable implementation of (*types.Func).Origin. @@ -549,7 +549,7 @@ func SortPostOrder(meta MetadataSource, ids []PackageID) { // within the specified packages, along with any other objects that // must be renamed as a consequence. The slice of packages must be // topologically ordered. -func renameExported(ctx context.Context, snapshot Snapshot, pkgs []Package, declPkgPath PackagePath, declObjPath objectpath.Path, newName string) (map[span.URI][]diff.Edit, error) { +func renameExported(pkgs []Package, declPkgPath PackagePath, declObjPath objectpath.Path, newName string) (map[span.URI][]diff.Edit, error) { // A target is a name for an object that is stable across types.Packages. type target struct { @@ -605,7 +605,7 @@ func renameExported(ctx context.Context, snapshot Snapshot, pkgs []Package, decl } // Apply the renaming. - editMap, moreObjects, err := renameObjects(ctx, snapshot, newName, pkg, objects...) + editMap, moreObjects, err := renameObjects(newName, pkg, objects...) if err != nil { return nil, err } @@ -970,7 +970,7 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath // become shadowed by an intervening declaration that // uses the new name. // It returns the edits if no conflict was detected. - editMap, _, err := renameObjects(ctx, snapshot, localName, pkg, pkgname) + editMap, _, err := renameObjects(localName, pkg, pkgname) if err != nil { return err } @@ -1004,7 +1004,7 @@ func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath // consequence of the requested renamings. // // It returns an error if the renaming would cause a conflict. -func renameObjects(ctx context.Context, snapshot Snapshot, newName string, pkg Package, targets ...types.Object) (map[span.URI][]diff.Edit, map[types.Object]bool, error) { +func renameObjects(newName string, pkg Package, targets ...types.Object) (map[span.URI][]diff.Edit, map[types.Object]bool, error) { r := renamer{ pkg: pkg, objsToUpdate: make(map[types.Object]bool), From 1cf8b0dd67b2efbcd7349e878f4214e91ee111c1 Mon Sep 17 00:00:00 2001 From: Tim King Date: Mon, 9 Oct 2023 14:01:09 -0700 Subject: [PATCH 043/100] go/ssa: new range var semantics Adds support for the new *ast.RangeStmt semantics based on the goversion. Updates golang/go#63374 Change-Id: I938be38fd1d190057ef585f9bd1cb9eb54f0e081 Reviewed-on: https://go-review.googlesource.com/c/tools/+/535375 Run-TryBot: Tim King Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI TryBot-Result: Gopher Robot --- go/ssa/builder.go | 27 ++++++-- go/ssa/builder_go122_test.go | 65 +++++++++++-------- go/ssa/interp/interp_go122_test.go | 9 ++- go/ssa/interp/interp_test.go | 1 + .../interp/testdata/rangevarlifetime_go122.go | 23 +++++++ .../interp/testdata/rangevarlifetime_old.go | 32 +++++++++ 6 files changed, 119 insertions(+), 38 deletions(-) create mode 100644 go/ssa/interp/testdata/rangevarlifetime_go122.go create mode 100644 go/ssa/interp/testdata/rangevarlifetime_old.go diff --git a/go/ssa/builder.go b/go/ssa/builder.go index c66a6c55079..d39bc1cb741 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -1798,6 +1798,7 @@ func (b *builder) forStmt(fn *Function, s *ast.ForStmt, label *lblock) { if s.Init != nil { b.stmt(fn, s.Init) } + body := fn.newBasicBlock("for.body") done := fn.newBasicBlock("for.done") // target of 'break' loop := body // target of back-edge @@ -2093,13 +2094,11 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { tv = fn.typeOf(s.Value) } - // If iteration variables are defined (:=), this - // occurs once outside the loop. - // - // Unlike a short variable declaration, a RangeStmt - // using := never redeclares an existing variable; it - // always creates a new one. - if s.Tok == token.DEFINE { + // create locals for s.Key and s.Value. + createVars := func() { + // Unlike a short variable declaration, a RangeStmt + // using := never redeclares an existing variable; it + // always creates a new one. if tk != nil { fn.addLocalForIdent(s.Key.(*ast.Ident)) } @@ -2108,6 +2107,15 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { } } + major, minor := parseGoVersion(fn.goversion) + afterGo122 := major >= 1 && minor >= 22 + + if s.Tok == token.DEFINE && !afterGo122 { + // pre-go1.22: If iteration variables are defined (:=), this + // occurs once outside the loop. + createVars() + } + x := b.expr(fn, s.X) var k, v Value @@ -2138,6 +2146,11 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { panic("Cannot range over: " + rt.String()) } + if s.Tok == token.DEFINE && afterGo122 { + // go1.22: If iteration variables are defined (:=), this occurs inside the loop. + createVars() + } + // Evaluate both LHS expressions before we update either. var kl, vl lvalue if tk != nil { diff --git a/go/ssa/builder_go122_test.go b/go/ssa/builder_go122_test.go index a57e6a5ba24..d98431296a7 100644 --- a/go/ssa/builder_go122_test.go +++ b/go/ssa/builder_go122_test.go @@ -21,32 +21,28 @@ import ( "golang.org/x/tools/internal/testenv" ) +// TestMultipleGoversions tests that globals initialized to equivalent +// function literals are compiled based on the different GoVersion in each file. func TestMultipleGoversions(t *testing.T) { var contents = map[string]string{ "post.go": ` //go:build go1.22 package p - var distinct = func(l []int) []*int { - var r []*int + var distinct = func(l []int) { for i := range l { - r = append(r, &i) + print(&i) } - return r - }(l) + } `, "pre.go": ` package p - var l = []int{0, 0, 0} - - var same = func(l []int) []*int { - var r []*int + var same = func(l []int) { for i := range l { - r = append(r, &i) + print(&i) } - return r - }(l) + } `, } @@ -64,28 +60,41 @@ func TestMultipleGoversions(t *testing.T) { conf := &types.Config{Importer: nil, GoVersion: "go1.21"} p, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions) if err != nil { - t.Fatalf("unexpected error: %v", err) + t.Fatal(err) } - fns := ssautil.AllFunctions(p.Prog) - names := make(map[string]*ssa.Function) - for fn := range fns { - names[fn.String()] = fn - } - for _, item := range []struct{ name, wantSyn, wantPos string }{ - {"p.init", "package initializer", "-"}, - {"p.init$1", "", "post.go:5:17"}, - {"p.init$2", "", "pre.go:6:13"}, + // Test that global is initialized to a function literal that was + // compiled to have the expected for loop range variable lifetime for i. + for _, test := range []struct { + global *ssa.Global + want string // basic block to []*ssa.Alloc. + }{ + {p.Var("same"), "map[entry:[new int (i)]]"}, // i is allocated in the entry block. + {p.Var("distinct"), "map[rangeindex.body:[new int (i)]]"}, // i is allocated in the body block. } { - fn := names[item.name] + // Find the function the test.name global is initialized to. + var fn *ssa.Function + for _, b := range p.Func("init").Blocks { + for _, instr := range b.Instrs { + if s, ok := instr.(*ssa.Store); ok && s.Addr == test.global { + fn, _ = s.Val.(*ssa.Function) + } + } + } if fn == nil { - t.Fatalf("Could not find function named %q in package %s", item.name, p) + t.Fatalf("Failed to find *ssa.Function for initial value of global %s", test.global) } - if fn.Synthetic != item.wantSyn { - t.Errorf("Function %q.Synthetic=%q. expected %q", fn, fn.Synthetic, item.wantSyn) + + allocs := make(map[string][]string) // block comments -> []Alloc + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + if a, ok := instr.(*ssa.Alloc); ok { + allocs[b.Comment] = append(allocs[b.Comment], a.String()) + } + } } - if got := fset.Position(fn.Pos()).String(); got != item.wantPos { - t.Errorf("Function %q.Pos()=%q. expected %q", fn, got, item.wantPos) + if got := fmt.Sprint(allocs); got != test.want { + t.Errorf("[%s:=%s] expected the allocations to be in the basic blocks %q, got %q", test.global, fn, test.want, got) } } } diff --git a/go/ssa/interp/interp_go122_test.go b/go/ssa/interp/interp_go122_test.go index 3d808df97b7..8a37e58e0d0 100644 --- a/go/ssa/interp/interp_go122_test.go +++ b/go/ssa/interp/interp_go122_test.go @@ -2,12 +2,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build go1.19 +//go:build go1.22 +// +build go1.22 package interp_test -// Utilities from interp_test.go require go1.19. - import ( "log" "os" @@ -17,6 +16,10 @@ import ( "golang.org/x/tools/internal/testenv" ) +func init() { + testdataTests = append(testdataTests, "rangevarlifetime_go122.go") +} + // TestExperimentRange tests files in testdata with GOEXPERIMENT=range set. func TestExperimentRange(t *testing.T) { testenv.NeedsGoExperiment(t, "range") diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index 9728d6ec523..01219ca2397 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go @@ -132,6 +132,7 @@ var testdataTests = []string{ "slice2arrayptr.go", "static.go", "width32.go", + "rangevarlifetime_old.go", "fixedbugs/issue52342.go", "fixedbugs/issue55115.go", diff --git a/go/ssa/interp/testdata/rangevarlifetime_go122.go b/go/ssa/interp/testdata/rangevarlifetime_go122.go new file mode 100644 index 00000000000..4e0ec852048 --- /dev/null +++ b/go/ssa/interp/testdata/rangevarlifetime_go122.go @@ -0,0 +1,23 @@ +//go:build go1.22 + +package main + +// After go1.22, each i will have a distinct address. +var distinct = func(a [3]int) []*int { + var r []*int + for i := range a { + r = append(r, &i) + } + return r +}([3]int{}) + +func main() { + if len(distinct) != 3 { + panic(distinct) + } + for i := 0; i < 3; i++ { + if i != *(distinct[i]) { + panic(distinct) + } + } +} diff --git a/go/ssa/interp/testdata/rangevarlifetime_old.go b/go/ssa/interp/testdata/rangevarlifetime_old.go new file mode 100644 index 00000000000..f8c82b156b0 --- /dev/null +++ b/go/ssa/interp/testdata/rangevarlifetime_old.go @@ -0,0 +1,32 @@ +//go:build go1.19 + +// goversion can be pinned to anything strictly before 1.22. + +package main + +// pre-go1.22 all of i will have the same address. +var same = func(a [3]int) []*int { + var r []*int + for i := range a { + r = append(r, &i) + } + return r +}([3]int{}) + +func main() { + if len(same) != 3 { + panic(same) + } + for i := range same { + for j := range same { + if !(same[i] == same[j]) { + panic(same) + } + } + } + for i := range same { + if *(same[i]) != 2 { + panic(same) + } + } +} From 931c74e7d38e5c7464d0165236725e07dc7dad9f Mon Sep 17 00:00:00 2001 From: Viktor Blomqvist Date: Thu, 19 Oct 2023 17:14:51 +0200 Subject: [PATCH 044/100] internal/lsp/analysis: warn about incorrect type in embed directive Report a warning if the type in the declaration following a go:embed directive is not string, []byte or embed.FS. Change-Id: Ic6add9509cbbb7d3ee016371df6b6ee13fb27dcd Reviewed-on: https://go-review.googlesource.com/c/tools/+/536475 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Reviewed-by: Alan Donovan --- .../analysis/embeddirective/embeddirective.go | 30 +++++++- .../testdata/src/a/import_present.go | 76 ++++++++++++++++--- .../testdata/src/a/import_present_go120.go | 2 +- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/gopls/internal/lsp/analysis/embeddirective/embeddirective.go b/gopls/internal/lsp/analysis/embeddirective/embeddirective.go index b7efe4753d4..33af72b9f65 100644 --- a/gopls/internal/lsp/analysis/embeddirective/embeddirective.go +++ b/gopls/internal/lsp/analysis/embeddirective/embeddirective.go @@ -9,6 +9,7 @@ package embeddirective import ( "go/ast" "go/token" + "go/types" "strings" "golang.org/x/tools/go/analysis" @@ -67,10 +68,12 @@ func run(pass *analysis.Pass) (interface{}, error) { switch { case spec == nil: report(`go:embed directives must precede a "var" declaration`) - case len(spec.Names) > 1: + case len(spec.Names) != 1: report("declarations following go:embed directives must define a single variable") case len(spec.Values) > 0: report("declarations following go:embed directives must not specify a value") + case !embeddableType(pass.TypesInfo.Defs[spec.Names[0]]): + report("declarations following go:embed directives must be of type string, []byte or embed.FS") } } } @@ -132,3 +135,28 @@ func nextVarSpec(com *ast.Comment, f *ast.File) *ast.ValueSpec { } return spec } + +// embeddableType in go:embed directives are string, []byte or embed.FS. +func embeddableType(o types.Object) bool { + if o == nil { + return false + } + + // For embed.FS the underlying type is an implementation detail. + // As long as the named type resolves to embed.FS, it is OK. + if named, ok := o.Type().(*types.Named); ok { + obj := named.Obj() + if obj.Pkg() != nil && obj.Pkg().Path() == "embed" && obj.Name() == "FS" { + return true + } + } + + switch v := o.Type().Underlying().(type) { + case *types.Basic: + return types.Identical(v, types.Typ[types.String]) + case *types.Slice: + return types.Identical(v.Elem(), types.Typ[types.Byte]) + } + + return false +} diff --git a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present.go index 6d8138fffab..a124a583f75 100644 --- a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present.go +++ b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present.go @@ -8,62 +8,118 @@ package a //go:embed embedText // want "go:embed directives must precede a \"var\" declaration" import ( + "embed" + embedPkg "embed" "fmt" _ "embed" ) //go:embed embedText // ok -var s string +var e1 string // The analyzer does not check for many directives using the same var. // //go:embed embedText // ok //go:embed embedText // ok -var s string +var e2 string -// Comments and blank lines between are OK. +// Comments and blank lines between are OK. All types OK. // //go:embed embedText // ok // // foo -var s string +var e3 string + +//go:embed embedText //ok +var e4 []byte + +//go:embed embedText //ok +var e5 embed.FS // Followed by wrong kind of decl. // //go:embed embedText // want "go:embed directives must precede a \"var\" declaration" -func foo() +func fooFunc() {} // Multiple variable specs. // //go:embed embedText // want "declarations following go:embed directives must define a single variable" -var foo, bar []byte +var e6, e7 []byte // Specifying a value is not allowed. // //go:embed embedText // want "declarations following go:embed directives must not specify a value" -var s string = "foo" +var e8 string = "foo" // TODO: This should not be OK, misplaced according to compiler. // //go:embed embedText // ok var ( - s string - x string + e9 string + e10 string ) +// Type definition. +type fooType []byte + +//go:embed embedText //ok +var e11 fooType + +// Type alias. +type barType = string + +//go:embed embedText //ok +var e12 barType + +// Renamed embed package. + +//go:embed embedText //ok +var e13 embedPkg.FS + +// Renamed embed package alias. +type embedAlias = embedPkg.FS + +//go:embed embedText //ok +var e14 embedAlias + // var blocks are OK as long as the variable following the directive is OK. var ( x, y, z string //go:embed embedText // ok - s string + e20 string q, r, t string ) //go:embed embedText // want "go:embed directives must precede a \"var\" declaration" var () +// Incorrect types. + +//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +var e16 byte + +//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +var e17 []string + +//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +var e18 embed.Foo + +//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +var e19 foo.FS + +type byteAlias byte + +//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +var e15 byteAlias + +// A type declaration of embed.FS is not accepted by the compiler, in contrast to an alias. +type embedDecl embed.FS + +//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +var e16 embedDecl + // This is main function func main() { fmt.Println(s) diff --git a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present_go120.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present_go120.go index f88babddc73..2eaad23c4b0 100644 --- a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present_go120.go +++ b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present_go120.go @@ -11,7 +11,7 @@ var ( // Okay directive wise but the compiler will complain that // imports must appear before other declarations. //go:embed embedText // ok - "foo" + foo string ) import ( From 672de52636f73c05ee730524b36a7a21b6d04ee3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 4 Oct 2023 16:44:56 -0400 Subject: [PATCH 045/100] go/ssa: simplify Alloc helpers This change simplifies the helper functions for emitting Alloc instructions: - "add" -> "emit", since they all emit an Alloc instruction. - methods -> functions, since they are logically part of the builder rather than the Function built by it. (No need to enlarge Function's method table.) - there is now one place that creates an Alloc, one place that adds to Function.Locals, and one place that applies instantiations to Var.Type. - emitNew/Local accept a comment parameter. - docs are improved. Change-Id: I6b87ff5ba1f113551d79259de5a5c35ddb3ebc52 Reviewed-on: https://go-review.googlesource.com/c/tools/+/532755 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI --- go/ssa/builder.go | 46 ++++++++++++++++++-------------------------- go/ssa/emit.go | 47 ++++++++++++++++++++++++++++++++++++++++----- go/ssa/func.go | 49 ++++++++++++----------------------------------- go/ssa/ssa.go | 15 ++++++--------- 4 files changed, 79 insertions(+), 78 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index d39bc1cb741..ee2e33f389f 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -323,10 +323,8 @@ func (b *builder) builtin(fn *Function, obj *types.Builtin, args []ast.Expr, typ // treat make([]T, n, m) as new([m]T)[:n] cap := m.Int64() at := types.NewArray(ct.Elem(), cap) - alloc := emitNew(fn, at, pos) - alloc.Comment = "makeslice" v := &Slice{ - X: alloc, + X: emitNew(fn, at, pos, "makeslice"), High: n, } v.setPos(pos) @@ -363,9 +361,7 @@ func (b *builder) builtin(fn *Function, obj *types.Builtin, args []ast.Expr, typ } case "new": - alloc := emitNew(fn, mustDeref(typ), pos) - alloc.Comment = "new" - return alloc + return emitNew(fn, mustDeref(typ), pos, "new") case "len", "cap": // Special case: len or cap of an array or *array is @@ -432,11 +428,10 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { typ, _ := deref(fn.typeOf(e)) var v *Alloc if escaping { - v = emitNew(fn, typ, e.Lbrace) + v = emitNew(fn, typ, e.Lbrace, "complit") } else { - v = fn.addLocal(typ, e.Lbrace) + v = emitLocal(fn, typ, e.Lbrace, "complit") } - v.Comment = "complit" var sb storebuf b.compLit(fn, v, e, true, &sb) sb.emit(fn) @@ -1091,9 +1086,8 @@ func (b *builder) emitCallArgs(fn *Function, sig *types.Signature, e *ast.CallEx } else { // Replace a suffix of args with a slice containing it. at := types.NewArray(vt, int64(len(varargs))) - a := emitNew(fn, at, token.NoPos) + a := emitNew(fn, at, token.NoPos, "varargs") a.setPos(e.Rparen) - a.Comment = "varargs" for i, arg := range varargs { iaddr := &IndexAddr{ X: a, @@ -1140,7 +1134,7 @@ func (b *builder) localValueSpec(fn *Function, spec *ast.ValueSpec) { // 1:1 assignment for i, id := range spec.Names { if !isBlankIdent(id) { - fn.addLocalForIdent(id) + emitLocalVar(fn, identVar(fn, id)) } lval := b.addr(fn, id, false) // non-escaping b.assign(fn, lval, spec.Values[i], true, nil) @@ -1151,7 +1145,7 @@ func (b *builder) localValueSpec(fn *Function, spec *ast.ValueSpec) { // Locals are implicitly zero-initialized. for _, id := range spec.Names { if !isBlankIdent(id) { - lhs := fn.addLocalForIdent(id) + lhs := emitLocalVar(fn, identVar(fn, id)) if fn.debugInfo() { emitDebugRef(fn, id, lhs, true) } @@ -1163,7 +1157,7 @@ func (b *builder) localValueSpec(fn *Function, spec *ast.ValueSpec) { tuple := b.exprN(fn, spec.Values[0]) for i, id := range spec.Names { if !isBlankIdent(id) { - fn.addLocalForIdent(id) + emitLocalVar(fn, identVar(fn, id)) lhs := b.addr(fn, id, false) // non-escaping lhs.store(fn, emitExtract(fn, tuple, i)) } @@ -1183,8 +1177,8 @@ func (b *builder) assignStmt(fn *Function, lhss, rhss []ast.Expr, isDef bool) { var lval lvalue = blank{} if !isBlankIdent(lhs) { if isDef { - if obj := fn.info.Defs[lhs.(*ast.Ident)]; obj != nil { - fn.addNamedLocal(obj) + if obj, ok := fn.info.Defs[lhs.(*ast.Ident)].(*types.Var); ok { + emitLocalVar(fn, obj) isZero[i] = true } } @@ -1293,9 +1287,7 @@ func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero switch t := t.(type) { case *types.Slice: at = types.NewArray(t.Elem(), b.arrayLen(fn, e.Elts)) - alloc := emitNew(fn, at, e.Lbrace) - alloc.Comment = "slicelit" - array = alloc + array = emitNew(fn, at, e.Lbrace, "slicelit") case *types.Array: at = t array = addr @@ -1583,13 +1575,13 @@ func (b *builder) typeSwitchStmt(fn *Function, s *ast.TypeSwitchStmt, label *lbl } func (b *builder) typeCaseBody(fn *Function, cc *ast.CaseClause, x Value, done *BasicBlock) { - if obj := fn.info.Implicits[cc]; obj != nil { + if obj, ok := fn.info.Implicits[cc].(*types.Var); ok { // In a switch y := x.(type), each case clause // implicitly declares a distinct object y. // In a single-type case, y has that type. // In multi-type cases, 'case nil' and default, // y has the same type as the interface operand. - emitStore(fn, fn.addNamedLocal(obj), x, obj.Pos()) + emitStore(fn, emitLocalVar(fn, obj), x, obj.Pos()) } fn.targets = &targets{ tail: fn.targets, @@ -1738,7 +1730,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { case *ast.AssignStmt: // x := <-states[state].Chan if comm.Tok == token.DEFINE { - fn.addLocalForIdent(comm.Lhs[0].(*ast.Ident)) + emitLocalVar(fn, identVar(fn, comm.Lhs[0].(*ast.Ident))) } x := b.addr(fn, comm.Lhs[0], false) // non-escaping v := emitExtract(fn, sel, r) @@ -1749,7 +1741,7 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { if len(comm.Lhs) == 2 { // x, ok := ... if comm.Tok == token.DEFINE { - fn.addLocalForIdent(comm.Lhs[1].(*ast.Ident)) + emitLocalVar(fn, identVar(fn, comm.Lhs[1].(*ast.Ident))) } ok := b.addr(fn, comm.Lhs[1], false) // non-escaping ok.store(fn, emitExtract(fn, sel, 1)) @@ -1874,7 +1866,7 @@ func (b *builder) rangeIndexed(fn *Function, x Value, tv types.Type, pos token.P length = fn.emit(&c) } - index := fn.addLocal(tInt, token.NoPos) + index := emitLocal(fn, tInt, token.NoPos, "rangeindex") emitStore(fn, index, intConst(-1), pos) loop = fn.newBasicBlock("rangeindex.loop") @@ -2053,7 +2045,7 @@ func (b *builder) rangeInt(fn *Function, x Value, tk types.Type, pos token.Pos) } T := x.Type() - iter := fn.addLocal(T, token.NoPos) + iter := emitLocal(fn, T, token.NoPos, "rangeint.iter") // x may be unsigned. Avoid initializing x to -1. body := fn.newBasicBlock("rangeint.body") @@ -2100,10 +2092,10 @@ func (b *builder) rangeStmt(fn *Function, s *ast.RangeStmt, label *lblock) { // using := never redeclares an existing variable; it // always creates a new one. if tk != nil { - fn.addLocalForIdent(s.Key.(*ast.Ident)) + emitLocalVar(fn, identVar(fn, s.Key.(*ast.Ident))) } if tv != nil { - fn.addLocalForIdent(s.Value.(*ast.Ident)) + emitLocalVar(fn, identVar(fn, s.Value.(*ast.Ident))) } } diff --git a/go/ssa/emit.go b/go/ssa/emit.go index abb617e6d40..448d1fb2852 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -13,16 +13,53 @@ import ( "go/types" ) -// emitNew emits to f a new (heap Alloc) instruction allocating an -// object of type typ. pos is the optional source location. -func emitNew(f *Function, typ types.Type, pos token.Pos) *Alloc { - v := &Alloc{Heap: true} +// emitAlloc emits to f a new Alloc instruction allocating a variable +// of type typ. +// +// The caller must set Alloc.Heap=true (for an heap-allocated variable) +// or add the Alloc to f.Locals (for a frame-allocated variable). +// +// During building, a variable in f.Locals may have its Heap flag +// set when it is discovered that its address is taken. +// These Allocs are removed from f.Locals at the end. +// +// The builder should generally call one of the emit{New,Local,LocalVar} wrappers instead. +func emitAlloc(f *Function, typ types.Type, pos token.Pos, comment string) *Alloc { + v := &Alloc{Comment: comment} v.setType(types.NewPointer(typ)) v.setPos(pos) f.emit(v) return v } +// emitNew emits to f a new Alloc instruction heap-allocating a +// variable of type typ. pos is the optional source location. +func emitNew(f *Function, typ types.Type, pos token.Pos, comment string) *Alloc { + alloc := emitAlloc(f, typ, pos, comment) + alloc.Heap = true + return alloc +} + +// emitLocal creates a local var for (t, pos, comment) and +// emits an Alloc instruction for it. +// +// (Use this function or emitNew for synthetic variables; +// for source-level variables, use emitLocalVar.) +func emitLocal(f *Function, t types.Type, pos token.Pos, comment string) *Alloc { + local := emitAlloc(f, t, pos, comment) + f.Locals = append(f.Locals, local) + return local +} + +// emitLocalVar creates a local var for v and emits an Alloc instruction for it. +// Subsequent calls to f.lookup(v) return it. +// It applies the appropriate generic instantiation to the type. +func emitLocalVar(f *Function, v *types.Var) *Alloc { + alloc := emitLocal(f, f.typ(v.Type()), v.Pos(), v.Name()) + f.objects[v] = alloc + return alloc +} + // emitLoad emits to f an instruction to load the address addr into a // new temporary, and returns the value so defined. func emitLoad(f *Function, addr Value) *UnOp { @@ -563,7 +600,7 @@ func emitSliceToArray(f *Function, val Value, typ types.Type) Value { emitIf(f, cond, nilb, nonnilb) f.currentBlock = nilb - zero := f.addLocal(typ, token.NoPos) + zero := emitLocal(f, f.typ(typ), token.NoPos, "phizero") emitJump(f, done) f.currentBlock = nonnilb diff --git a/go/ssa/func.go b/go/ssa/func.go index 7b374442e95..0465f84d6b3 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -133,7 +133,7 @@ func (f *Function) addParam(name string, typ types.Type, pos token.Pos) *Paramet return v } -func (f *Function) addParamObj(obj types.Object) *Parameter { +func (f *Function) addParamObj(obj *types.Var) *Parameter { name := obj.Name() if name == "" { name = fmt.Sprintf("arg%d", len(f.Params)) @@ -146,14 +146,9 @@ func (f *Function) addParamObj(obj types.Object) *Parameter { // addSpilledParam declares a parameter that is pre-spilled to the // stack; the function body will load/store the spilled location. // Subsequent lifting will eliminate spills where possible. -func (f *Function) addSpilledParam(obj types.Object) { +func (f *Function) addSpilledParam(obj *types.Var) { param := f.addParamObj(obj) - spill := &Alloc{Comment: obj.Name()} - spill.setType(types.NewPointer(param.Type())) - spill.setPos(obj.Pos()) - f.objects[obj] = spill - f.Locals = append(f.Locals, spill) - f.emit(spill) + spill := emitLocalVar(f, obj) f.emit(&Store{Addr: spill, Val: param}) } @@ -177,7 +172,7 @@ func (f *Function) createSyntacticParams(recv *ast.FieldList, functype *ast.Func if recv != nil { for _, field := range recv.List { for _, n := range field.Names { - f.addSpilledParam(f.info.Defs[n]) + f.addSpilledParam(identVar(f, n)) } // Anonymous receiver? No need to spill. if field.Names == nil { @@ -191,7 +186,7 @@ func (f *Function) createSyntacticParams(recv *ast.FieldList, functype *ast.Func n := len(f.Params) // 1 if has recv, 0 otherwise for _, field := range functype.Params.List { for _, n := range field.Names { - f.addSpilledParam(f.info.Defs[n]) + f.addSpilledParam(identVar(f, n)) } // Anonymous parameter? No need to spill. if field.Names == nil { @@ -205,7 +200,8 @@ func (f *Function) createSyntacticParams(recv *ast.FieldList, functype *ast.Func for _, field := range functype.Results.List { // Implicit "var" decl of locals for named results. for _, n := range field.Names { - f.namedResults = append(f.namedResults, f.addLocalForIdent(n)) + namedResult := emitLocalVar(f, identVar(f, n)) + f.namedResults = append(f.namedResults, namedResult) } } } @@ -388,32 +384,6 @@ func (f *Function) debugInfo() bool { return p != nil && p.debug } -// addNamedLocal creates a local variable, adds it to function f and -// returns it. Its name and type are taken from obj. Subsequent -// calls to f.lookup(obj) will return the same local. -func (f *Function) addNamedLocal(obj types.Object) *Alloc { - l := f.addLocal(obj.Type(), obj.Pos()) - l.Comment = obj.Name() - f.objects[obj] = l - return l -} - -func (f *Function) addLocalForIdent(id *ast.Ident) *Alloc { - return f.addNamedLocal(f.info.Defs[id]) -} - -// addLocal creates an anonymous local variable of type typ, adds it -// to function f and returns it. pos is the optional source location. -func (f *Function) addLocal(typ types.Type, pos token.Pos) *Alloc { - typ = f.typ(typ) - v := &Alloc{} - v.setType(types.NewPointer(typ)) - v.setPos(pos) - f.Locals = append(f.Locals, v) - f.emit(v) - return v -} - // lookup returns the address of the named variable identified by obj // that is local to function f or one of its enclosing functions. // If escaping, the reference comes from a potentially escaping pointer @@ -704,3 +674,8 @@ func (n extentNode) End() token.Pos { return n[1] } // function. Otherwise, it is an opaque Node providing only position // information; this avoids pinning the AST in memory. func (f *Function) Syntax() ast.Node { return f.syntax } + +// identVar returns the variable defined by id. +func identVar(fn *Function, id *ast.Ident) *types.Var { + return fn.info.Defs[id].(*types.Var) +} diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 04b7157deb4..b7ecd813720 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -318,7 +318,7 @@ type Function struct { Prog *Program // enclosing program Params []*Parameter // function parameters; for methods, includes receiver FreeVars []*FreeVar // free variables whose values must be supplied by closure - Locals []*Alloc // local variables of this function + Locals []*Alloc // frame-allocated variables of this function Blocks []*BasicBlock // basic blocks of the function; nil => external Recover *BasicBlock // optional; control transfers here after recovered panic AnonFuncs []*Function // anonymous functions directly beneath this one @@ -484,15 +484,12 @@ type Builtin struct { // type of the allocated variable is actually // Type().Underlying().(*types.Pointer).Elem(). // -// If Heap is false, Alloc allocates space in the function's -// activation record (frame); we refer to an Alloc(Heap=false) as a -// "local" alloc. Each local Alloc returns the same address each time -// it is executed within the same activation; the space is -// re-initialized to zero. +// If Heap is false, Alloc zero-initializes the same local variable in +// the call frame and returns its address; in this case the Alloc must +// be present in Function.Locals. We call this a "local" alloc. // -// If Heap is true, Alloc allocates space in the heap; we -// refer to an Alloc(Heap=true) as a "new" alloc. Each new Alloc -// returns a different address each time it is executed. +// If Heap is true, Alloc allocates a new zero-initialized variable +// each time the instruction is executed. We call this a "new" alloc. // // When Alloc is applied to a channel, map or slice type, it returns // the address of an uninitialized (nil) reference of that kind; store From 4dca90297fdaf2755bbdc2c2ad76f31e34df216a Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 26 Oct 2023 11:36:33 -0400 Subject: [PATCH 046/100] go/internal/packagesdriver: be defensive wrt error results While investigating a bug that turned out to lie elsewhere (#63700), we were troubled by the inconsistencies between the pair of errors returned by functions in this package. This change makes things more consistent. Updates golang/go#63700 Change-Id: I926c47572b7f666327bd1dba71ace68a5591bf2f Reviewed-on: https://go-review.googlesource.com/c/tools/+/537875 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- go/internal/packagesdriver/sizes.go | 15 ++++++++++----- internal/gocommand/invoke.go | 24 ++++++++++++++---------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/go/internal/packagesdriver/sizes.go b/go/internal/packagesdriver/sizes.go index 0454cdd78e5..333676b7cfc 100644 --- a/go/internal/packagesdriver/sizes.go +++ b/go/internal/packagesdriver/sizes.go @@ -13,16 +13,17 @@ import ( "golang.org/x/tools/internal/gocommand" ) -var debug = false - func GetSizesForArgsGolist(ctx context.Context, inv gocommand.Invocation, gocmdRunner *gocommand.Runner) (string, string, error) { inv.Verb = "list" inv.Args = []string{"-f", "{{context.GOARCH}} {{context.Compiler}}", "--", "unsafe"} stdout, stderr, friendlyErr, rawErr := gocmdRunner.RunRaw(ctx, inv) var goarch, compiler string if rawErr != nil { - if rawErrMsg := rawErr.Error(); strings.Contains(rawErrMsg, "cannot find main module") || strings.Contains(rawErrMsg, "go.mod file not found") { - // User's running outside of a module. All bets are off. Get GOARCH and guess compiler is gc. + rawErrMsg := rawErr.Error() + if strings.Contains(rawErrMsg, "cannot find main module") || + strings.Contains(rawErrMsg, "go.mod file not found") { + // User's running outside of a module. + // All bets are off. Get GOARCH and guess compiler is gc. // TODO(matloob): Is this a problem in practice? inv.Verb = "env" inv.Args = []string{"GOARCH"} @@ -32,8 +33,12 @@ func GetSizesForArgsGolist(ctx context.Context, inv gocommand.Invocation, gocmdR } goarch = strings.TrimSpace(envout.String()) compiler = "gc" - } else { + } else if friendlyErr != nil { return "", "", friendlyErr + } else { + // This should be unreachable, but be defensive + // in case RunRaw's error results are inconsistent. + return "", "", rawErr } } else { fields := strings.Fields(stdout.String()) diff --git a/internal/gocommand/invoke.go b/internal/gocommand/invoke.go index 53cf66da019..c27b91f8c7e 100644 --- a/internal/gocommand/invoke.go +++ b/internal/gocommand/invoke.go @@ -85,6 +85,7 @@ func (runner *Runner) RunPiped(ctx context.Context, inv Invocation, stdout, stde // RunRaw runs the invocation, serializing requests only if they fight over // go.mod changes. +// Postcondition: both error results have same nilness. func (runner *Runner) RunRaw(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) { ctx, done := event.Start(ctx, "gocommand.Runner.RunRaw", invLabels(inv)...) defer done() @@ -95,23 +96,24 @@ func (runner *Runner) RunRaw(ctx context.Context, inv Invocation) (*bytes.Buffer stdout, stderr, friendlyErr, err := runner.runConcurrent(ctx, inv) // If we encounter a load concurrency error, we need to retry serially. - if friendlyErr == nil || !modConcurrencyError.MatchString(friendlyErr.Error()) { - return stdout, stderr, friendlyErr, err + if friendlyErr != nil && modConcurrencyError.MatchString(friendlyErr.Error()) { + event.Error(ctx, "Load concurrency error, will retry serially", err) + + // Run serially by calling runPiped. + stdout.Reset() + stderr.Reset() + friendlyErr, err = runner.runPiped(ctx, inv, stdout, stderr) } - event.Error(ctx, "Load concurrency error, will retry serially", err) - // Run serially by calling runPiped. - stdout.Reset() - stderr.Reset() - friendlyErr, err = runner.runPiped(ctx, inv, stdout, stderr) return stdout, stderr, friendlyErr, err } +// Postcondition: both error results have same nilness. func (runner *Runner) runConcurrent(ctx context.Context, inv Invocation) (*bytes.Buffer, *bytes.Buffer, error, error) { // Wait for 1 worker to become available. select { case <-ctx.Done(): - return nil, nil, nil, ctx.Err() + return nil, nil, ctx.Err(), ctx.Err() case runner.inFlight <- struct{}{}: defer func() { <-runner.inFlight }() } @@ -121,6 +123,7 @@ func (runner *Runner) runConcurrent(ctx context.Context, inv Invocation) (*bytes return stdout, stderr, friendlyErr, err } +// Postcondition: both error results have same nilness. func (runner *Runner) runPiped(ctx context.Context, inv Invocation, stdout, stderr io.Writer) (error, error) { // Make sure the runner is always initialized. runner.initialize() @@ -129,7 +132,7 @@ func (runner *Runner) runPiped(ctx context.Context, inv Invocation, stdout, stde // runPiped commands. select { case <-ctx.Done(): - return nil, ctx.Err() + return ctx.Err(), ctx.Err() case runner.serialized <- struct{}{}: defer func() { <-runner.serialized }() } @@ -139,7 +142,7 @@ func (runner *Runner) runPiped(ctx context.Context, inv Invocation, stdout, stde for i := 0; i < maxInFlight; i++ { select { case <-ctx.Done(): - return nil, ctx.Err() + return ctx.Err(), ctx.Err() case runner.inFlight <- struct{}{}: // Make sure we always "return" any workers we took. defer func() { <-runner.inFlight }() @@ -172,6 +175,7 @@ type Invocation struct { Logf func(format string, args ...interface{}) } +// Postcondition: both error results have same nilness. func (i *Invocation) runWithFriendlyError(ctx context.Context, stdout, stderr io.Writer) (friendlyError error, rawError error) { rawError = i.run(ctx, stdout, stderr) if rawError != nil { From ff1953bdd2380e886652a84eb29e3d3b40d98305 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 26 Oct 2023 11:57:12 -0400 Subject: [PATCH 047/100] go/packages: don't fail if GOPACKAGESDRIVER leaves Compiler/GOARCH="" This change causes packages.Load to set TypesSizes to a fallback value whenever the external GOPACKAGESDRIVER fails to populate the Compiler and GOARCH fields of the response. Fixes golang/go#63700 Change-Id: I189ae97dde04f313f79e5db39a10b4a217cd8534 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537876 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/packages/packages.go | 45 ++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/go/packages/packages.go b/go/packages/packages.go index 40af0adc461..2f7c998d584 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -259,17 +259,28 @@ type driverResponse struct { // provided for convenient display of all errors. func Load(cfg *Config, patterns ...string) ([]*Package, error) { ld := newLoader(cfg) - response, err := defaultDriver(&ld.Config, patterns...) + response, external, err := defaultDriver(&ld.Config, patterns...) if err != nil { return nil, err } - // If type size information is needed but unavailable. - // reject the whole Load since the error is the same for every package. ld.sizes = types.SizesFor(response.Compiler, response.Arch) if ld.sizes == nil && ld.Config.Mode&(NeedTypes|NeedTypesSizes|NeedTypesInfo) != 0 { - return nil, fmt.Errorf("can't determine type sizes for compiler %q on GOARCH %q", - response.Compiler, response.Arch) + // Type size information is needed but unavailable. + if external { + // An external driver may fail to populate the Compiler/GOARCH fields, + // especially since they are relatively new (see #63700). + // Provide a sensible fallback in this case. + ld.sizes = types.SizesFor("gc", runtime.GOARCH) + if ld.sizes == nil { // gccgo-only arch + ld.sizes = types.SizesFor("gc", "amd64") + } + } else { + // Go list should never fail to deliver accurate size information. + // Reject the whole Load since the error is the same for every package. + return nil, fmt.Errorf("can't determine type sizes for compiler %q on GOARCH %q", + response.Compiler, response.Arch) + } } return ld.refine(response) @@ -279,18 +290,20 @@ func Load(cfg *Config, patterns ...string) ([]*Package, error) { // It will try to request to an external driver, if one exists. If there's // no external driver, or the driver returns a response with NotHandled set, // defaultDriver will fall back to the go list driver. -func defaultDriver(cfg *Config, patterns ...string) (*driverResponse, error) { - driver := findExternalDriver(cfg) - if driver == nil { - driver = goListDriver - } - response, err := driver(cfg, patterns...) - if err != nil { - return response, err - } else if response.NotHandled { - return goListDriver(cfg, patterns...) +// The boolean result indicates that an external driver handled the request. +func defaultDriver(cfg *Config, patterns ...string) (*driverResponse, bool, error) { + if driver := findExternalDriver(cfg); driver != nil { + response, err := driver(cfg, patterns...) + if err != nil { + return nil, false, err + } else if !response.NotHandled { + return response, true, nil + } + // (fall through) } - return response, nil + + response, err := goListDriver(cfg, patterns...) + return response, false, err } // A Package describes a loaded Go package. From 080c20204649f4ff382fee2ae9b7304c044286ae Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 26 Oct 2023 14:12:07 -0400 Subject: [PATCH 048/100] gopls/internal/lsp: fix code action panic on params of external funcs Fix a panic when inspecting a nil function body. Fixes golang/go#63755 Change-Id: I39342902b44192dd373dfdb24947079b40dbe115 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537878 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/lsp/code_action.go | 4 +++ gopls/internal/regtest/misc/fix_test.go | 33 +++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 23856fbb595..8978ab5bfed 100644 --- a/gopls/internal/lsp/code_action.go +++ b/gopls/internal/lsp/code_action.go @@ -541,6 +541,10 @@ func canRemoveParameter(pkg source.Package, pgf *source.ParsedGoFile, rng protoc return false } + if info.Decl.Body == nil { + return false // external function + } + if len(info.Field.Names) == 0 { return true // no names => field is unused } diff --git a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go index 7a5e530e307..67e37c9a2cb 100644 --- a/gopls/internal/regtest/misc/fix_test.go +++ b/gopls/internal/regtest/misc/fix_test.go @@ -101,3 +101,36 @@ func Foo() error { env.AfterChange(NoDiagnostics(ForFile("main.go"))) }) } + +func TestUnusedParameter_Issue63755(t *testing.T) { + // This test verifies the fix for #63755, where codeActions panicked on parameters + // of functions with no function body. + + // We should not detect parameters as unused for external functions. + + const files = ` +-- go.mod -- +module unused.mod + +go 1.18 + +-- external.go -- +package external + +func External(z int) //@codeaction("refactor.rewrite", "z", "z", recursive) + +func _() { + External(1) +} + ` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("external.go") + actions, err := env.Editor.CodeAction(env.Ctx, env.RegexpSearch("external.go", "z"), nil) + if err != nil { + t.Fatal(err) + } + if len(actions) > 0 { + t.Errorf("CodeAction(): got %d code actions, want 0", len(actions)) + } + }) +} From d187d998aba141c3f1e202ac2abf5678229750fe Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 25 Oct 2023 15:01:47 -0400 Subject: [PATCH 049/100] gopls/internal/regtest/marker: port remaining @signature markers Clean up remaining signature markers, as well as related completion markers. For golang/go#54845 Change-Id: Ibba44bc667e6166e0b89ee388ad3de77a4863a86 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537737 Reviewed-by: Alan Donovan Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/lsp_test.go | 34 --- gopls/internal/lsp/regtest/marker.go | 35 +-- .../importedcomplit/imported_complit.go.in | 54 ----- .../lsp/testdata/signature/signature.go | 85 -------- .../testdata/signature/signature.go.golden | 53 ----- .../testdata/signature/signature2.go.golden | 3 - .../lsp/testdata/signature/signature2.go.in | 5 - .../testdata/signature/signature3.go.golden | 3 - .../lsp/testdata/signature/signature3.go.in | 5 - .../lsp/testdata/signature/signature_test.go | 13 -- .../signature/signature_test.go.golden | 9 - .../testdata/snippets/func_snippets118.go.in | 19 -- .../internal/lsp/testdata/snippets/literal.go | 22 -- .../lsp/testdata/snippets/literal.go.golden | 3 - .../lsp/testdata/snippets/snippets.go.golden | 3 - .../lsp/testdata/snippets/snippets.go.in | 64 ------ .../internal/lsp/testdata/summary.txt.golden | 7 +- gopls/internal/lsp/tests/tests.go | 31 --- gopls/internal/lsp/tests/util.go | 43 ---- .../testdata/completion/func_snippets.txt | 32 +++ .../marker/testdata/completion/postfix.txt} | 18 +- .../marker/testdata/completion/snippet.txt | 77 +++++++ .../completion/snippet_placeholder.txt | 83 +++++++ .../marker/testdata/completion/testy.txt | 4 +- .../marker/testdata/signature/signature.txt | 206 ++++++++++++++++++ 25 files changed, 437 insertions(+), 474 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in delete mode 100644 gopls/internal/lsp/testdata/signature/signature.go delete mode 100644 gopls/internal/lsp/testdata/signature/signature.go.golden delete mode 100644 gopls/internal/lsp/testdata/signature/signature2.go.golden delete mode 100644 gopls/internal/lsp/testdata/signature/signature2.go.in delete mode 100644 gopls/internal/lsp/testdata/signature/signature3.go.golden delete mode 100644 gopls/internal/lsp/testdata/signature/signature3.go.in delete mode 100644 gopls/internal/lsp/testdata/signature/signature_test.go delete mode 100644 gopls/internal/lsp/testdata/signature/signature_test.go.golden delete mode 100644 gopls/internal/lsp/testdata/snippets/func_snippets118.go.in delete mode 100644 gopls/internal/lsp/testdata/snippets/literal.go delete mode 100644 gopls/internal/lsp/testdata/snippets/literal.go.golden delete mode 100644 gopls/internal/lsp/testdata/snippets/snippets.go.golden delete mode 100644 gopls/internal/lsp/testdata/snippets/snippets.go.in create mode 100644 gopls/internal/regtest/marker/testdata/completion/func_snippets.txt rename gopls/internal/{lsp/testdata/snippets/postfix.go => regtest/marker/testdata/completion/postfix.txt} (82%) create mode 100644 gopls/internal/regtest/marker/testdata/completion/snippet.txt create mode 100644 gopls/internal/regtest/marker/testdata/completion/snippet_placeholder.txt create mode 100644 gopls/internal/regtest/marker/testdata/signature/signature.txt diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 0bda4e6a6d0..87430e83796 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -572,40 +572,6 @@ func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[sp return res, nil } -func (r *runner) SignatureHelp(t *testing.T, spn span.Span, want *protocol.SignatureHelp) { - m, err := r.data.Mapper(spn.URI()) - if err != nil { - t.Fatal(err) - } - loc, err := m.SpanLocation(spn) - if err != nil { - t.Fatalf("failed for %v: %v", loc, err) - } - params := &protocol.SignatureHelpParams{ - TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } - got, err := r.server.SignatureHelp(r.ctx, params) - if err != nil { - // Only fail if we got an error we did not expect. - if want != nil { - t.Fatal(err) - } - return - } - if want == nil { - if got != nil { - t.Errorf("expected no signature, got %v", got) - } - return - } - if got == nil { - t.Fatalf("expected %v, got nil", want) - } - if diff := tests.DiffSignatures(spn, want, got); diff != "" { - t.Error(diff) - } -} - func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { m, err := r.data.Mapper(uri) if err != nil { diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index 928f77dc40d..c6bb611c7e7 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -222,6 +222,10 @@ var update = flag.Bool("update", false, "if set, update test data during marker // - renameerr(location, new, wantError): specifies a renaming that // fails with an error that matches the expectation. // +// - signature(location, label, active): specifies that +// signatureHelp at the given location should match the provided string, with +// the active parameter (an index) highlighted. +// // - suggestedfix(location, regexp, kind, golden): like diag, the location and // regexp identify an expected diagnostic. This diagnostic must // to have exactly one associated code action of the specified kind. @@ -354,7 +358,6 @@ var update = flag.Bool("update", false, "if set, update test data during marker // internal/lsp/tests. // // Remaining TODO: -// - optimize test execution // - reorganize regtest packages (and rename to just 'test'?) // - Rename the files .txtar. // - Provide some means by which locations in the standard library @@ -363,7 +366,6 @@ var update = flag.Bool("update", false, "if set, update test data during marker // // Existing marker tests (in ../testdata) to port: // - CallHierarchy -// - Diagnostics // - CompletionItems // - Completions // - CompletionSnippets @@ -371,18 +373,14 @@ var update = flag.Bool("update", false, "if set, update test data during marker // - FuzzyCompletions // - CaseSensitiveCompletions // - RankCompletions -// - Formats -// - Imports // - SemanticTokens // - FunctionExtractions // - MethodExtractions // - Renames // - PrepareRenames // - InlayHints -// - WorkspaceSymbols // - Signatures // - Links -// - AddImport // - SelectionRanges func RunMarkerTests(t *testing.T, dir string) { // The marker tests must be able to run go/packages.Load. @@ -850,7 +848,7 @@ func (g *Golden) Get(t testing.TB, name string, updated []byte) ([]byte, bool) { // archive. func loadMarkerTests(dir string) ([]*markerTest, error) { var tests []*markerTest - err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + err := filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { if strings.HasSuffix(path, ".txt") { content, err := os.ReadFile(path) if err != nil { @@ -864,7 +862,7 @@ func loadMarkerTests(dir string) ([]*markerTest, error) { } tests = append(tests, test) } - return nil + return err }) return tests, err } @@ -1222,7 +1220,7 @@ func convert(mark marker, arg any, paramType reflect.Type) (any, error) { if id, ok := arg.(expect.Identifier); ok { if arg, ok := mark.run.values[id]; ok { if !reflect.TypeOf(arg).AssignableTo(paramType) { - return nil, fmt.Errorf("cannot convert %v to %s", arg, paramType) + return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType) } return arg, nil } @@ -1236,7 +1234,7 @@ func convert(mark marker, arg any, paramType reflect.Type) (any, error) { case wantErrorType: return convertWantError(mark, arg) default: - return nil, fmt.Errorf("cannot convert %v to %s", arg, paramType) + return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType) } } @@ -1793,14 +1791,23 @@ func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr wantErr.check(mark, err) } -func signatureMarker(mark marker, src protocol.Location, want string) { +func signatureMarker(mark marker, src protocol.Location, label string, active int64) { got := mark.run.env.SignatureHelp(src) + if label == "" { + if got != nil && len(got.Signatures) > 0 { + mark.errorf("signatureHelp = %v, want 0 signatures", got) + } + return + } if got == nil || len(got.Signatures) != 1 { mark.errorf("signatureHelp = %v, want exactly 1 signature", got) return } - if got := got.Signatures[0].Label; got != want { - mark.errorf("signatureHelp: got %q, want %q", got, want) + if got := got.Signatures[0].Label; got != label { + mark.errorf("signatureHelp: got label %q, want %q", got, label) + } + if got := int64(got.ActiveParameter); got != active { + mark.errorf("signatureHelp: got active parameter %d, want %d", got, active) } } @@ -1912,7 +1919,7 @@ func codeLensesMarker(mark marker) { } var want []codeLens - mark.consumeExtraNotes("codelens", actionMarkerFunc(func(mark marker, loc protocol.Location, title string) { + mark.consumeExtraNotes("codelens", actionMarkerFunc(func(_ marker, loc protocol.Location, title string) { want = append(want, codeLens{loc.Range, title}) })) diff --git a/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in b/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in deleted file mode 100644 index 05ba54006a5..00000000000 --- a/gopls/internal/lsp/testdata/importedcomplit/imported_complit.go.in +++ /dev/null @@ -1,54 +0,0 @@ -package importedcomplit - -import ( - // TODO(rfindley): re-enable after moving to new framework - // "golang.org/lsptests/foo" - - // import completions (separate blocks to avoid comment alignment) - "crypto/elli" //@complete("\" //", cryptoImport) - - "fm" //@complete("\" //", fmtImport) - - "go/pars" //@complete("\" //", parserImport) - - namedParser "go/pars" //@complete("\" //", parserImport) - - "golang.org/lspte" //@complete("\" //", lsptestsImport) - - "golang.org/lsptests/sign" //@complete("\" //", signatureImport) - - "golang.org/lsptests/sign" //@complete("ests", lsptestsImport) - - "golang.org/lsptests/signa" //@complete("na\" //", signatureImport) -) - -func _() { - var V int //@item(icVVar, "V", "int", "var") - - // TODO(rfindley): re-enable after moving to new framework - // _ = foo.StructFoo{V} // complete("}", Value, icVVar) -} - -func _() { - var ( - aa string //@item(icAAVar, "aa", "string", "var") - ab int //@item(icABVar, "ab", "int", "var") - ) - - // TODO(rfindley): re-enable after moving to new framework - // _ = foo.StructFoo{a} // complete("}", abVar, aaVar) - - var s struct { - AA string //@item(icFieldAA, "AA", "string", "field") - AB int //@item(icFieldAB, "AB", "int", "field") - } - - // TODO(rfindley): re-enable after moving to new framework - //_ = foo.StructFoo{s.} // complete("}", icFieldAB, icFieldAA) -} - -/* "fmt" */ //@item(fmtImport, "fmt", "\"fmt\"", "package") -/* "go/parser" */ //@item(parserImport, "parser", "\"go/parser\"", "package") -/* "golang.org/lsptests/signature" */ //@item(signatureImport, "signature", "\"golang.org/lsptests/signature\"", "package") -/* "golang.org/lsptests/" */ //@item(lsptestsImport, "lsptests/", "\"golang.org/lsptests/\"", "package") -/* "crypto/elliptic" */ //@item(cryptoImport, "elliptic", "\"crypto/elliptic\"", "package") diff --git a/gopls/internal/lsp/testdata/signature/signature.go b/gopls/internal/lsp/testdata/signature/signature.go deleted file mode 100644 index 4e2b12bc419..00000000000 --- a/gopls/internal/lsp/testdata/signature/signature.go +++ /dev/null @@ -1,85 +0,0 @@ -// Package signature has tests for signature help. -package signature - -import ( - "bytes" - "encoding/json" - "math/big" -) - -func Foo(a string, b int) (c bool) { - return -} - -func Bar(float64, ...byte) { -} - -type myStruct struct{} - -func (*myStruct) foo(e *json.Decoder) (*big.Int, error) { - return nil, nil -} - -type MyType struct{} - -type MyFunc func(foo int) string - -type Alias = int -type OtherAlias = int -type StringAlias = string - -func AliasSlice(a []*Alias) (b Alias) { return 0 } -func AliasMap(a map[*Alias]StringAlias) (b, c map[*Alias]StringAlias) { return nil, nil } -func OtherAliasMap(a, b map[Alias]OtherAlias) map[Alias]OtherAlias { return nil } - -func Qux() { - Foo("foo", 123) //@signature("(", "Foo(a string, b int) (c bool)", 0) - Foo("foo", 123) //@signature("123", "Foo(a string, b int) (c bool)", 1) - Foo("foo", 123) //@signature(",", "Foo(a string, b int) (c bool)", 0) - Foo("foo", 123) //@signature(" 1", "Foo(a string, b int) (c bool)", 1) - Foo("foo", 123) //@signature(")", "Foo(a string, b int) (c bool)", 1) - - Bar(13.37, 0x13) //@signature("13.37", "Bar(float64, ...byte)", 0) - Bar(13.37, 0x37) //@signature("0x37", "Bar(float64, ...byte)", 1) - Bar(13.37, 1, 2, 3, 4) //@signature("4", "Bar(float64, ...byte)", 1) - - fn := func(hi, there string) func(i int) rune { - return func(int) rune { return 0 } - } - - fn("hi", "there") //@signature("hi", "", 0) - fn("hi", "there") //@signature(",", "fn(hi string, there string) func(i int) rune", 0) - fn("hi", "there")(1) //@signature("1", "func(i int) rune", 0) - - fnPtr := &fn - (*fnPtr)("hi", "there") //@signature(",", "func(hi string, there string) func(i int) rune", 0) - - var fnIntf interface{} = Foo - fnIntf.(func(string, int) bool)("hi", 123) //@signature("123", "func(string, int) bool", 1) - - (&bytes.Buffer{}).Next(2) //@signature("2", "Next(n int) []byte", 0) - - myFunc := MyFunc(func(n int) string { return "" }) - myFunc(123) //@signature("123", "myFunc(foo int) string", 0) - - var ms myStruct - ms.foo(nil) //@signature("nil", "foo(e *json.Decoder) (*big.Int, error)", 0) - - _ = make([]int, 1, 2) //@signature("2", "make(t Type, size ...int) Type", 1) - - Foo(myFunc(123), 456) //@signature("myFunc", "Foo(a string, b int) (c bool)", 0) - Foo(myFunc(123), 456) //@signature("123", "myFunc(foo int) string", 0) - - panic("oops!") //@signature(")", "panic(v interface{})", 0) - println("hello", "world") //@signature(",", "println(args ...Type)", 0) - - Hello(func() { - //@signature("//", "", 0) - }) - - AliasSlice() //@signature(")", "AliasSlice(a []*Alias) (b Alias)", 0) - AliasMap() //@signature(")", "AliasMap(a map[*Alias]StringAlias) (b map[*Alias]StringAlias, c map[*Alias]StringAlias)", 0) - OtherAliasMap() //@signature(")", "OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias", 0) -} - -func Hello(func()) {} diff --git a/gopls/internal/lsp/testdata/signature/signature.go.golden b/gopls/internal/lsp/testdata/signature/signature.go.golden deleted file mode 100644 index 90a4facf9a7..00000000000 --- a/gopls/internal/lsp/testdata/signature/signature.go.golden +++ /dev/null @@ -1,53 +0,0 @@ --- AliasMap(a map[*Alias]StringAlias) (b map[*Alias]StringAlias, c map[*Alias]StringAlias)-signature -- -AliasMap(a map[*Alias]StringAlias) (b map[*Alias]StringAlias, c map[*Alias]StringAlias) - --- AliasSlice(a []*Alias) (b Alias)-signature -- -AliasSlice(a []*Alias) (b Alias) - --- Bar(float64, ...byte)-signature -- -Bar(float64, ...byte) - --- Foo(a string, b int) (c bool)-signature -- -Foo(a string, b int) (c bool) - --- Next(n int) []byte-signature -- -Next(n int) []byte - -Next returns a slice containing the next n bytes from the buffer, advancing the buffer as if the bytes had been returned by Read. - --- OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias-signature -- -OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias - --- fn(hi string, there string) func(i int) rune-signature -- -fn(hi string, there string) func(i int) rune - --- foo(e *json.Decoder) (*big.Int, error)-signature -- -foo(e *json.Decoder) (*big.Int, error) - --- func(hi string, there string) func(i int) rune-signature -- -func(hi string, there string) func(i int) rune - --- func(i int) rune-signature -- -func(i int) rune - --- func(string, int) bool-signature -- -func(string, int) bool - --- make(t Type, size ...int) Type-signature -- -make(t Type, size ...int) Type - -The make built-in function allocates and initializes an object of type slice, map, or chan (only). - --- myFunc(foo int) string-signature -- -myFunc(foo int) string - --- panic(v interface{})-signature -- -panic(v any) - -The panic built-in function stops normal execution of the current goroutine. - --- println(args ...Type)-signature -- -println(args ...Type) - -The println built-in function formats its arguments in an implementation-specific way and writes the result to standard error. - diff --git a/gopls/internal/lsp/testdata/signature/signature2.go.golden b/gopls/internal/lsp/testdata/signature/signature2.go.golden deleted file mode 100644 index e8102584fe0..00000000000 --- a/gopls/internal/lsp/testdata/signature/signature2.go.golden +++ /dev/null @@ -1,3 +0,0 @@ --- Foo(a string, b int) (c bool)-signature -- -Foo(a string, b int) (c bool) - diff --git a/gopls/internal/lsp/testdata/signature/signature2.go.in b/gopls/internal/lsp/testdata/signature/signature2.go.in deleted file mode 100644 index 16355ffc01d..00000000000 --- a/gopls/internal/lsp/testdata/signature/signature2.go.in +++ /dev/null @@ -1,5 +0,0 @@ -package signature - -func _() { - Foo(//@signature("//", "Foo(a string, b int) (c bool)", 0) -} diff --git a/gopls/internal/lsp/testdata/signature/signature3.go.golden b/gopls/internal/lsp/testdata/signature/signature3.go.golden deleted file mode 100644 index e8102584fe0..00000000000 --- a/gopls/internal/lsp/testdata/signature/signature3.go.golden +++ /dev/null @@ -1,3 +0,0 @@ --- Foo(a string, b int) (c bool)-signature -- -Foo(a string, b int) (c bool) - diff --git a/gopls/internal/lsp/testdata/signature/signature3.go.in b/gopls/internal/lsp/testdata/signature/signature3.go.in deleted file mode 100644 index 032be130453..00000000000 --- a/gopls/internal/lsp/testdata/signature/signature3.go.in +++ /dev/null @@ -1,5 +0,0 @@ -package signature - -func _() { - Foo("hello",//@signature("//", "Foo(a string, b int) (c bool)", 1) -} \ No newline at end of file diff --git a/gopls/internal/lsp/testdata/signature/signature_test.go b/gopls/internal/lsp/testdata/signature/signature_test.go deleted file mode 100644 index 500247dbdec..00000000000 --- a/gopls/internal/lsp/testdata/signature/signature_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package signature_test - -import ( - "testing" - - sig "golang.org/lsptests/signature" -) - -func TestSignature(t *testing.T) { - sig.AliasSlice() //@signature(")", "AliasSlice(a []*sig.Alias) (b sig.Alias)", 0) - sig.AliasMap() //@signature(")", "AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias)", 0) - sig.OtherAliasMap() //@signature(")", "OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias", 0) -} diff --git a/gopls/internal/lsp/testdata/signature/signature_test.go.golden b/gopls/internal/lsp/testdata/signature/signature_test.go.golden deleted file mode 100644 index 9e6561ac529..00000000000 --- a/gopls/internal/lsp/testdata/signature/signature_test.go.golden +++ /dev/null @@ -1,9 +0,0 @@ --- AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias)-signature -- -AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias) - --- AliasSlice(a []*sig.Alias) (b sig.Alias)-signature -- -AliasSlice(a []*sig.Alias) (b sig.Alias) - --- OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias-signature -- -OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias - diff --git a/gopls/internal/lsp/testdata/snippets/func_snippets118.go.in b/gopls/internal/lsp/testdata/snippets/func_snippets118.go.in deleted file mode 100644 index d4933689d65..00000000000 --- a/gopls/internal/lsp/testdata/snippets/func_snippets118.go.in +++ /dev/null @@ -1,19 +0,0 @@ -// +build go1.18 -//go:build go1.18 - -package snippets - -type SyncMap[K comparable, V any] struct{} - -func NewSyncMap[K comparable, V any]() (result *SyncMap[K, V]) { //@item(NewSyncMap, "NewSyncMap", "", "") - return -} - -func Identity[P ~int](p P) P { //@item(Identity, "Identity", "", "") - return p -} - -func _() { - _ = NewSyncM //@snippet(" //", NewSyncMap, "NewSyncMap[${1:}]()", "NewSyncMap[${1:K comparable}, ${2:V any}]()") - _ = Identi //@snippet(" //", Identity, "Identity[${1:}](${2:})", "Identity[${1:P ~int}](${2:p P})") -} diff --git a/gopls/internal/lsp/testdata/snippets/literal.go b/gopls/internal/lsp/testdata/snippets/literal.go deleted file mode 100644 index fbb642f08a5..00000000000 --- a/gopls/internal/lsp/testdata/snippets/literal.go +++ /dev/null @@ -1,22 +0,0 @@ -package snippets - -import ( - "golang.org/lsptests/signature" - t "golang.org/lsptests/types" -) - -type structy struct { - x signature.MyType -} - -func X(_ map[signature.Alias]t.CoolAlias) (map[signature.Alias]t.CoolAlias) { - return nil -} - -func _() { - X() //@signature(")", "X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias", 0) - _ = signature.MyType{} //@item(literalMyType, "signature.MyType{}", "", "var") - s := structy{ - x: //@snippet(" //", literalMyType, "signature.MyType{\\}", "signature.MyType{\\}") - } -} \ No newline at end of file diff --git a/gopls/internal/lsp/testdata/snippets/literal.go.golden b/gopls/internal/lsp/testdata/snippets/literal.go.golden deleted file mode 100644 index c91e5e9e086..00000000000 --- a/gopls/internal/lsp/testdata/snippets/literal.go.golden +++ /dev/null @@ -1,3 +0,0 @@ --- X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias-signature -- -X(_ map[signature.Alias]t.CoolAlias) map[signature.Alias]t.CoolAlias - diff --git a/gopls/internal/lsp/testdata/snippets/snippets.go.golden b/gopls/internal/lsp/testdata/snippets/snippets.go.golden deleted file mode 100644 index 3f20ba50bfb..00000000000 --- a/gopls/internal/lsp/testdata/snippets/snippets.go.golden +++ /dev/null @@ -1,3 +0,0 @@ --- baz(at AliasType, b bool)-signature -- -baz(at AliasType, b bool) - diff --git a/gopls/internal/lsp/testdata/snippets/snippets.go.in b/gopls/internal/lsp/testdata/snippets/snippets.go.in deleted file mode 100644 index 79bff334233..00000000000 --- a/gopls/internal/lsp/testdata/snippets/snippets.go.in +++ /dev/null @@ -1,64 +0,0 @@ -package snippets - -// Pre-set this marker, as we don't have a "source" for it in this package. -/* Error() */ //@item(Error, "Error", "func() string", "method") - -type AliasType = int //@item(sigAliasType, "AliasType", "AliasType", "type") - -func foo(i int, b bool) {} //@item(snipFoo, "foo", "func(i int, b bool)", "func") -func bar(fn func()) func() {} //@item(snipBar, "bar", "func(fn func())", "func") -func baz(at AliasType, b bool) {} //@item(snipBaz, "baz", "func(at AliasType, b bool)", "func") - -type Foo struct { - Bar int //@item(snipFieldBar, "Bar", "int", "field") - Func func(at AliasType) error //@item(snipFieldFunc, "Func", "func(at AliasType) error", "field") -} - -func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz", "func() func()", "method") -func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method") -func (Foo) BazBaz(at AliasType) func() {} //@item(snipMethodBazBaz, "BazBaz", "func(at AliasType) func()", "method") - -func _() { - f //@snippet(" //", snipFoo, "foo(${1:})", "foo(${1:i int}, ${2:b bool})") - - bar //@snippet(" //", snipBar, "bar(${1:})", "bar(${1:fn func()})") - - baz //@snippet(" //", snipBaz, "baz(${1:})", "baz(${1:at AliasType}, ${2:b bool})") - baz() //@signature("(", "baz(at AliasType, b bool)", 0) - - bar(nil) //@snippet("(", snipBar, "bar", "bar") - bar(ba) //@snippet(")", snipBar, "bar(${1:})", "bar(${1:fn func()})") - var f Foo - bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()", "Baz()") - (bar)(nil) //@snippet(")", snipBar, "bar(${1:})", "bar(${1:fn func()})") - (f.Ba)() //@snippet(")", snipMethodBaz, "Baz()", "Baz()") - - Foo{ - B //@snippet(" //", snipFieldBar, "Bar: ${1:},", "Bar: ${1:int},") - } - - Foo{ - F //@snippet(" //", snipFieldFunc, "Func: ${1:},", "Func: ${1:func(at AliasType) error},") - } - - Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1:}", "Bar: ${1:int}") - Foo{} //@snippet("}", snipFieldBar, "Bar: ${1:}", "Bar: ${1:int}") - - Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar", "Bar") - - var err error - err.Error() //@snippet("E", Error, "Error()", "Error()") - f.Baz() //@snippet("B", snipMethodBaz, "Baz()", "Baz()") - - f.Baz() //@snippet("(", snipMethodBazBar, "BazBar", "BazBar") - - f.Baz() //@snippet("B", snipMethodBazBaz, "BazBaz(${1:})", "BazBaz(${1:at AliasType})") -} - -func _() { - type bar struct { - a int - b float64 //@item(snipBarB, "b", "float64", "field") - } - bar{b} //@snippet("}", snipBarB, "b: ${1:}", "b: ${1:float64}") -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 7059a381d19..58a709d8009 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,10 +1,10 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 194 -CompletionSnippetCount = 74 +CompletionsCount = 181 +CompletionSnippetCount = 53 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 -RankedCompletionsCount = 166 +RankedCompletionsCount = 163 CaseSensitiveCompletionsCount = 4 SemanticTokenCount = 3 SuggestedFixCount = 80 @@ -12,7 +12,6 @@ MethodExtractionCount = 8 InlayHintsCount = 5 RenamesCount = 48 PrepareRenamesCount = 7 -SignaturesCount = 32 LinksCount = 7 SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 5b7074fe6fa..1e682f37c15 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -68,7 +68,6 @@ type MethodExtractions = map[span.Span]span.Span type Renames = map[span.Span]string type PrepareRenames = map[span.Span]*source.PrepareItem type InlayHints = []span.Span -type Signatures = map[span.Span]*protocol.SignatureHelp type Links = map[span.URI][]Link type AddImport = map[span.URI]string type SelectionRanges = []span.Span @@ -90,7 +89,6 @@ type Data struct { Renames Renames InlayHints InlayHints PrepareRenames PrepareRenames - Signatures Signatures Links Links AddImport AddImport SelectionRanges SelectionRanges @@ -126,7 +124,6 @@ type Tests interface { InlayHints(*testing.T, span.Span) Rename(*testing.T, span.Span, string) PrepareRename(*testing.T, span.Span, *source.PrepareItem) - SignatureHelp(*testing.T, span.Span, *protocol.SignatureHelp) Link(*testing.T, span.URI, []Link) AddImport(*testing.T, span.URI, string) SelectionRanges(*testing.T, span.Span) @@ -246,7 +243,6 @@ func load(t testing.TB, mode string, dir string) *Data { PrepareRenames: make(PrepareRenames), SuggestedFixes: make(SuggestedFixes), MethodExtractions: make(MethodExtractions), - Signatures: make(Signatures), Links: make(Links), AddImport: make(AddImport), @@ -392,7 +388,6 @@ func load(t testing.TB, mode string, dir string) *Data { "inlayHint": datum.collectInlayHints, "rename": datum.collectRenames, "prepare": datum.collectPrepareRenames, - "signature": datum.collectSignatures, "link": datum.collectLinks, "suggestedfix": datum.collectSuggestedFixes, "extractmethod": datum.collectMethodExtractions, @@ -593,16 +588,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("SignatureHelp", func(t *testing.T) { - t.Helper() - for spn, expectedSignature := range data.Signatures { - t.Run(SpanName(spn), func(t *testing.T) { - t.Helper() - tests.SignatureHelp(t, spn, expectedSignature) - }) - } - }) - t.Run("Link", func(t *testing.T) { t.Helper() for uri, wantLinks := range data.Links { @@ -690,7 +675,6 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "InlayHintsCount = %v\n", len(data.InlayHints)) fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames)) fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames)) - fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures)) fmt.Fprintf(buf, "LinksCount = %v\n", linksCount) fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges)) @@ -897,21 +881,6 @@ func (data *Data) mustRange(spn span.Span) protocol.Range { return rng } -func (data *Data) collectSignatures(spn span.Span, signature string, activeParam int64) { - data.Signatures[spn] = &protocol.SignatureHelp{ - Signatures: []protocol.SignatureInformation{ - { - Label: signature, - }, - }, - ActiveParameter: uint32(activeParam), - } - // Hardcode special case to test the lack of a signature. - if signature == "" && activeParam == 0 { - data.Signatures[spn] = nil - } -} - func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain, placeholder string) { data.CompletionSnippets[spn] = append(data.CompletionSnippets[spn], CompletionSnippet{ CompletionItem: item, diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index b9a21fe9627..79e31f409d4 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -16,7 +16,6 @@ import ( "github.com/google/go-cmp/cmp/cmpopts" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source/completion" - "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/gopls/internal/span" ) @@ -88,48 +87,6 @@ func DiffLinks(mapper *protocol.Mapper, wantLinks []Link, gotLinks []protocol.Do return msg.String() } -// inRange reports whether p is contained within [r.Start, r.End), or if p == -// r.Start == r.End (special handling for the case where the range is a single -// point). -func inRange(p protocol.Position, r protocol.Range) bool { - if protocol.IsPoint(r) { - return protocol.ComparePosition(r.Start, p) == 0 - } - if protocol.ComparePosition(r.Start, p) <= 0 && protocol.ComparePosition(p, r.End) < 0 { - return true - } - return false -} - -func DiffSignatures(spn span.Span, want, got *protocol.SignatureHelp) string { - decorate := func(f string, args ...interface{}) string { - return fmt.Sprintf("invalid signature at %s: %s", spn, fmt.Sprintf(f, args...)) - } - if len(got.Signatures) != 1 { - return decorate("wanted 1 signature, got %d", len(got.Signatures)) - } - if got.ActiveSignature != 0 { - return decorate("wanted active signature of 0, got %d", int(got.ActiveSignature)) - } - if want.ActiveParameter != got.ActiveParameter { - return decorate("wanted active parameter of %d, got %d", want.ActiveParameter, int(got.ActiveParameter)) - } - g := got.Signatures[0] - w := want.Signatures[0] - if diff := compare.Text(NormalizeAny(w.Label), NormalizeAny(g.Label)); diff != "" { - return decorate("mismatched labels:\n%s", diff) - } - var paramParts []string - for _, p := range g.Parameters { - paramParts = append(paramParts, p.Label) - } - paramsStr := strings.Join(paramParts, ", ") - if !strings.Contains(g.Label, paramsStr) { - return decorate("expected signature %q to contain params %q", g.Label, paramsStr) - } - return "" -} - // NormalizeAny replaces occurrences of interface{} in input with any. // // In Go 1.18, standard library functions were changed to use the 'any' diff --git a/gopls/internal/regtest/marker/testdata/completion/func_snippets.txt b/gopls/internal/regtest/marker/testdata/completion/func_snippets.txt new file mode 100644 index 00000000000..efbc393f30f --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/func_snippets.txt @@ -0,0 +1,32 @@ +This test exercises function snippets using generics. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "usePlaceholders": true +} + +-- go.mod -- +module golang.org/lsptests/snippets + +go 1.18 + +-- funcsnippets.go -- +package snippets + +type SyncMap[K comparable, V any] struct{} + +func NewSyncMap[K comparable, V any]() (result *SyncMap[K, V]) { //@item(NewSyncMap, "NewSyncMap", "", "") + return +} + +func Identity[P ~int](p P) P { //@item(Identity, "Identity", "", "") + return p +} + +func _() { + _ = NewSyncM //@snippet(" //", NewSyncMap, "NewSyncMap[${1:K comparable}, ${2:V any}]()") + _ = Identi //@snippet(" //", Identity, "Identity[${1:P ~int}](${2:p P})") +} diff --git a/gopls/internal/lsp/testdata/snippets/postfix.go b/gopls/internal/regtest/marker/testdata/completion/postfix.txt similarity index 82% rename from gopls/internal/lsp/testdata/snippets/postfix.go rename to gopls/internal/regtest/marker/testdata/completion/postfix.txt index 78a091ada5c..a4485d7efd6 100644 --- a/gopls/internal/lsp/testdata/snippets/postfix.go +++ b/gopls/internal/regtest/marker/testdata/completion/postfix.txt @@ -1,11 +1,19 @@ -package snippets +These tests check that postfix completions do and do not show up in certain +cases. Tests for the postfix completion contents are implemented as ad-hoc +regtests. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/snippets -// These tests check that postfix completions do and do not show up in -// certain cases. Tests for the postfix completion contents are under -// regtest. +go 1.18 + +-- postfix.go -- +package snippets func _() { - /* append! */ //@item(postfixAppend, "append!", "append and re-assign slice", "snippet") var foo []int foo.append //@rank(" //", postfixAppend) diff --git a/gopls/internal/regtest/marker/testdata/completion/snippet.txt b/gopls/internal/regtest/marker/testdata/completion/snippet.txt new file mode 100644 index 00000000000..eb0a4140b90 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/snippet.txt @@ -0,0 +1,77 @@ +This test checks basic completion snippet support. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/snippet + +-- snippet.go -- +package snippets + +// Pre-set this marker, as we don't have a "source" for it in this package. +// The comment is used to create a synthetic completion item. +// +// TODO(rfindley): allow completion markers to refer to ad-hoc items inline, +// without this trick. +/* Error() */ //@item(Error, "Error", "func() string", "method") + +type AliasType = int //@item(sigAliasType, "AliasType", "AliasType", "type") + +func foo(i int, b bool) {} //@item(snipFoo, "foo", "func(i int, b bool)", "func") +func bar(fn func()) func() {} //@item(snipBar, "bar", "func(fn func())", "func") +func baz(at AliasType, b bool) {} //@item(snipBaz, "baz", "func(at AliasType, b bool)", "func") + +type Foo struct { + Bar int //@item(snipFieldBar, "Bar", "int", "field") + Func func(at AliasType) error //@item(snipFieldFunc, "Func", "func(at AliasType) error", "field") +} + +func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz", "func() func()", "method") +func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method") +func (Foo) BazBaz(at AliasType) func() {} //@item(snipMethodBazBaz, "BazBaz", "func(at AliasType) func()", "method") + +func _() { + f //@snippet(" //", snipFoo, "foo(${1:})") + + bar //@snippet(" //", snipBar, "bar(${1:})") + + baz //@snippet(" //", snipBaz, "baz(${1:})") + baz() //@signature("(", "baz(at AliasType, b bool)", 0) + + bar(nil) //@snippet("(", snipBar, "bar") + bar(ba) //@snippet(")", snipBar, "bar(${1:})") + var f Foo + bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()") + (bar)(nil) //@snippet(")", snipBar, "bar(${1:})") + (f.Ba)() //@snippet(")", snipMethodBaz, "Baz()") + + Foo{ + B //@snippet(" //", snipFieldBar, "Bar: ${1:},") + } + + Foo{ + F //@snippet(" //", snipFieldFunc, "Func: ${1:},") + } + + Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1:}") + Foo{} //@snippet("}", snipFieldBar, "Bar: ${1:}") + + Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar") + + var err error + err.Error() //@snippet("E", Error, "Error()") + f.Baz() //@snippet("B", snipMethodBaz, "Baz()") + + f.Baz() //@snippet("(", snipMethodBazBar, "BazBar") + + f.Baz() //@snippet("B", snipMethodBazBaz, "BazBaz(${1:})") +} + +func _() { + type bar struct { + a int + b float64 //@item(snipBarB, "b", "float64") + } + bar{b} //@snippet("}", snipBarB, "b: ${1:}") +} diff --git a/gopls/internal/regtest/marker/testdata/completion/snippet_placeholder.txt b/gopls/internal/regtest/marker/testdata/completion/snippet_placeholder.txt new file mode 100644 index 00000000000..e19ccb06aa2 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/snippet_placeholder.txt @@ -0,0 +1,83 @@ +This test checks basic completion snippet support, using placeholders. + +Unlike the old marker tests, the new marker tests assume static configuration +(as defined by settings.json), and therefore there is duplication between this +test and snippet.txt. This is a price we pay so that we don't have to mutate +the server during testing. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "usePlaceholders": true +} + +-- go.mod -- +module golang.org/lsptests/snippet + +-- snippet.go -- +package snippets + +// Pre-set this marker, as we don't have a "source" for it in this package. +/* Error() */ //@item(Error, "Error", "func() string", "method") + +type AliasType = int //@item(sigAliasType, "AliasType", "AliasType", "type") + +func foo(i int, b bool) {} //@item(snipFoo, "foo", "func(i int, b bool)", "func") +func bar(fn func()) func() {} //@item(snipBar, "bar", "func(fn func())", "func") +func baz(at AliasType, b bool) {} //@item(snipBaz, "baz", "func(at AliasType, b bool)", "func") + +type Foo struct { + Bar int //@item(snipFieldBar, "Bar", "int", "field") + Func func(at AliasType) error //@item(snipFieldFunc, "Func", "func(at AliasType) error", "field") +} + +func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz", "func() func()", "method") +func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method") +func (Foo) BazBaz(at AliasType) func() {} //@item(snipMethodBazBaz, "BazBaz", "func(at AliasType) func()", "method") + +func _() { + f //@snippet(" //", snipFoo, "foo(${1:i int}, ${2:b bool})") + + bar //@snippet(" //", snipBar, "bar(${1:fn func()})") + + baz //@snippet(" //", snipBaz, "baz(${1:at AliasType}, ${2:b bool})") + baz() //@signature("(", "baz(at AliasType, b bool)", 0) + + bar(nil) //@snippet("(", snipBar, "bar") + bar(ba) //@snippet(")", snipBar, "bar(${1:fn func()})") + var f Foo + bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()") + (bar)(nil) //@snippet(")", snipBar, "bar(${1:fn func()})") + (f.Ba)() //@snippet(")", snipMethodBaz, "Baz()") + + Foo{ + B //@snippet(" //", snipFieldBar, "Bar: ${1:int},") + } + + Foo{ + F //@snippet(" //", snipFieldFunc, "Func: ${1:func(at AliasType) error},") + } + + Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1:int}") + Foo{} //@snippet("}", snipFieldBar, "Bar: ${1:int}") + + Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar") + + var err error + err.Error() //@snippet("E", Error, "Error()") + f.Baz() //@snippet("B", snipMethodBaz, "Baz()") + + f.Baz() //@snippet("(", snipMethodBazBar, "BazBar") + + f.Baz() //@snippet("B", snipMethodBazBaz, "BazBaz(${1:at AliasType})") +} + +func _() { + type bar struct { + a int + b float64 //@item(snipBarB, "b", "field") + } + bar{b} //@snippet("}", snipBarB, "b: ${1:float64}") +} diff --git a/gopls/internal/regtest/marker/testdata/completion/testy.txt b/gopls/internal/regtest/marker/testdata/completion/testy.txt index 1fbc7a272e3..983fc09160b 100644 --- a/gopls/internal/regtest/marker/testdata/completion/testy.txt +++ b/gopls/internal/regtest/marker/testdata/completion/testy.txt @@ -52,10 +52,10 @@ func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *test } func _() { - _ = snippets.X(nil) //@signature("nil", "X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias") + _ = snippets.X(nil) //@signature("nil", "X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias", 0) var _ sig.Alias } func issue63578(err error) { - err.Error() //@signature(")", "Error()") + err.Error() //@signature(")", "Error()", 0) } diff --git a/gopls/internal/regtest/marker/testdata/signature/signature.txt b/gopls/internal/regtest/marker/testdata/signature/signature.txt new file mode 100644 index 00000000000..973ff50165d --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/signature/signature.txt @@ -0,0 +1,206 @@ +This test exercises basic tests for signature help. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests + +go 1.18 + +-- signature/signature.go -- +// Package signature has tests for signature help. +package signature + +import ( + "bytes" + "encoding/json" + "math/big" +) + +func Foo(a string, b int) (c bool) { + return +} + +func Bar(float64, ...byte) { +} + +type myStruct struct{} + +func (*myStruct) foo(e *json.Decoder) (*big.Int, error) { + return nil, nil +} + +type MyType struct{} + +type MyFunc func(foo int) string + +type Alias = int +type OtherAlias = int +type StringAlias = string + +func AliasSlice(a []*Alias) (b Alias) { return 0 } +func AliasMap(a map[*Alias]StringAlias) (b, c map[*Alias]StringAlias) { return nil, nil } +func OtherAliasMap(a, b map[Alias]OtherAlias) map[Alias]OtherAlias { return nil } + +func Qux() { + Foo("foo", 123) //@signature("(", "Foo(a string, b int) (c bool)", 0) + Foo("foo", 123) //@signature("123", "Foo(a string, b int) (c bool)", 1) + Foo("foo", 123) //@signature(",", "Foo(a string, b int) (c bool)", 0) + Foo("foo", 123) //@signature(" 1", "Foo(a string, b int) (c bool)", 1) + Foo("foo", 123) //@signature(")", "Foo(a string, b int) (c bool)", 1) + + Bar(13.37, 0x13) //@signature("13.37", "Bar(float64, ...byte)", 0) + Bar(13.37, 0x37) //@signature("0x37", "Bar(float64, ...byte)", 1) + Bar(13.37, 1, 2, 3, 4) //@signature("4", "Bar(float64, ...byte)", 1) + + fn := func(hi, there string) func(i int) rune { + return func(int) rune { return 0 } + } + + fn("hi", "there") //@signature("hi", "", 0) + fn("hi", "there") //@signature(",", "fn(hi string, there string) func(i int) rune", 0) + fn("hi", "there")(1) //@signature("1", "func(i int) rune", 0) + + fnPtr := &fn + (*fnPtr)("hi", "there") //@signature(",", "func(hi string, there string) func(i int) rune", 0) + + var fnIntf interface{} = Foo + fnIntf.(func(string, int) bool)("hi", 123) //@signature("123", "func(string, int) bool", 1) + + (&bytes.Buffer{}).Next(2) //@signature("2", "Next(n int) []byte", 0) + + myFunc := MyFunc(func(n int) string { return "" }) + myFunc(123) //@signature("123", "myFunc(foo int) string", 0) + + var ms myStruct + ms.foo(nil) //@signature("nil", "foo(e *json.Decoder) (*big.Int, error)", 0) + + _ = make([]int, 1, 2) //@signature("2", "make(t Type, size ...int) Type", 1) + + Foo(myFunc(123), 456) //@signature("myFunc", "Foo(a string, b int) (c bool)", 0) + Foo(myFunc(123), 456) //@signature("123", "myFunc(foo int) string", 0) + + panic("oops!") //@signature(")", "panic(v any)", 0) + println("hello", "world") //@signature(",", "println(args ...Type)", 0) + + Hello(func() { + //@signature("//", "", 0) + }) + + AliasSlice() //@signature(")", "AliasSlice(a []*Alias) (b Alias)", 0) + AliasMap() //@signature(")", "AliasMap(a map[*Alias]StringAlias) (b map[*Alias]StringAlias, c map[*Alias]StringAlias)", 0) + OtherAliasMap() //@signature(")", "OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias", 0) +} + +func Hello(func()) {} + +-- signature/signature2.go -- +package signature + +func _() { + Foo(//@signature("//", "Foo(a string, b int) (c bool)", 0) +} + +-- signature/signature3.go -- +package signature + +func _() { + Foo("hello",//@signature("//", "Foo(a string, b int) (c bool)", 1) +} + +-- signature/signature_test.go -- +package signature_test + +import ( + "testing" + + sig "golang.org/lsptests/signature" +) + +func TestSignature(t *testing.T) { + sig.AliasSlice() //@signature(")", "AliasSlice(a []*sig.Alias) (b sig.Alias)", 0) + sig.AliasMap() //@signature(")", "AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias)", 0) + sig.OtherAliasMap() //@signature(")", "OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias", 0) +} + +-- snippets/snippets.go -- +package snippets + +import ( + "golang.org/lsptests/signature" +) + +type CoolAlias = int //@item(CoolAlias, "CoolAlias", "int", "type") + +type structy struct { + x signature.MyType +} + +func X(_ map[signature.Alias]CoolAlias) (map[signature.Alias]CoolAlias) { + return nil +} + +func _() { + X() //@signature(")", "X(_ map[signature.Alias]CoolAlias) map[signature.Alias]CoolAlias", 0) + _ = signature.MyType{} //@item(literalMyType, "signature.MyType{}", "", "var") + s := structy{ + x: //@snippet(" //", literalMyType, "signature.MyType{\\}") + } +} + +-- importedcomplit/importedcomplit.go -- +package importedcomplit + +import ( + // TODO(rfindley): re-enable after moving to new framework + // "golang.org/lsptests/foo" + + // import completions (separate blocks to avoid comment alignment) + "crypto/elli" //@complete("\" //", cryptoImport) + + "fm" //@complete("\" //", fmtImport) + + "go/pars" //@complete("\" //", parserImport) + + namedParser "go/pars" //@complete("\" //", parserImport) + + "golang.org/lspte" //@complete("\" //", lsptestsImport) + + "golang.org/lsptests/sign" //@complete("\" //", signatureImport) + + "golang.org/lsptests/sign" //@complete("ests", lsptestsImport) + + "golang.org/lsptests/signa" //@complete("na\" //", signatureImport) +) + +func _() { + var V int //@item(icVVar, "V", "int", "var") + + // TODO(rfindley): re-enable after moving to new framework + // _ = foo.StructFoo{V} // complete("}", Value, icVVar) +} + +func _() { + var ( + aa string //@item(icAAVar, "aa", "string", "var") + ab int //@item(icABVar, "ab", "int", "var") + ) + + // TODO(rfindley): re-enable after moving to new framework + // _ = foo.StructFoo{a} // complete("}", abVar, aaVar) + + var s struct { + AA string //@item(icFieldAA, "AA", "string", "field") + AB int //@item(icFieldAB, "AB", "int", "field") + } + + // TODO(rfindley): re-enable after moving to new framework + //_ = foo.StructFoo{s.} // complete("}", icFieldAB, icFieldAA) +} + +/* "fmt" */ //@item(fmtImport, "fmt", "\"fmt\"", "package") +/* "go/parser" */ //@item(parserImport, "parser", "\"go/parser\"", "package") +/* "golang.org/lsptests/signature" */ //@item(signatureImport, "signature", "\"golang.org/lsptests/signature\"", "package") +/* "golang.org/lsptests/" */ //@item(lsptestsImport, "lsptests/", "\"golang.org/lsptests/\"", "package") +/* "crypto/elliptic" */ //@item(cryptoImport, "elliptic", "\"crypto/elliptic\"", "package") From b90ce15c5bca1e7b51bc5ee5fe202dc46b90bd0e Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 25 Oct 2023 15:20:34 -0400 Subject: [PATCH 050/100] gopls/internal/regtest/marker: port statements completion tests For golang/go#54845 Change-Id: I2257cac9ac7d08bffd4ba90eec4dcb5b750fcc44 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537738 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- .../lsp/testdata/statements/append.go | 42 ------ .../statements/if_err_check_return.go | 27 ---- .../statements/if_err_check_return_2.go | 12 -- .../testdata/statements/if_err_check_test.go | 20 --- .../internal/lsp/testdata/summary.txt.golden | 4 +- .../marker/testdata/completion/statements.txt | 121 ++++++++++++++++++ 6 files changed, 123 insertions(+), 103 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/statements/append.go delete mode 100644 gopls/internal/lsp/testdata/statements/if_err_check_return.go delete mode 100644 gopls/internal/lsp/testdata/statements/if_err_check_return_2.go delete mode 100644 gopls/internal/lsp/testdata/statements/if_err_check_test.go create mode 100644 gopls/internal/regtest/marker/testdata/completion/statements.txt diff --git a/gopls/internal/lsp/testdata/statements/append.go b/gopls/internal/lsp/testdata/statements/append.go deleted file mode 100644 index 0eea85a2825..00000000000 --- a/gopls/internal/lsp/testdata/statements/append.go +++ /dev/null @@ -1,42 +0,0 @@ -package statements - -func _() { - type mySlice []int - - var ( - abc []int //@item(stmtABC, "abc", "[]int", "var") - abcdef mySlice //@item(stmtABCDEF, "abcdef", "mySlice", "var") - ) - - /* abcdef = append(abcdef, ) */ //@item(stmtABCDEFAssignAppend, "abcdef = append(abcdef, )", "", "func") - - // don't offer "abc = append(abc, )" because "abc" isn't necessarily - // better than "abcdef". - abc //@complete(" //", stmtABC, stmtABCDEF) - - abcdef //@complete(" //", stmtABCDEF, stmtABCDEFAssignAppend) - - /* append(abc, ) */ //@item(stmtABCAppend, "append(abc, )", "", "func") - - abc = app //@snippet(" //", stmtABCAppend, "append(abc, ${1:})", "append(abc, ${1:})") -} - -func _() { - var s struct{ xyz []int } - - /* xyz = append(s.xyz, ) */ //@item(stmtXYZAppend, "xyz = append(s.xyz, )", "", "func") - - s.x //@snippet(" //", stmtXYZAppend, "xyz = append(s.xyz, ${1:})", "xyz = append(s.xyz, ${1:})") - - /* s.xyz = append(s.xyz, ) */ //@item(stmtDeepXYZAppend, "s.xyz = append(s.xyz, )", "", "func") - - sx //@snippet(" //", stmtDeepXYZAppend, "s.xyz = append(s.xyz, ${1:})", "s.xyz = append(s.xyz, ${1:})") -} - -func _() { - var foo [][]int - - /* append(foo[0], ) */ //@item(stmtFooAppend, "append(foo[0], )", "", "func") - - foo[0] = app //@complete(" //"),snippet(" //", stmtFooAppend, "append(foo[0], ${1:})", "append(foo[0], ${1:})") -} diff --git a/gopls/internal/lsp/testdata/statements/if_err_check_return.go b/gopls/internal/lsp/testdata/statements/if_err_check_return.go deleted file mode 100644 index e82b7833379..00000000000 --- a/gopls/internal/lsp/testdata/statements/if_err_check_return.go +++ /dev/null @@ -1,27 +0,0 @@ -package statements - -import ( - "bytes" - "io" - "os" -) - -func one() (int, float32, io.Writer, *int, []int, bytes.Buffer, error) { - /* if err != nil { return err } */ //@item(stmtOneIfErrReturn, "if err != nil { return err }", "", "") - /* err != nil { return err } */ //@item(stmtOneErrReturn, "err != nil { return err }", "", "") - - _, err := os.Open("foo") - //@snippet("", stmtOneIfErrReturn, "", "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") - - _, err = os.Open("foo") - i //@snippet(" //", stmtOneIfErrReturn, "", "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") - - _, err = os.Open("foo") - if er //@snippet(" //", stmtOneErrReturn, "", "err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") - - _, err = os.Open("foo") - if //@snippet(" //", stmtOneIfErrReturn, "", "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") - - _, err = os.Open("foo") - if //@snippet("//", stmtOneIfErrReturn, "", "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") -} diff --git a/gopls/internal/lsp/testdata/statements/if_err_check_return_2.go b/gopls/internal/lsp/testdata/statements/if_err_check_return_2.go deleted file mode 100644 index e2dce804f4a..00000000000 --- a/gopls/internal/lsp/testdata/statements/if_err_check_return_2.go +++ /dev/null @@ -1,12 +0,0 @@ -package statements - -import "os" - -func two() error { - var s struct{ err error } - - /* if s.err != nil { return s.err } */ //@item(stmtTwoIfErrReturn, "if s.err != nil { return s.err }", "", "") - - _, s.err = os.Open("foo") - //@snippet("", stmtTwoIfErrReturn, "", "if s.err != nil {\n\treturn ${1:s.err}\n\\}") -} diff --git a/gopls/internal/lsp/testdata/statements/if_err_check_test.go b/gopls/internal/lsp/testdata/statements/if_err_check_test.go deleted file mode 100644 index 6de58787981..00000000000 --- a/gopls/internal/lsp/testdata/statements/if_err_check_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package statements - -import ( - "os" - "testing" -) - -func TestErr(t *testing.T) { - /* if err != nil { t.Fatal(err) } */ //@item(stmtOneIfErrTFatal, "if err != nil { t.Fatal(err) }", "", "") - - _, err := os.Open("foo") - //@snippet("", stmtOneIfErrTFatal, "", "if err != nil {\n\tt.Fatal(err)\n\\}") -} - -func BenchmarkErr(b *testing.B) { - /* if err != nil { b.Fatal(err) } */ //@item(stmtOneIfErrBFatal, "if err != nil { b.Fatal(err) }", "", "") - - _, err := os.Open("foo") - //@snippet("", stmtOneIfErrBFatal, "", "if err != nil {\n\tb.Fatal(err)\n\\}") -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 58a709d8009..2681ab20914 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,7 +1,7 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 181 -CompletionSnippetCount = 53 +CompletionsCount = 178 +CompletionSnippetCount = 41 DeepCompletionsCount = 5 FuzzyCompletionsCount = 8 RankedCompletionsCount = 163 diff --git a/gopls/internal/regtest/marker/testdata/completion/statements.txt b/gopls/internal/regtest/marker/testdata/completion/statements.txt new file mode 100644 index 00000000000..d013fefa5d6 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/statements.txt @@ -0,0 +1,121 @@ +This test exercises completion around various statements. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "usePlaceholders": true +} + +-- go.mod -- +module golang.org/lsptests/statements + +-- append.go -- +package statements + +func _() { + type mySlice []int + + var ( + abc []int //@item(stmtABC, "abc", "[]int", "var") + abcdef mySlice //@item(stmtABCDEF, "abcdef", "mySlice", "var") + ) + + /* abcdef = append(abcdef, ) */ //@item(stmtABCDEFAssignAppend, "abcdef = append(abcdef, )", "", "func") + + // don't offer "abc = append(abc, )" because "abc" isn't necessarily + // better than "abcdef". + abc //@complete(" //", stmtABC, stmtABCDEF) + + abcdef //@complete(" //", stmtABCDEF, stmtABCDEFAssignAppend) + + /* append(abc, ) */ //@item(stmtABCAppend, "append(abc, )", "", "func") + + abc = app //@snippet(" //", stmtABCAppend, "append(abc, ${1:})") +} + +func _() { + var s struct{ xyz []int } + + /* xyz = append(s.xyz, ) */ //@item(stmtXYZAppend, "xyz = append(s.xyz, )", "", "func") + + s.x //@snippet(" //", stmtXYZAppend, "xyz = append(s.xyz, ${1:})") + + /* s.xyz = append(s.xyz, ) */ //@item(stmtDeepXYZAppend, "s.xyz = append(s.xyz, )", "", "func") + + sx //@snippet(" //", stmtDeepXYZAppend, "s.xyz = append(s.xyz, ${1:})") +} + +func _() { + var foo [][]int + + /* append(foo[0], ) */ //@item(stmtFooAppend, "append(foo[0], )", "", "func") + + foo[0] = app //@complete(" //", stmtFooAppend),snippet(" //", stmtFooAppend, "append(foo[0], ${1:})") +} + +-- if_err_check_return.go -- +package statements + +import ( + "bytes" + "io" + "os" +) + +func one() (int, float32, io.Writer, *int, []int, bytes.Buffer, error) { + /* if err != nil { return err } */ //@item(stmtOneIfErrReturn, "if err != nil { return err }", "", "") + /* err != nil { return err } */ //@item(stmtOneErrReturn, "err != nil { return err }", "", "") + + _, err := os.Open("foo") + //@snippet("", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") + + _, err = os.Open("foo") + i //@snippet(" //", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") + + _, err = os.Open("foo") + if er //@snippet(" //", stmtOneErrReturn, "err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") + + _, err = os.Open("foo") + if //@snippet(" //", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") + + _, err = os.Open("foo") + if //@snippet("//", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") +} + +-- if_err_check_return2.go -- +package statements + +import "os" + +func two() error { + var s struct{ err error } + + /* if s.err != nil { return s.err } */ //@item(stmtTwoIfErrReturn, "if s.err != nil { return s.err }", "", "") + + _, s.err = os.Open("foo") + //@snippet("", stmtTwoIfErrReturn, "if s.err != nil {\n\treturn ${1:s.err}\n\\}") +} + +-- if_err_check_test.go -- +package statements + +import ( + "os" + "testing" +) + +func TestErr(t *testing.T) { + /* if err != nil { t.Fatal(err) } */ //@item(stmtOneIfErrTFatal, "if err != nil { t.Fatal(err) }", "", "") + + _, err := os.Open("foo") + //@snippet("", stmtOneIfErrTFatal, "if err != nil {\n\tt.Fatal(err)\n\\}") +} + +func BenchmarkErr(b *testing.B) { + /* if err != nil { b.Fatal(err) } */ //@item(stmtOneIfErrBFatal, "if err != nil { b.Fatal(err) }", "", "") + + _, err := os.Open("foo") + //@snippet("", stmtOneIfErrBFatal, "if err != nil {\n\tb.Fatal(err)\n\\}") +} From df7023e0136c8fe4964d70be19eed36395caf48d Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 26 Oct 2023 17:00:09 -0400 Subject: [PATCH 051/100] gopls/internal/regtest/marker: port all deep and fuzzy completion tests Port @deep and @fuzzy markers, which were just @complete with different options. Since we support settings.json in the new marker tests, there was no need for new markers. For golang/go#54845 Change-Id: I56f670b51084fa20e71c607a7f20e0372c793598 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537881 Auto-Submit: Robert Findley Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/completion_test.go | 26 ------ gopls/internal/lsp/source/options.go | 3 + .../internal/lsp/testdata/summary.txt.golden | 8 +- gopls/internal/lsp/tests/tests.go | 28 ------- .../marker/testdata/completion/address.txt} | 44 ++++++---- .../marker/testdata/completion/deep.txt} | 80 ++++++------------- .../marker/testdata/completion/deep2.txt | 65 +++++++++++++++ .../marker/testdata/completion/fuzzy.txt} | 25 +++--- 8 files changed, 140 insertions(+), 139 deletions(-) rename gopls/internal/{lsp/testdata/address/address.go => regtest/marker/testdata/completion/address.txt} (65%) rename gopls/internal/{lsp/testdata/deep/deep.go => regtest/marker/testdata/completion/deep.txt} (57%) create mode 100644 gopls/internal/regtest/marker/testdata/completion/deep2.txt rename gopls/internal/{lsp/testdata/fuzzymatch/fuzzymatch.go => regtest/marker/testdata/completion/fuzzy.txt} (59%) diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index 06a6a09aa1a..2c8efa1ecb0 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -49,32 +49,6 @@ func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.C } } -func (r *runner) DeepCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - got := r.callCompletion(t, src, func(opts *source.Options) { - opts.DeepCompletion = true - opts.Matcher = source.CaseInsensitive - opts.CompleteUnimported = false - }) - got = tests.FilterBuiltins(src, got) - want := expected(t, test, items) - if diff := tests.DiffCompletionItems(want, got); diff != "" { - t.Errorf("mismatching completion items (-want +got):\n%s", diff) - } -} - -func (r *runner) FuzzyCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - got := r.callCompletion(t, src, func(opts *source.Options) { - opts.DeepCompletion = true - opts.Matcher = source.Fuzzy - opts.CompleteUnimported = false - }) - got = tests.FilterBuiltins(src, got) - want := expected(t, test, items) - if diff := tests.DiffCompletionItems(want, got); diff != "" { - t.Errorf("mismatching completion items (-want +got):\n%s", diff) - } -} - func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { got := r.callCompletion(t, src, func(opts *source.Options) { opts.Matcher = source.CaseSensitive diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index 44bc7737f9a..e03c4c614b8 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -595,6 +595,9 @@ type InternalOptions struct { // LiteralCompletions controls whether literal candidates such as // "&someStruct{}" are offered. Tests disable this flag to simplify // their expected values. + // + // TODO(rfindley): this is almost unnecessary now. Remove it along with the + // old marker tests. LiteralCompletions bool // VerboseWorkDoneProgress controls whether the LSP server should send diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 2681ab20914..19344e306f5 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,10 +1,8 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 178 -CompletionSnippetCount = 41 -DeepCompletionsCount = 5 -FuzzyCompletionsCount = 8 -RankedCompletionsCount = 163 +CompletionsCount = 177 +CompletionSnippetCount = 27 +RankedCompletionsCount = 151 CaseSensitiveCompletionsCount = 4 SemanticTokenCount = 3 SuggestedFixCount = 80 diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 1e682f37c15..1ad63c2c2e5 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -58,8 +58,6 @@ type CallHierarchy = map[span.Span]*CallHierarchyResult type CompletionItems = map[token.Pos]*completion.CompletionItem type Completions = map[span.Span][]Completion type CompletionSnippets = map[span.Span][]CompletionSnippet -type DeepCompletions = map[span.Span][]Completion -type FuzzyCompletions = map[span.Span][]Completion type CaseSensitiveCompletions = map[span.Span][]Completion type RankCompletions = map[span.Span][]Completion type SemanticTokens = []span.Span @@ -79,8 +77,6 @@ type Data struct { CompletionItems CompletionItems Completions Completions CompletionSnippets CompletionSnippets - DeepCompletions DeepCompletions - FuzzyCompletions FuzzyCompletions CaseSensitiveCompletions CaseSensitiveCompletions RankCompletions RankCompletions SemanticTokens SemanticTokens @@ -114,8 +110,6 @@ type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) Completion(*testing.T, span.Span, Completion, CompletionItems) CompletionSnippet(*testing.T, span.Span, CompletionSnippet, bool, CompletionItems) - DeepCompletion(*testing.T, span.Span, Completion, CompletionItems) - FuzzyCompletion(*testing.T, span.Span, Completion, CompletionItems) CaseSensitiveCompletion(*testing.T, span.Span, Completion, CompletionItems) RankCompletion(*testing.T, span.Span, Completion, CompletionItems) SemanticTokens(*testing.T, span.Span) @@ -235,8 +229,6 @@ func load(t testing.TB, mode string, dir string) *Data { CompletionItems: make(CompletionItems), Completions: make(Completions), CompletionSnippets: make(CompletionSnippets), - DeepCompletions: make(DeepCompletions), - FuzzyCompletions: make(FuzzyCompletions), RankCompletions: make(RankCompletions), CaseSensitiveCompletions: make(CaseSensitiveCompletions), Renames: make(Renames), @@ -500,16 +492,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("DeepCompletion", func(t *testing.T) { - t.Helper() - eachCompletion(t, data.DeepCompletions, tests.DeepCompletion) - }) - - t.Run("FuzzyCompletion", func(t *testing.T) { - t.Helper() - eachCompletion(t, data.FuzzyCompletions, tests.FuzzyCompletion) - }) - t.Run("CaseSensitiveCompletion", func(t *testing.T) { t.Helper() eachCompletion(t, data.CaseSensitiveCompletions, tests.CaseSensitiveCompletion) @@ -665,8 +647,6 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy)) fmt.Fprintf(buf, "CompletionsCount = %v\n", countCompletions(data.Completions)) fmt.Fprintf(buf, "CompletionSnippetCount = %v\n", snippetCount) - fmt.Fprintf(buf, "DeepCompletionsCount = %v\n", countCompletions(data.DeepCompletions)) - fmt.Fprintf(buf, "FuzzyCompletionsCount = %v\n", countCompletions(data.FuzzyCompletions)) fmt.Fprintf(buf, "RankedCompletionsCount = %v\n", countCompletions(data.RankCompletions)) fmt.Fprintf(buf, "CaseSensitiveCompletionsCount = %v\n", countCompletions(data.CaseSensitiveCompletions)) fmt.Fprintf(buf, "SemanticTokenCount = %v\n", len(data.SemanticTokens)) @@ -764,14 +744,6 @@ func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []t }) } switch typ { - case CompletionDeep: - return func(src span.Span, expected []token.Pos) { - result(data.DeepCompletions, src, expected) - } - case CompletionFuzzy: - return func(src span.Span, expected []token.Pos) { - result(data.FuzzyCompletions, src, expected) - } case CompletionRank: return func(src span.Span, expected []token.Pos) { result(data.RankCompletions, src, expected) diff --git a/gopls/internal/lsp/testdata/address/address.go b/gopls/internal/regtest/marker/testdata/completion/address.txt similarity index 65% rename from gopls/internal/lsp/testdata/address/address.go rename to gopls/internal/regtest/marker/testdata/completion/address.txt index 3f1c2fa8de2..676b9ad9b55 100644 --- a/gopls/internal/lsp/testdata/address/address.go +++ b/gopls/internal/regtest/marker/testdata/completion/address.txt @@ -1,3 +1,17 @@ +This test exercises the reference and dereference completion modifiers. + +TODO: remove the need to set "literalCompletions" here, as this is one of the +few places this setting is needed. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests + +go 1.18 + +-- address/address.go -- package address func wantsPtr(*int) {} @@ -13,28 +27,28 @@ func _() { b int //@item(addrB, "b", "int", "var") ) - wantsPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b", "&b") - wantsPtr(&b) //@snippet(")", addrB, "b", "b") + wantsPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b") + wantsPtr(&b) //@snippet(")", addrB, "b") - wantsVariadicPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b", "&b") + wantsVariadicPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b") var s foo s.c //@item(addrDeepC, "s.c", "int", "field") - wantsPtr() //@snippet(")", addrDeepC, "&s.c", "&s.c") - wantsPtr(s) //@snippet(")", addrDeepC, "&s.c", "&s.c") - wantsPtr(&s) //@snippet(")", addrDeepC, "s.c", "s.c") + wantsPtr() //@snippet(")", addrDeepC, "&s.c") + wantsPtr(s) //@snippet(")", addrDeepC, "&s.c") + wantsPtr(&s) //@snippet(")", addrDeepC, "s.c") // don't add "&" in item (it gets added as an additional edit) - wantsPtr(&s.c) //@snippet(")", addrFieldC, "c", "c") + wantsPtr(&s.c) //@snippet(")", addrFieldC, "c") // check dereferencing as well var c *int //@item(addrCPtr, "c", "*int", "var") - var _ int = _ //@rank("_ //", addrCPtr, addrA),snippet("_ //", addrCPtr, "*c", "*c") + var _ int = _ //@rank("_ //", addrCPtr, addrA),snippet("_ //", addrCPtr, "*c") - wantsVariadic() //@rank(")", addrCPtr, addrA),snippet(")", addrCPtr, "*c", "*c") + wantsVariadic() //@rank(")", addrCPtr, addrA),snippet(")", addrCPtr, "*c") var d **int //@item(addrDPtr, "d", "**int", "var") - var _ int = _ //@rank("_ //", addrDPtr, addrA),snippet("_ //", addrDPtr, "**d", "**d") + var _ int = _ //@rank("_ //", addrDPtr, addrA),snippet("_ //", addrDPtr, "**d") type namedPtr *int var np namedPtr //@item(addrNamedPtr, "np", "namedPtr", "var") @@ -42,10 +56,10 @@ func _() { var _ int = _ //@rank("_ //", addrNamedPtr, addrA) // don't get tripped up by recursive pointer type - type dontMessUp *dontMessUp + type dontMessUp *dontMessUp //@item(dontMessUp, "dontMessUp", "*dontMessUp", "type") var dmu *dontMessUp //@item(addrDMU, "dmu", "*dontMessUp", "var") - var _ int = dmu //@complete(" //", addrDMU) + var _ int = dmu //@complete(" //", addrDMU, dontMessUp) } func (f foo) ptr() *foo { return &f } @@ -59,8 +73,8 @@ func _() { // addressable getFoo().ptr().c //@item(addrGetFooPtrC, "getFoo().ptr().c", "int", "field") - wantsPtr() //@rank(addrGetFooPtrC, addrGetFooC),snippet(")", addrGetFooPtrC, "&getFoo().ptr().c", "&getFoo().ptr().c") - wantsPtr(&g) //@rank(addrGetFooPtrC, addrGetFooC),snippet(")", addrGetFooPtrC, "getFoo().ptr().c", "getFoo().ptr().c") + wantsPtr() //@snippet(")", addrGetFooPtrC, "&getFoo().ptr().c") + wantsPtr(&g) //@snippet(")", addrGetFooPtrC, "getFoo().ptr().c") } type nested struct { @@ -74,5 +88,5 @@ func _() { getNested().f.ptr().c //@item(addrNestedPtrC, "getNested().f.ptr().c", "int", "field") // addrNestedC is not addressable, so rank lower - wantsPtr(getNestedfc) //@fuzzy(")", addrNestedPtrC, addrNestedC) + wantsPtr(getNestedfc) //@complete(")", addrNestedPtrC, addrNestedC) } diff --git a/gopls/internal/lsp/testdata/deep/deep.go b/gopls/internal/regtest/marker/testdata/completion/deep.txt similarity index 57% rename from gopls/internal/lsp/testdata/deep/deep.go rename to gopls/internal/regtest/marker/testdata/completion/deep.txt index 6908824f82f..68d306a8c32 100644 --- a/gopls/internal/lsp/testdata/deep/deep.go +++ b/gopls/internal/regtest/marker/testdata/completion/deep.txt @@ -1,3 +1,20 @@ +This test exercises deep completion. + +-- settings.json -- +{ + "completeUnimported": false, + "matcher": "caseInsensitive" +} + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests + +go 1.18 + +-- deep/deep.go -- package deep import "context" @@ -14,9 +31,9 @@ func wantsDeepB(deepB) {} func _() { var a deepA //@item(deepAVar, "a", "deepA", "var") a.b //@item(deepABField, "a.b", "deepB", "field") - wantsDeepB(a) //@deep(")", deepABField, deepAVar) + wantsDeepB(a) //@complete(")", deepABField, deepAVar) - deepA{a} //@snippet("}", deepABField, "a.b", "a.b") + deepA{a} //@snippet("}", deepABField, "a.b") } func wantsContext(context.Context) {} @@ -32,7 +49,7 @@ func _() { var cork struct{ err error } cork.err //@item(deepCorkErr, "cork.err", "error", "field") context //@item(deepContextPkg, "context", "\"context\"", "package") - var _ error = co //@rank(" //", deepCorkErr, deepContextPkg) + var _ error = co // rank(" //", deepCorkErr, deepContextPkg) } func _() { @@ -42,7 +59,7 @@ func _() { } var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var") circle.deepCircle //@item(deepCircleField, "circle.deepCircle", "*deepCircle", "field") - var _ deepCircle = circ //@deep(" //", deepCircle, deepCircleField),snippet(" //", deepCircleField, "*circle.deepCircle", "*circle.deepCircle") + var _ deepCircle = circ //@complete(" //", deepCircle, deepCircleField),snippet(" //", deepCircleField, "*circle.deepCircle") } func _() { @@ -60,7 +77,7 @@ func _() { var a deepEmbedA //@item(deepEmbedA, "a", "deepEmbedA", "var") a.deepEmbedB //@item(deepEmbedB, "a.deepEmbedB", "deepEmbedB", "field") a.deepEmbedC //@item(deepEmbedC, "a.deepEmbedC", "deepEmbedC", "field") - wantsC(a) //@deep(")", deepEmbedC, deepEmbedA, deepEmbedB) + wantsC(a) //@complete(")", deepEmbedC, deepEmbedA, deepEmbedB) } func _() { @@ -70,7 +87,7 @@ func _() { } nested{ - a: 123, //@deep(" //", deepNestedField) + a: 123, //@complete(" //", deepNestedField) } } @@ -89,54 +106,5 @@ func _() { // "a.d" should be ranked above the deeper "a.b.c" var i int - i = a //@deep(" //", deepAD, deepABC, deepA, deepAB) -} - -type foo struct { - b bar -} - -func (f foo) bar() bar { - return f.b -} - -func (f foo) barPtr() *bar { - return &f.b -} - -type bar struct{} - -func (b bar) valueReceiver() int { - return 0 -} - -func (b *bar) ptrReceiver() int { - return 0 -} - -func _() { - var ( - i int - f foo - ) - - f.bar().valueReceiver //@item(deepBarValue, "f.bar().valueReceiver", "func() int", "method") - f.barPtr().ptrReceiver //@item(deepBarPtrPtr, "f.barPtr().ptrReceiver", "func() int", "method") - f.barPtr().valueReceiver //@item(deepBarPtrValue, "f.barPtr().valueReceiver", "func() int", "method") - - i = fbar //@fuzzy(" //", deepBarValue, deepBarPtrPtr, deepBarPtrValue) -} - -func (b baz) Thing() struct{ val int } { - return b.thing -} - -type baz struct { - thing struct{ val int } -} - -func (b baz) _() { - b.Thing().val //@item(deepBazMethVal, "b.Thing().val", "int", "field") - b.thing.val //@item(deepBazFieldVal, "b.thing.val", "int", "field") - var _ int = bval //@rank(" //", deepBazFieldVal, deepBazMethVal) + i = a //@complete(" //", deepAD, deepABC, deepA, deepAB) } diff --git a/gopls/internal/regtest/marker/testdata/completion/deep2.txt b/gopls/internal/regtest/marker/testdata/completion/deep2.txt new file mode 100644 index 00000000000..cf343ce4e3f --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/deep2.txt @@ -0,0 +1,65 @@ +This test exercises deep completion. + +It was originally bundled with deep.go, but is split into a separate test as +the new marker tests do not permit mutating server options for individual +marks. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests + +go 1.18 + +-- deep/deep2.go -- +package deep + +type foo struct { + b bar +} + +func (f foo) bar() bar { + return f.b +} + +func (f foo) barPtr() *bar { + return &f.b +} + +type bar struct{} + +func (b bar) valueReceiver() int { + return 0 +} + +func (b *bar) ptrReceiver() int { + return 0 +} + +func _() { + var ( + i int + f foo + ) + + f.bar().valueReceiver //@item(deepBarValue, "f.bar().valueReceiver", "func() int", "method") + f.barPtr().ptrReceiver //@item(deepBarPtrPtr, "f.barPtr().ptrReceiver", "func() int", "method") + f.barPtr().valueReceiver //@item(deepBarPtrValue, "f.barPtr().valueReceiver", "func() int", "method") + + i = fbar //@complete(" //", deepBarValue, deepBarPtrPtr, deepBarPtrValue) +} + +func (b baz) Thing() struct{ val int } { + return b.thing +} + +type baz struct { + thing struct{ val int } +} + +func (b baz) _() { + b.Thing().val //@item(deepBazMethVal, "b.Thing().val", "int", "field") + b.thing.val //@item(deepBazFieldVal, "b.thing.val", "int", "field") + var _ int = bval //@rank(" //", deepBazFieldVal, deepBazMethVal) +} diff --git a/gopls/internal/lsp/testdata/fuzzymatch/fuzzymatch.go b/gopls/internal/regtest/marker/testdata/completion/fuzzy.txt similarity index 59% rename from gopls/internal/lsp/testdata/fuzzymatch/fuzzymatch.go rename to gopls/internal/regtest/marker/testdata/completion/fuzzy.txt index 73268f553e2..2a94dce7a2d 100644 --- a/gopls/internal/lsp/testdata/fuzzymatch/fuzzymatch.go +++ b/gopls/internal/regtest/marker/testdata/completion/fuzzy.txt @@ -1,7 +1,14 @@ -// 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. +This test exercises fuzzy completion matching. +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests + +go 1.18 + +-- fuzzy/fuzzy.go -- package fuzzy func _() { @@ -13,13 +20,13 @@ func _() { a.fabar //@item(fuzzFabarField, "a.fabar", "int", "field") a.fooBar //@item(fuzzFooBarField, "a.fooBar", "string", "field") - afa //@fuzzy(" //", fuzzFabarField, fuzzFooBarField) - afb //@fuzzy(" //", fuzzFooBarField, fuzzFabarField) + afa //@complete(" //", fuzzFabarField, fuzzFooBarField) + afb //@complete(" //", fuzzFooBarField, fuzzFabarField) - fab //@fuzzy(" //", fuzzFabarField) + fab //@complete(" //", fuzzFabarField) var myString string - myString = af //@fuzzy(" //", fuzzFooBarField, fuzzFabarField) + myString = af //@complete(" //", fuzzFooBarField, fuzzFabarField) var b struct { c struct { @@ -40,9 +47,9 @@ func _() { b.c.d.e.abc //@item(fuzzABCstring, "b.c.d.e.abc", "string", "field") // in depth order by default - abc //@fuzzy(" //", fuzzABCInt, fuzzABCbool, fuzzABCfloat) + abc //@complete(" //", fuzzABCInt, fuzzABCbool, fuzzABCfloat) // deep candidate that matches expected type should still ranked first var s string - s = abc //@fuzzy(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool) + s = abc //@complete(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool) } From 50cdf2a811b1d9a92aba634081ce65f094f7bd89 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 26 Oct 2023 17:07:43 -0400 Subject: [PATCH 052/100] gopls/internal/regtest/marker: port case sensitive completion tests Like others, these are just different settings. Also clean up some remnants of Deep and Fuzzy. For golang/go#54845 Change-Id: I5af2c4d3407c9fabca180ccded2531a74e6d38f0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538035 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/completion_test.go | 12 --- .../testdata/casesensitive/casesensitive.go | 16 ---- .../internal/lsp/testdata/summary.txt.golden | 1 - gopls/internal/lsp/tests/tests.go | 80 +++++++------------ .../testdata/completion/casesensitive.txt | 24 ++++++ 5 files changed, 51 insertions(+), 82 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/casesensitive/casesensitive.go create mode 100644 gopls/internal/regtest/marker/testdata/completion/casesensitive.txt diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index 2c8efa1ecb0..0e91c55411f 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -49,18 +49,6 @@ func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.C } } -func (r *runner) CaseSensitiveCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - got := r.callCompletion(t, src, func(opts *source.Options) { - opts.Matcher = source.CaseSensitive - opts.CompleteUnimported = false - }) - got = tests.FilterBuiltins(src, got) - want := expected(t, test, items) - if diff := tests.DiffCompletionItems(want, got); diff != "" { - t.Errorf("mismatching completion items (-want +got):\n%s", diff) - } -} - func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { got := r.callCompletion(t, src, func(opts *source.Options) { opts.DeepCompletion = true diff --git a/gopls/internal/lsp/testdata/casesensitive/casesensitive.go b/gopls/internal/lsp/testdata/casesensitive/casesensitive.go deleted file mode 100644 index 6f49d36ffec..00000000000 --- a/gopls/internal/lsp/testdata/casesensitive/casesensitive.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package casesensitive - -func _() { - var lower int //@item(lower, "lower", "int", "var") - var Upper int //@item(upper, "Upper", "int", "var") - - l //@casesensitive(" //", lower) - U //@casesensitive(" //", upper) - - L //@casesensitive(" //") - u //@casesensitive(" //") -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 19344e306f5..4476fd89782 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -3,7 +3,6 @@ CallHierarchyCount = 2 CompletionsCount = 177 CompletionSnippetCount = 27 RankedCompletionsCount = 151 -CaseSensitiveCompletionsCount = 4 SemanticTokenCount = 3 SuggestedFixCount = 80 MethodExtractionCount = 8 diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 1ad63c2c2e5..8049cbcbf41 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -58,7 +58,6 @@ type CallHierarchy = map[span.Span]*CallHierarchyResult type CompletionItems = map[token.Pos]*completion.CompletionItem type Completions = map[span.Span][]Completion type CompletionSnippets = map[span.Span][]CompletionSnippet -type CaseSensitiveCompletions = map[span.Span][]Completion type RankCompletions = map[span.Span][]Completion type SemanticTokens = []span.Span type SuggestedFixes = map[span.Span][]SuggestedFix @@ -71,23 +70,22 @@ type AddImport = map[span.URI]string type SelectionRanges = []span.Span type Data struct { - Config packages.Config - Exported *packagestest.Exported - CallHierarchy CallHierarchy - CompletionItems CompletionItems - Completions Completions - CompletionSnippets CompletionSnippets - CaseSensitiveCompletions CaseSensitiveCompletions - RankCompletions RankCompletions - SemanticTokens SemanticTokens - SuggestedFixes SuggestedFixes - MethodExtractions MethodExtractions - Renames Renames - InlayHints InlayHints - PrepareRenames PrepareRenames - Links Links - AddImport AddImport - SelectionRanges SelectionRanges + Config packages.Config + Exported *packagestest.Exported + CallHierarchy CallHierarchy + CompletionItems CompletionItems + Completions Completions + CompletionSnippets CompletionSnippets + RankCompletions RankCompletions + SemanticTokens SemanticTokens + SuggestedFixes SuggestedFixes + MethodExtractions MethodExtractions + Renames Renames + InlayHints InlayHints + PrepareRenames PrepareRenames + Links Links + AddImport AddImport + SelectionRanges SelectionRanges fragments map[string]string dir string @@ -110,7 +108,6 @@ type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) Completion(*testing.T, span.Span, Completion, CompletionItems) CompletionSnippet(*testing.T, span.Span, CompletionSnippet, bool, CompletionItems) - CaseSensitiveCompletion(*testing.T, span.Span, Completion, CompletionItems) RankCompletion(*testing.T, span.Span, Completion, CompletionItems) SemanticTokens(*testing.T, span.Span) SuggestedFix(*testing.T, span.Span, []SuggestedFix, int) @@ -129,15 +126,6 @@ const ( // Default runs the standard completion tests. CompletionDefault = CompletionTestType(iota) - // Deep tests deep completion. - CompletionDeep - - // Fuzzy tests deep completion and fuzzy matching. - CompletionFuzzy - - // CaseSensitive tests case sensitive completion. - CompletionCaseSensitive - // CompletionRank candidates in test must be valid and in the right relative order. CompletionRank ) @@ -225,18 +213,17 @@ func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*tes func load(t testing.TB, mode string, dir string) *Data { datum := &Data{ - CallHierarchy: make(CallHierarchy), - CompletionItems: make(CompletionItems), - Completions: make(Completions), - CompletionSnippets: make(CompletionSnippets), - RankCompletions: make(RankCompletions), - CaseSensitiveCompletions: make(CaseSensitiveCompletions), - Renames: make(Renames), - PrepareRenames: make(PrepareRenames), - SuggestedFixes: make(SuggestedFixes), - MethodExtractions: make(MethodExtractions), - Links: make(Links), - AddImport: make(AddImport), + CallHierarchy: make(CallHierarchy), + CompletionItems: make(CompletionItems), + Completions: make(Completions), + CompletionSnippets: make(CompletionSnippets), + RankCompletions: make(RankCompletions), + Renames: make(Renames), + PrepareRenames: make(PrepareRenames), + SuggestedFixes: make(SuggestedFixes), + MethodExtractions: make(MethodExtractions), + Links: make(Links), + AddImport: make(AddImport), dir: dir, fragments: map[string]string{}, @@ -371,9 +358,6 @@ func load(t testing.TB, mode string, dir string) *Data { if err := datum.Exported.Expect(map[string]interface{}{ "item": datum.collectCompletionItems, "complete": datum.collectCompletions(CompletionDefault), - "deep": datum.collectCompletions(CompletionDeep), - "fuzzy": datum.collectCompletions(CompletionFuzzy), - "casesensitive": datum.collectCompletions(CompletionCaseSensitive), "rank": datum.collectCompletions(CompletionRank), "snippet": datum.collectCompletionSnippets, "semantic": datum.collectSemanticTokens, @@ -492,11 +476,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("CaseSensitiveCompletion", func(t *testing.T) { - t.Helper() - eachCompletion(t, data.CaseSensitiveCompletions, tests.CaseSensitiveCompletion) - }) - t.Run("RankCompletions", func(t *testing.T) { t.Helper() eachCompletion(t, data.RankCompletions, tests.RankCompletion) @@ -648,7 +627,6 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "CompletionsCount = %v\n", countCompletions(data.Completions)) fmt.Fprintf(buf, "CompletionSnippetCount = %v\n", snippetCount) fmt.Fprintf(buf, "RankedCompletionsCount = %v\n", countCompletions(data.RankCompletions)) - fmt.Fprintf(buf, "CaseSensitiveCompletionsCount = %v\n", countCompletions(data.CaseSensitiveCompletions)) fmt.Fprintf(buf, "SemanticTokenCount = %v\n", len(data.SemanticTokens)) fmt.Fprintf(buf, "SuggestedFixCount = %v\n", len(data.SuggestedFixes)) fmt.Fprintf(buf, "MethodExtractionCount = %v\n", len(data.MethodExtractions)) @@ -748,10 +726,6 @@ func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []t return func(src span.Span, expected []token.Pos) { result(data.RankCompletions, src, expected) } - case CompletionCaseSensitive: - return func(src span.Span, expected []token.Pos) { - result(data.CaseSensitiveCompletions, src, expected) - } default: return func(src span.Span, expected []token.Pos) { result(data.Completions, src, expected) diff --git a/gopls/internal/regtest/marker/testdata/completion/casesensitive.txt b/gopls/internal/regtest/marker/testdata/completion/casesensitive.txt new file mode 100644 index 00000000000..418dcea29e8 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/casesensitive.txt @@ -0,0 +1,24 @@ +This test exercises the caseSensitive completion matcher. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false, + "matcher": "caseSensitive" +} + +-- casesensitive.go -- +package casesensitive + +func _() { + var lower int //@item(lower, "lower", "int", "var") + var Upper int //@item(upper, "Upper", "int", "var") + + l //@complete(" //", lower) + U //@complete(" //", upper) + + L //@complete(" //") + u //@complete(" //") +} From 099b552938448c23e5f18f2de70ebff799876d2d Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 27 Oct 2023 10:38:51 -0400 Subject: [PATCH 053/100] gopls/internal/regtest/marker: port the links marker Keep it simple: no need to filter out links within the marks themselves; just move them to a golden file. For golang/go#54845 Change-Id: Ic42b73ce11391ba5d9ee74cd5ae7d32eaf30ac20 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537883 LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/lsp_test.go | 18 ------- gopls/internal/lsp/regtest/marker.go | 46 ++++++++++++------ gopls/internal/lsp/testdata/links/links.go | 26 ---------- .../internal/lsp/testdata/summary.txt.golden | 1 - gopls/internal/lsp/tests/tests.go | 45 ------------------ gopls/internal/lsp/tests/util.go | 45 ------------------ .../regtest/marker/testdata/links/links.txt | 47 +++++++++++++++++++ 7 files changed, 79 insertions(+), 149 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/links/links.go create mode 100644 gopls/internal/regtest/marker/testdata/links/links.txt diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 87430e83796..55d5b7572f8 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -572,24 +572,6 @@ func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[sp return res, nil } -func (r *runner) Link(t *testing.T, uri span.URI, wantLinks []tests.Link) { - m, err := r.data.Mapper(uri) - if err != nil { - t.Fatal(err) - } - got, err := r.server.DocumentLink(r.ctx, &protocol.DocumentLinkParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(uri), - }, - }) - if err != nil { - t.Fatal(err) - } - if diff := tests.DiffLinks(m, wantLinks, got); diff != "" { - t.Error(diff) - } -} - func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) { cmd, err := command.NewListKnownPackagesCommand("List Known Packages", command.URIArg{ URI: protocol.URIFromSpanURI(uri), diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index c6bb611c7e7..9f71ba26fa9 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -181,6 +181,9 @@ var update = flag.Bool("update", false, "if set, update test data during marker // - def(src, dst location): perform a textDocument/definition request at // the src location, and check the result points to the dst location. // +// - documentLink(golden): asserts that textDocument/documentLink returns +// links as described by the golden file. +// // - foldingrange(golden): perform a textDocument/foldingRange for the // current document, and compare with the golden content, which is the // original source annotated with numbered tags delimiting the resulting @@ -707,6 +710,7 @@ var actionMarkerFuncs = map[string]func(marker){ "complete": actionMarkerFunc(completeMarker), "def": actionMarkerFunc(defMarker), "diag": actionMarkerFunc(diagMarker), + "documentlink": actionMarkerFunc(documentLinkMarker), "foldingrange": actionMarkerFunc(foldingRangeMarker), "format": actionMarkerFunc(formatMarker), "highlight": actionMarkerFunc(highlightMarker), @@ -1680,15 +1684,7 @@ func formatMarker(mark marker, golden *Golden) { } } - want, ok := golden.Get(mark.run.env.T, "", got) - if !ok { - mark.errorf("missing golden file @%s", golden.id) - return - } - - if diff := compare.Bytes(want, got); diff != "" { - mark.errorf("golden file @%s does not match format results:\n%s", golden.id, diff) - } + compareGolden(mark, "format", got, golden) } func highlightMarker(mark marker, src protocol.Location, dsts ...protocol.Location) { @@ -1938,6 +1934,21 @@ func codeLensesMarker(mark marker) { } } +func documentLinkMarker(mark marker, g *Golden) { + var b bytes.Buffer + links := mark.run.env.DocumentLink(mark.path()) + for _, l := range links { + if l.Target == nil { + mark.errorf("%s: nil link target", l.Range) + continue + } + loc := protocol.Location{URI: mark.uri(), Range: l.Range} + fmt.Fprintln(&b, mark.run.fmtLocDetails(loc, false), *l.Target) + } + + compareGolden(mark, "documentLink", b.Bytes(), g) +} + // consumeExtraNotes runs the provided func for each extra note with the given // name, and deletes all matching notes. func (mark marker) consumeExtraNotes(name string, f func(marker)) { @@ -2225,13 +2236,20 @@ func workspaceSymbolMarker(mark marker, query string, golden *Golden) { fmt.Fprintf(&got, "%s %s %s\n", loc, s.Name, s.Kind) } - want, ok := golden.Get(mark.run.env.T, "", got.Bytes()) + compareGolden(mark, fmt.Sprintf("Symbol(%q)", query), got.Bytes(), golden) +} + +// compareGolden compares the content of got with that of g.Get(""), reporting +// errors on any mismatch. +// +// TODO(rfindley): use this helper in more places. +func compareGolden(mark marker, op string, got []byte, g *Golden) { + want, ok := g.Get(mark.run.env.T, "", got) if !ok { - mark.errorf("missing golden file @%s", golden.id) + mark.errorf("missing golden file @%s", g.id) return } - - if diff := compare.Bytes(want, got.Bytes()); diff != "" { - mark.errorf("Symbol(%q) mismatch:\n%s", query, diff) + if diff := compare.Bytes(want, got); diff != "" { + mark.errorf("%s mismatch:\n%s", op, diff) } } diff --git a/gopls/internal/lsp/testdata/links/links.go b/gopls/internal/lsp/testdata/links/links.go deleted file mode 100644 index 378134341b4..00000000000 --- a/gopls/internal/lsp/testdata/links/links.go +++ /dev/null @@ -1,26 +0,0 @@ -package links - -import ( - "fmt" //@link(`fmt`,"https://pkg.go.dev/fmt") - - "golang.org/lsptests/foo" //@link(`golang.org/lsptests/foo`,`https://pkg.go.dev/golang.org/lsptests/foo`) - - _ "database/sql" //@link(`database/sql`, `https://pkg.go.dev/database/sql`) -) - -var ( - _ fmt.Formatter - _ foo.StructFoo - _ errors.Formatter -) - -// Foo function -func Foo() string { - /*https://example.com/comment */ //@link("https://example.com/comment","https://example.com/comment") - - url := "https://example.com/string_literal" //@link("https://example.com/string_literal","https://example.com/string_literal") - return url - - // TODO(golang/go#1234): Link the relevant issue. //@link("golang/go#1234", "https://github.com/golang/go/issues/1234") - // TODO(microsoft/vscode-go#12): Another issue. //@link("microsoft/vscode-go#12", "https://github.com/microsoft/vscode-go/issues/12") -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 4476fd89782..b7c54e7009a 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -9,6 +9,5 @@ MethodExtractionCount = 8 InlayHintsCount = 5 RenamesCount = 48 PrepareRenamesCount = 7 -LinksCount = 7 SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 8049cbcbf41..4350b24ae76 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -15,7 +15,6 @@ import ( "io" "os" "path/filepath" - "regexp" "sort" "strconv" "strings" @@ -23,11 +22,9 @@ import ( "testing" "time" - "golang.org/x/tools/go/expect" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/safetoken" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/gopls/internal/lsp/tests/compare" @@ -65,7 +62,6 @@ type MethodExtractions = map[span.Span]span.Span type Renames = map[span.Span]string type PrepareRenames = map[span.Span]*source.PrepareItem type InlayHints = []span.Span -type Links = map[span.URI][]Link type AddImport = map[span.URI]string type SelectionRanges = []span.Span @@ -83,7 +79,6 @@ type Data struct { Renames Renames InlayHints InlayHints PrepareRenames PrepareRenames - Links Links AddImport AddImport SelectionRanges SelectionRanges @@ -115,7 +110,6 @@ type Tests interface { InlayHints(*testing.T, span.Span) Rename(*testing.T, span.Span, string) PrepareRename(*testing.T, span.Span, *source.PrepareItem) - Link(*testing.T, span.URI, []Link) AddImport(*testing.T, span.URI, string) SelectionRanges(*testing.T, span.Span) } @@ -222,7 +216,6 @@ func load(t testing.TB, mode string, dir string) *Data { PrepareRenames: make(PrepareRenames), SuggestedFixes: make(SuggestedFixes), MethodExtractions: make(MethodExtractions), - Links: make(Links), AddImport: make(AddImport), dir: dir, @@ -364,7 +357,6 @@ func load(t testing.TB, mode string, dir string) *Data { "inlayHint": datum.collectInlayHints, "rename": datum.collectRenames, "prepare": datum.collectPrepareRenames, - "link": datum.collectLinks, "suggestedfix": datum.collectSuggestedFixes, "extractmethod": datum.collectMethodExtractions, "incomingcalls": datum.collectIncomingCalls, @@ -549,28 +541,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("Link", func(t *testing.T) { - t.Helper() - for uri, wantLinks := range data.Links { - // If we are testing GOPATH, then we do not want links with the versions - // attached (pkg.go.dev/repoa/moda@v1.1.0/pkg), unless the file is a - // go.mod, then we can skip it altogether. - if data.Exported.Exporter == packagestest.GOPATH { - if strings.HasSuffix(uri.Filename(), ".mod") { - continue - } - re := regexp.MustCompile(`@v\d+\.\d+\.[\w-]+`) - for i, link := range wantLinks { - wantLinks[i].Target = re.ReplaceAllString(link.Target, "") - } - } - t.Run(uriName(uri), func(t *testing.T) { - t.Helper() - tests.Link(t, uri, wantLinks) - }) - } - }) - t.Run("AddImport", func(t *testing.T) { t.Helper() for uri, exp := range data.AddImport { @@ -606,10 +576,6 @@ func Run(t *testing.T, tests Tests, data *Data) { func checkData(t *testing.T, data *Data) { buf := &bytes.Buffer{} - linksCount := 0 - for _, want := range data.Links { - linksCount += len(want) - } snippetCount := 0 for _, want := range data.CompletionSnippets { @@ -633,7 +599,6 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "InlayHintsCount = %v\n", len(data.InlayHints)) fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames)) fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames)) - fmt.Fprintf(buf, "LinksCount = %v\n", linksCount) fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges)) want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { @@ -835,16 +800,6 @@ func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain }) } -func (data *Data) collectLinks(spn span.Span, link string, note *expect.Note, fset *token.FileSet) { - position := safetoken.StartPosition(fset, note.Pos) - uri := spn.URI() - data.Links[uri] = append(data.Links[uri], Link{ - Src: spn, - Target: link, - NotePosition: position, - }) -} - func uriName(uri span.URI) string { return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go")) } diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index 79e31f409d4..c7739f42652 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -42,51 +42,6 @@ var builtins = map[string]bool{ "true": true, } -// DiffLinks takes the links we got and checks if they are located within the source or a Note. -// If the link is within a Note, the link is removed. -// Returns an diff comment if there are differences and empty string if no diffs. -func DiffLinks(mapper *protocol.Mapper, wantLinks []Link, gotLinks []protocol.DocumentLink) string { - var notePositions []token.Position - links := make(map[span.Span]string, len(wantLinks)) - for _, link := range wantLinks { - links[link.Src] = link.Target - notePositions = append(notePositions, link.NotePosition) - } - - var msg strings.Builder - for _, link := range gotLinks { - spn, err := mapper.RangeSpan(link.Range) - if err != nil { - return fmt.Sprintf("%v", err) - } - linkInNote := false - for _, notePosition := range notePositions { - // Drop the links found inside expectation notes arguments as this links are not collected by expect package. - if notePosition.Line == spn.Start().Line() && - notePosition.Column <= spn.Start().Column() { - delete(links, spn) - linkInNote = true - } - } - if linkInNote { - continue - } - - if target, ok := links[spn]; ok { - delete(links, spn) - if target != *link.Target { - fmt.Fprintf(&msg, "%s: want link with target %q, got %q\n", spn, target, *link.Target) - } - } else { - fmt.Fprintf(&msg, "%s: got unexpected link with target %q\n", spn, *link.Target) - } - } - for spn, target := range links { - fmt.Fprintf(&msg, "%s: expected link with target %q is missing\n", spn, target) - } - return msg.String() -} - // NormalizeAny replaces occurrences of interface{} in input with any. // // In Go 1.18, standard library functions were changed to use the 'any' diff --git a/gopls/internal/regtest/marker/testdata/links/links.txt b/gopls/internal/regtest/marker/testdata/links/links.txt new file mode 100644 index 00000000000..19ebcb4cb1a --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/links/links.txt @@ -0,0 +1,47 @@ +This test verifies behavior of textDocument/documentLink. + +-- go.mod -- +module golang.org/lsptests + +go 1.18 +-- foo/foo.go -- +package foo + +type StructFoo struct {} + +-- links/links.go -- +package links //@documentlink(links) + +import ( + "fmt" + + "golang.org/lsptests/foo" + + _ "database/sql" +) + +var ( + _ fmt.Formatter + _ foo.StructFoo + _ errors.Formatter //@diag("errors", re"(undeclared|undefined)") +) + +// Foo function +func Foo() string { + /*https://example.com/comment */ + + url := "https://example.com/string_literal" + return url + + // TODO(golang/go#1234): Link the relevant issue. + // TODO(microsoft/vscode-go#12): Another issue. +} + +-- @links -- +links/links.go:4:3-6 https://pkg.go.dev/fmt +links/links.go:6:3-26 https://pkg.go.dev/golang.org/lsptests/foo +links/links.go:8:5-17 https://pkg.go.dev/database/sql +links/links.go:21:10-44 https://example.com/string_literal +links/links.go:19:4-31 https://example.com/comment +links/links.go:24:10-24 https://github.com/golang/go/issues/1234 +links/links.go:25:10-32 https://github.com/microsoft/vscode-go/issues/12 From fcb8d5b1a810c653611b491448c7df4d1945c6cb Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 27 Oct 2023 13:47:13 -0400 Subject: [PATCH 054/100] go/ssa: some preparatory cleanups - use specific Object types (*Func,*Var,*Label) where appropriate. - document Parameter.object as non-nil - minor renamings Change-Id: I0e27c454eb26e1953849322d86ddccc21b6b6b99 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537884 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI --- go/ssa/builder.go | 8 ++++---- go/ssa/emit.go | 2 +- go/ssa/func.go | 47 ++++++++++++++++++------------------------- go/ssa/instantiate.go | 5 ++--- go/ssa/ssa.go | 47 +++++++++++++++++++++++++------------------ go/ssa/wrappers.go | 4 ++-- 6 files changed, 56 insertions(+), 57 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index ee2e33f389f..9af8321cee0 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -415,7 +415,7 @@ func (b *builder) addr(fn *Function, e ast.Expr, escaping bool) lvalue { if isBlankIdent(e) { return blank{} } - obj := fn.objectOf(e) + obj := fn.objectOf(e).(*types.Var) var v Value if g := fn.Prog.packageLevelMember(obj); g != nil { v = g.(*Global) // var (address) @@ -801,7 +801,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { return callee } // Local var. - return emitLoad(fn, fn.lookup(obj, false)) // var (address) + return emitLoad(fn, fn.lookup(obj.(*types.Var), false)) // var (address) case *ast.SelectorExpr: sel := fn.selection(e) @@ -2414,11 +2414,11 @@ func (b *builder) buildFunctionBody(fn *Function) { // We set Function.Params even though there is no body // code to reference them. This simplifies clients. if recv := fn.Signature.Recv(); recv != nil { - fn.addParamObj(recv) + fn.addParamVar(recv) } params := fn.Signature.Params() for i, n := 0, params.Len(); i < n; i++ { - fn.addParamObj(params.At(i)) + fn.addParamVar(params.At(i)) } } return diff --git a/go/ssa/emit.go b/go/ssa/emit.go index 448d1fb2852..132f9766e4f 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -56,7 +56,7 @@ func emitLocal(f *Function, t types.Type, pos token.Pos, comment string) *Alloc // It applies the appropriate generic instantiation to the type. func emitLocalVar(f *Function, v *types.Var) *Alloc { alloc := emitLocal(f, f.typ(v.Type()), v.Pos(), v.Name()) - f.objects[v] = alloc + f.vars[v] = alloc return alloc } diff --git a/go/ssa/func.go b/go/ssa/func.go index 0465f84d6b3..0e37b68bd04 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -108,38 +108,31 @@ type lblock struct { // labelledBlock returns the branch target associated with the // specified label, creating it if needed. func (f *Function) labelledBlock(label *ast.Ident) *lblock { - obj := f.objectOf(label) + obj := f.objectOf(label).(*types.Label) lb := f.lblocks[obj] if lb == nil { lb = &lblock{_goto: f.newBasicBlock(label.Name)} if f.lblocks == nil { - f.lblocks = make(map[types.Object]*lblock) + f.lblocks = make(map[*types.Label]*lblock) } f.lblocks[obj] = lb } return lb } -// addParam adds a (non-escaping) parameter to f.Params of the -// specified name, type and source position. -func (f *Function) addParam(name string, typ types.Type, pos token.Pos) *Parameter { - v := &Parameter{ - name: name, - typ: typ, - pos: pos, - parent: f, - } - f.Params = append(f.Params, v) - return v -} - -func (f *Function) addParamObj(obj *types.Var) *Parameter { - name := obj.Name() +// addParamVar adds a parameter to f.Params. +func (f *Function) addParamVar(v *types.Var) *Parameter { + name := v.Name() if name == "" { name = fmt.Sprintf("arg%d", len(f.Params)) } - param := f.addParam(name, f.typ(obj.Type()), obj.Pos()) - param.object = obj + param := &Parameter{ + name: name, + object: v, + typ: f.typ(v.Type()), + parent: f, + } + f.Params = append(f.Params, param) return param } @@ -147,7 +140,7 @@ func (f *Function) addParamObj(obj *types.Var) *Parameter { // stack; the function body will load/store the spilled location. // Subsequent lifting will eliminate spills where possible. func (f *Function) addSpilledParam(obj *types.Var) { - param := f.addParamObj(obj) + param := f.addParamVar(obj) spill := emitLocalVar(f, obj) f.emit(&Store{Addr: spill, Val: param}) } @@ -156,7 +149,7 @@ func (f *Function) addSpilledParam(obj *types.Var) { // Precondition: f.Type() already set. func (f *Function) startBody() { f.currentBlock = f.newBasicBlock("entry") - f.objects = make(map[types.Object]Value) // needed for some synthetics, e.g. init + f.vars = make(map[*types.Var]Value) // needed for some synthetics, e.g. init } // createSyntacticParams populates f.Params and generates code (spills @@ -176,7 +169,7 @@ func (f *Function) createSyntacticParams(recv *ast.FieldList, functype *ast.Func } // Anonymous receiver? No need to spill. if field.Names == nil { - f.addParamObj(f.Signature.Recv()) + f.addParamVar(f.Signature.Recv()) } } } @@ -190,7 +183,7 @@ func (f *Function) createSyntacticParams(recv *ast.FieldList, functype *ast.Func } // Anonymous parameter? No need to spill. if field.Names == nil { - f.addParamObj(f.Signature.Params().At(len(f.Params) - n)) + f.addParamVar(f.Signature.Params().At(len(f.Params) - n)) } } } @@ -280,7 +273,7 @@ func mayNeedRuntimeTypes(fn *Function) []types.Type { // // The function is not done being built until done() is called. func (f *Function) finishBody() { - f.objects = nil + f.vars = nil f.currentBlock = nil f.lblocks = nil @@ -388,8 +381,8 @@ func (f *Function) debugInfo() bool { // that is local to function f or one of its enclosing functions. // If escaping, the reference comes from a potentially escaping pointer // expression and the referent must be heap-allocated. -func (f *Function) lookup(obj types.Object, escaping bool) Value { - if v, ok := f.objects[obj]; ok { +func (f *Function) lookup(obj *types.Var, escaping bool) Value { + if v, ok := f.vars[obj]; ok { if alloc, ok := v.(*Alloc); ok && escaping { alloc.Heap = true } @@ -409,7 +402,7 @@ func (f *Function) lookup(obj types.Object, escaping bool) Value { outer: outer, parent: f, } - f.objects[obj] = v + f.vars[obj] = v f.FreeVars = append(f.FreeVars, v) return v } diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index 94c7b6411bc..3760e9884ed 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -126,8 +126,7 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa var obj *types.Func if recv := fn.Signature.Recv(); recv != nil { // method - m := fn.object.(*types.Func) - obj = prog.canon.instantiateMethod(m, targs, prog.ctxt) + obj = prog.canon.instantiateMethod(fn.object, targs, prog.ctxt) sig = obj.Type().(*types.Signature) } else { instSig, err := typeparams.Instantiate(prog.ctxt, fn.Signature, targs, false) @@ -138,7 +137,7 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa if !ok { panic("Instantiate of a Signature returned a non-signature") } - obj = fn.object.(*types.Func) // instantiation does not exist yet + obj = fn.object // instantiation does not exist yet sig = prog.canon.Type(instance).(*types.Signature) } diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index b7ecd813720..0d9da533863 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -306,16 +306,19 @@ type Node interface { // respectively, and is nil in the generic method. type Function struct { name string - object types.Object // a declared *types.Func or one of its wrappers - method *selection // info about provenance of synthetic methods; thunk => non-nil + object *types.Func // symbol for declared function (nil for FuncLit or synthetic init) + method *selection // info about provenance of synthetic methods; thunk => non-nil Signature *types.Signature pos token.Pos - Synthetic string // provenance of synthetic function; "" for true source functions - syntax ast.Node // *ast.Func{Decl,Lit}; replaced with simple ast.Node after build, unless debug mode - parent *Function // enclosing function if anon; nil if global - Pkg *Package // enclosing package; nil for shared funcs (wrappers and error.Error) - Prog *Program // enclosing program + Synthetic string // provenance of synthetic function; "" for true source functions + syntax ast.Node // *ast.Func{Decl,Lit}; replaced with simple ast.Node after build, unless debug mode + parent *Function // enclosing function if anon; nil if global + Pkg *Package // enclosing package; nil for shared funcs (wrappers and error.Error) + Prog *Program // enclosing program + + // These fields are populated only when the function body is built: + Params []*Parameter // function parameters; for methods, includes receiver FreeVars []*FreeVar // free variables whose values must be supplied by closure Locals []*Alloc // frame-allocated variables of this function @@ -333,10 +336,10 @@ type Function struct { // The following fields are set transiently during building, // then cleared. currentBlock *BasicBlock // where to emit code - objects map[types.Object]Value // addresses of local variables + vars map[*types.Var]Value // addresses of local variables namedResults []*Alloc // tuple of named results targets *targets // linked stack of branch targets - lblocks map[types.Object]*lblock // labelled blocks + lblocks map[*types.Label]*lblock // labelled blocks info *types.Info // *types.Info to build from. nil for wrappers. subst *subster // non-nil => expand generic body using this type substitution of ground types goversion string // Go version of syntax (NB: init is special) @@ -404,9 +407,8 @@ type FreeVar struct { // A Parameter represents an input parameter of a function. type Parameter struct { name string - object types.Object // a *types.Var; nil for non-source locals + object *types.Var // non-nil typ types.Type - pos token.Pos parent *Function referrers []Instruction } @@ -1506,14 +1508,19 @@ func (v *Global) String() string { return v.RelString(nil) func (v *Global) Package() *Package { return v.Pkg } func (v *Global) RelString(from *types.Package) string { return relString(v, from) } -func (v *Function) Name() string { return v.name } -func (v *Function) Type() types.Type { return v.Signature } -func (v *Function) Pos() token.Pos { return v.pos } -func (v *Function) Token() token.Token { return token.FUNC } -func (v *Function) Object() types.Object { return v.object } -func (v *Function) String() string { return v.RelString(nil) } -func (v *Function) Package() *Package { return v.Pkg } -func (v *Function) Parent() *Function { return v.parent } +func (v *Function) Name() string { return v.name } +func (v *Function) Type() types.Type { return v.Signature } +func (v *Function) Pos() token.Pos { return v.pos } +func (v *Function) Token() token.Token { return token.FUNC } +func (v *Function) Object() types.Object { + if v.object != nil { + return types.Object(v.object) + } + return nil +} +func (v *Function) String() string { return v.RelString(nil) } +func (v *Function) Package() *Package { return v.Pkg } +func (v *Function) Parent() *Function { return v.parent } func (v *Function) Referrers() *[]Instruction { if v.parent != nil { return &v.referrers @@ -1561,7 +1568,7 @@ func (v *Parameter) Type() types.Type { return v.typ } func (v *Parameter) Name() string { return v.name } func (v *Parameter) Object() types.Object { return v.object } func (v *Parameter) Referrers() *[]Instruction { return &v.referrers } -func (v *Parameter) Pos() token.Pos { return v.pos } +func (v *Parameter) Pos() token.Pos { return v.object.Pos() } func (v *Parameter) Parent() *Function { return v.parent } func (v *Alloc) Type() types.Type { return v.typ } diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index f71c27ddc2f..b63ef846cd2 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -153,7 +153,7 @@ func makeWrapper(prog *Program, sel *selection, cr *creator) *Function { func createParams(fn *Function, start int) { tparams := fn.Signature.Params() for i, n := start, tparams.Len(); i < n; i++ { - fn.addParamObj(tparams.At(i)) + fn.addParamVar(tparams.At(i)) } } @@ -342,7 +342,7 @@ func buildInstantiationWrapper(fn *Function) { fn.startBody() if sig.Recv() != nil { - fn.addParamObj(sig.Recv()) + fn.addParamVar(sig.Recv()) } createParams(fn, 0) From ccc652ac3e5ada9c22f680ba47e55f25b6298484 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 27 Oct 2023 14:52:50 -0400 Subject: [PATCH 055/100] go/ssa: eliminate Program.{bounds,thunks} memoization This was a premature optimization (saves 0.1% of functions in std, and the ones it saves are tiny). Change-Id: I22e011b7a8b0dc3a60a8ca982152fec609820515 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538275 LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King --- go/ssa/builder_test.go | 7 ++- go/ssa/create.go | 2 - go/ssa/ssa.go | 2 - go/ssa/wrappers.go | 126 ++++++++++++++--------------------------- 4 files changed, 50 insertions(+), 87 deletions(-) diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index 25b72fc1ec6..4c3a35f01a7 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -422,6 +422,7 @@ var ( "P.init": "package initializer", } + var seen []string // may contain dups for fn := range ssautil.AllFunctions(prog) { if fn.Synthetic == "" { continue @@ -432,12 +433,16 @@ var ( t.Errorf("got unexpected/duplicate func: %q: %q", name, fn.Synthetic) continue } - delete(want, name) + seen = append(seen, name) if wantDescr != fn.Synthetic { t.Errorf("(%s).Synthetic = %q, want %q", name, fn.Synthetic, wantDescr) } } + + for _, name := range seen { + delete(want, name) + } for fn, descr := range want { t.Errorf("want func: %q: %q", fn, descr) } diff --git a/go/ssa/create.go b/go/ssa/create.go index 90cb9bb2e94..fb59fe341dc 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -27,8 +27,6 @@ func NewProgram(fset *token.FileSet, mode BuilderMode) *Program { Fset: fset, imported: make(map[string]*Package), packages: make(map[*types.Package]*Package), - thunks: make(map[selectionKey]*Function), - bounds: make(map[boundsKey]*Function), mode: mode, canon: newCanonizer(), ctxt: typeparams.NewContext(), diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 0d9da533863..325493ced2b 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -33,8 +33,6 @@ type Program struct { methodsMu sync.Mutex // guards the following maps: methodSets typeutil.Map // maps type to its concrete methodSet runtimeTypes typeutil.Map // types for which rtypes are needed - bounds map[boundsKey]*Function // bounds for curried x.Method closures - thunks map[selectionKey]*Function // thunks for T.Method expressions instances map[*Function]*instanceSet // instances of generic functions parameterized tpWalker // determines whether a type reaches a type parameter. } diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index b63ef846cd2..77979ec02c1 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -184,56 +184,50 @@ func createParams(fn *Function, start int) { // // EXCLUSIVE_LOCKS_ACQUIRED(meth.Prog.methodsMu) func makeBound(prog *Program, obj *types.Func, cr *creator) *Function { - targs := receiverTypeArgs(obj) - key := boundsKey{obj, prog.canon.List(targs)} - prog.methodsMu.Lock() defer prog.methodsMu.Unlock() - fn, ok := prog.bounds[key] - if !ok { - description := fmt.Sprintf("bound method wrapper for %s", obj) - if prog.mode&LogSource != 0 { - defer logStack("%s", description)() - } - fn = &Function{ - name: obj.Name() + "$bound", - object: obj, - Signature: changeRecv(obj.Type().(*types.Signature), nil), // drop receiver - Synthetic: description, - Prog: prog, - pos: obj.Pos(), - // wrappers have no syntax - info: nil, - goversion: "", - } - cr.Add(fn) - - fv := &FreeVar{name: "recv", typ: recvType(obj), parent: fn} - fn.FreeVars = []*FreeVar{fv} - fn.startBody() - createParams(fn, 0) - var c Call - - if !types.IsInterface(recvType(obj)) { // concrete - callee := prog.originFunc(obj) - if callee.typeparams.Len() > 0 { - callee = prog.lookupOrCreateInstance(callee, targs, cr) - } - c.Call.Value = callee - c.Call.Args = []Value{fv} - } else { - c.Call.Method = obj - c.Call.Value = fv // interface (possibly a typeparam) - } - for _, arg := range fn.Params { - c.Call.Args = append(c.Call.Args, arg) - } - emitTailCall(fn, &c) - fn.finishBody() - fn.done() - prog.bounds[key] = fn + description := fmt.Sprintf("bound method wrapper for %s", obj) + if prog.mode&LogSource != 0 { + defer logStack("%s", description)() } + fn := &Function{ + name: obj.Name() + "$bound", + object: obj, + Signature: changeRecv(obj.Type().(*types.Signature), nil), // drop receiver + Synthetic: description, + Prog: prog, + pos: obj.Pos(), + // wrappers have no syntax + info: nil, + goversion: "", + } + cr.Add(fn) + + fv := &FreeVar{name: "recv", typ: recvType(obj), parent: fn} + fn.FreeVars = []*FreeVar{fv} + fn.startBody() + createParams(fn, 0) + var c Call + + if !types.IsInterface(recvType(obj)) { // concrete + callee := prog.originFunc(obj) + if callee.typeparams.Len() > 0 { + callee = prog.lookupOrCreateInstance(callee, receiverTypeArgs(obj), cr) + } + c.Call.Value = callee + c.Call.Args = []Value{fv} + } else { + c.Call.Method = obj + c.Call.Value = fv // interface (possibly a typeparam) + } + for _, arg := range fn.Params { + c.Call.Args = append(c.Call.Args, arg) + } + emitTailCall(fn, &c) + fn.finishBody() + fn.done() + return fn } @@ -256,37 +250,20 @@ func makeBound(prog *Program, obj *types.Func, cr *creator) *Function { // // f := func(t T) { return t.meth() } // -// TODO(adonovan): opt: currently the stub is created even when used -// directly in a function call: C.f(i, 0). This is less efficient -// than inlining the stub. -// // EXCLUSIVE_LOCKS_ACQUIRED(meth.Prog.methodsMu) func makeThunk(prog *Program, sel *selection, cr *creator) *Function { if sel.kind != types.MethodExpr { panic(sel) } - // Canonicalize sel.recv to avoid constructing duplicate thunks. - canonRecv := prog.canon.Type(sel.recv) - key := selectionKey{ - kind: sel.kind, - recv: canonRecv, - obj: sel.obj, - index: fmt.Sprint(sel.index), - indirect: sel.indirect, - } - prog.methodsMu.Lock() defer prog.methodsMu.Unlock() - fn, ok := prog.thunks[key] - if !ok { - fn = makeWrapper(prog, sel, cr) - if fn.Signature.Recv() != nil { - panic(fn) // unexpected receiver - } - prog.thunks[key] = fn + fn := makeWrapper(prog, sel, cr) + if fn.Signature.Recv() != nil { + panic(fn) // unexpected receiver } + return fn } @@ -294,21 +271,6 @@ func changeRecv(s *types.Signature, recv *types.Var) *types.Signature { return types.NewSignature(recv, s.Params(), s.Results(), s.Variadic()) } -// selectionKey is like types.Selection but a usable map key. -type selectionKey struct { - kind types.SelectionKind - recv types.Type // canonicalized via Program.canon - obj types.Object - index string - indirect bool -} - -// boundsKey is a unique for the object and a type instantiation. -type boundsKey struct { - obj types.Object // t.meth - inst *typeList // canonical type instantiation list. -} - // A local version of *types.Selection. // Needed for some additional control, such as creating a MethodExpr for an instantiation. type selection struct { From 9482e85840f2904143bb614fe37731c9c0b2d3a1 Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Thu, 26 Oct 2023 19:10:33 -0400 Subject: [PATCH 056/100] gopls/internal/lsp/cmd: add GOPACKAGESDRIVER to stat GOPACKAGESDRIVER can affect gopls's functionality. Include it in the stat report to distinguish from the default case. This field can carry private info but we want to know at least whether this is set. Explicitly mark redacted non-zero values with 'redacted' note in -anon output. Change-Id: I0ca31caeef0633a52b5ac4c5ce538e6ee82668d2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/537882 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/cmd/stats.go | 17 +++++++------ .../internal/lsp/cmd/test/integration_test.go | 24 +++++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/gopls/internal/lsp/cmd/stats.go b/gopls/internal/lsp/cmd/stats.go index 4e339f1c543..1fd3367945b 100644 --- a/gopls/internal/lsp/cmd/stats.go +++ b/gopls/internal/lsp/cmd/stats.go @@ -70,11 +70,12 @@ func (s *stats) Run(ctx context.Context, args ...string) error { } stats := GoplsStats{ - GOOS: runtime.GOOS, - GOARCH: runtime.GOARCH, - GOPLSCACHE: os.Getenv("GOPLSCACHE"), - GoVersion: runtime.Version(), - GoplsVersion: debug.Version(), + GOOS: runtime.GOOS, + GOARCH: runtime.GOARCH, + GOPLSCACHE: os.Getenv("GOPLSCACHE"), + GoVersion: runtime.Version(), + GoplsVersion: debug.Version(), + GOPACKAGESDRIVER: os.Getenv("GOPACKAGESDRIVER"), } opts := s.app.options @@ -198,11 +199,12 @@ func (s *stats) Run(ctx context.Context, args ...string) error { if !token.IsExported(f.Name) { continue } - if s.Anon && f.Tag.Get("anon") != "ok" { + vf := v.FieldByName(f.Name) + if s.Anon && f.Tag.Get("anon") != "ok" && !vf.IsZero() { // Fields that can be served with -anon must be explicitly marked as OK. + // But, if it's zero value, it's ok to print. continue } - vf := v.FieldByName(f.Name) okFields[f.Name] = vf.Interface() } } @@ -227,6 +229,7 @@ type GoplsStats struct { GOPLSCACHE string GoVersion string `anon:"ok"` GoplsVersion string `anon:"ok"` + GOPACKAGESDRIVER string InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form CacheDir string BugReports []goplsbug.Bug diff --git a/gopls/internal/lsp/cmd/test/integration_test.go b/gopls/internal/lsp/cmd/test/integration_test.go index 4ee9e3eb7c5..c14f1d9cf70 100644 --- a/gopls/internal/lsp/cmd/test/integration_test.go +++ b/gopls/internal/lsp/cmd/test/integration_test.go @@ -760,6 +760,7 @@ package foo { res2 := gopls(t, tree, "stats", "-anon") res2.checkExit(true) + var stats2 cmd.GoplsStats if err := json.Unmarshal([]byte(res2.stdout), &stats2); err != nil { t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) @@ -767,6 +768,29 @@ package foo if got := len(stats2.BugReports); got > 0 { t.Errorf("Got %d bug reports with -anon, want 0. Reports:%+v", got, stats2.BugReports) } + var stats2AsMap map[string]interface{} + if err := json.Unmarshal([]byte(res2.stdout), &stats2AsMap); err != nil { + t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) + } + // GOPACKAGESDRIVER is user information, but is ok to print zero value. + if v, ok := stats2AsMap["GOPACKAGESDRIVER"]; !ok || v != "" { + t.Errorf(`Got GOPACKAGESDRIVER=(%q, %v); want ("", true(found))`, v, ok) + } + } + + // Check that -anon suppresses fields containing non-zero user information. + { + res3 := goplsWithEnv(t, tree, []string{"GOPACKAGESDRIVER=off"}, "stats", "-anon") + res3.checkExit(true) + + var statsAsMap3 map[string]interface{} + if err := json.Unmarshal([]byte(res3.stdout), &statsAsMap3); err != nil { + t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) + } + // GOPACKAGESDRIVER is user information, want non-empty value to be omitted. + if v, ok := statsAsMap3["GOPACKAGESDRIVER"]; ok { + t.Errorf(`Got GOPACKAGESDRIVER=(%q, %v); want ("", false(not found))`, v, ok) + } } } From ec032e3dbaf15ae66da4a96ae1ffe594b39c4f2d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 23 Oct 2023 18:04:15 -0400 Subject: [PATCH 057/100] go/packages: two clean-ups 1. Move visit recursion into "if NeedImports" block. 2. Merge two loops to avoid the need to gather srcpkgs. No behavior change is intended Change-Id: I085e8ba0d2acd1f6fae074c0db188e0b953c2fcd Reviewed-on: https://go-review.googlesource.com/c/tools/+/537117 LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Matloob --- go/packages/packages.go | 145 +++++++++++++++++------------------ go/packages/packages_test.go | 13 ++-- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/go/packages/packages.go b/go/packages/packages.go index 2f7c998d584..6cbd3de83ec 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -699,92 +699,91 @@ func (ld *loader) refine(response *driverResponse) ([]*Package, error) { } } - // Materialize the import graph (if NeedImports). - - const ( - white = 0 // new - grey = 1 // in progress - black = 2 // complete - ) - - // visit traverses the import graph, depth-first, - // and materializes the graph as Packages.Imports. - // - // Valid imports are saved in the Packages.Import map. - // Invalid imports (cycles and missing nodes) are saved in the importErrors map. - // Thus, even in the presence of both kinds of errors, the Import graph remains a DAG. - // - // visit returns whether the package needs src or has a transitive - // dependency on a package that does. These are the only packages - // for which we load source code. - var stack, srcPkgs []*loaderPackage - var visit func(lpkg *loaderPackage) bool - visit = func(lpkg *loaderPackage) bool { - switch lpkg.color { - case black: - return lpkg.needsrc - case grey: - panic("internal error: grey node") - } - lpkg.color = grey - stack = append(stack, lpkg) // push - stubs := lpkg.Imports // the structure form has only stubs with the ID in the Imports - lpkg.Imports = make(map[string]*Package, len(stubs)) - for importPath, ipkg := range stubs { - var importErr error - imp := ld.pkgs[ipkg.ID] - if imp == nil { - // (includes package "C" when DisableCgo) - importErr = fmt.Errorf("missing package: %q", ipkg.ID) - } else if imp.color == grey { - importErr = fmt.Errorf("import cycle: %s", stack) + if ld.Mode&NeedImports != 0 { + // Materialize the import graph. + + const ( + white = 0 // new + grey = 1 // in progress + black = 2 // complete + ) + + // visit traverses the import graph, depth-first, + // and materializes the graph as Packages.Imports. + // + // Valid imports are saved in the Packages.Import map. + // Invalid imports (cycles and missing nodes) are saved in the importErrors map. + // Thus, even in the presence of both kinds of errors, + // the Import graph remains a DAG. + // + // visit returns whether the package needs src or has a transitive + // dependency on a package that does. These are the only packages + // for which we load source code. + var stack []*loaderPackage + var visit func(lpkg *loaderPackage) bool + visit = func(lpkg *loaderPackage) bool { + switch lpkg.color { + case black: + return lpkg.needsrc + case grey: + panic("internal error: grey node") } - if importErr != nil { - if lpkg.importErrors == nil { - lpkg.importErrors = make(map[string]error) + lpkg.color = grey + stack = append(stack, lpkg) // push + stubs := lpkg.Imports // the structure form has only stubs with the ID in the Imports + lpkg.Imports = make(map[string]*Package, len(stubs)) + for importPath, ipkg := range stubs { + var importErr error + imp := ld.pkgs[ipkg.ID] + if imp == nil { + // (includes package "C" when DisableCgo) + importErr = fmt.Errorf("missing package: %q", ipkg.ID) + } else if imp.color == grey { + importErr = fmt.Errorf("import cycle: %s", stack) } - lpkg.importErrors[importPath] = importErr - continue + if importErr != nil { + if lpkg.importErrors == nil { + lpkg.importErrors = make(map[string]error) + } + lpkg.importErrors[importPath] = importErr + continue + } + + if visit(imp) { + lpkg.needsrc = true + } + lpkg.Imports[importPath] = imp.Package } - if visit(imp) { - lpkg.needsrc = true + // Complete type information is required for the + // immediate dependencies of each source package. + if lpkg.needsrc && ld.Mode&NeedTypes != 0 { + for _, ipkg := range lpkg.Imports { + ld.pkgs[ipkg.ID].needtypes = true + } } - lpkg.Imports[importPath] = imp.Package - } - if lpkg.needsrc { - srcPkgs = append(srcPkgs, lpkg) - } - // NeedTypeSizes causes TypeSizes to be set even - // on packages for which types aren't needed. - if ld.Mode&NeedTypesSizes != 0 { - lpkg.TypesSizes = ld.sizes - } - stack = stack[:len(stack)-1] // pop - lpkg.color = black - return lpkg.needsrc - } + // NeedTypeSizes causes TypeSizes to be set even + // on packages for which types aren't needed. + if ld.Mode&NeedTypesSizes != 0 { + lpkg.TypesSizes = ld.sizes + } + stack = stack[:len(stack)-1] // pop + lpkg.color = black - if ld.Mode&NeedImports == 0 { - // We do this to drop the stub import packages that we are not even going to try to resolve. - for _, lpkg := range initial { - lpkg.Imports = nil + return lpkg.needsrc } - } else { + // For each initial package, create its import DAG. for _, lpkg := range initial { visit(lpkg) } - if ld.Mode&NeedTypes != 0 { - // Complete type information is required for the - // immediate dependencies of each source package. - for _, lpkg := range srcPkgs { - for _, ipkg := range lpkg.Imports { - ld.pkgs[ipkg.ID].needtypes = true - } - } + } else { + // !NeedImports: drop the stub (ID-only) import packages + // that we are not even going to try to resolve. + for _, lpkg := range initial { + lpkg.Imports = nil } } diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 4fb7f0bdcef..6e461c8acad 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -1217,12 +1217,13 @@ func testSizes(t *testing.T, exporter packagestest.Exporter) { } } -// This is a regression test for the root cause of -// github.com/golang/vscode-go/issues/3021. -// If types are needed (any of NeedTypes{,Info,Sizes} -// and the types.Sizes cannot be obtained (e.g. due to a bad GOARCH) -// then the Load operation must fail. It must not return a nil -// TypesSizes, or use the default (wrong) size. +// This is a regression test for a bug related to +// github.com/golang/vscode-go/issues/3021: if types are needed (any +// of NeedTypes{,Info,Sizes} and the types.Sizes cannot be obtained +// (e.g. due to a bad GOARCH) then the Load operation must fail. It +// must not return a nil TypesSizes, or use the default (wrong) size. +// (The root cause of that issue turned out to be due to skew in the +// Bazel GOPACKAGESDRIVER; see CL 537876.) // // We use a file=... query because it suppresses the bad-GOARCH check // that the go command would otherwise perform eagerly. From 9cf559ce9f9d243580e8c4e54d4c1b3a907dd781 Mon Sep 17 00:00:00 2001 From: Anton Telyshev Date: Sun, 29 Oct 2023 11:58:31 +0200 Subject: [PATCH 058/100] go/analysis/passes/errorsas: remove old comment Change-Id: I7e64014265639c9164e0161dcccfd8181207ad79 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538415 Reviewed-by: Tim King Reviewed-by: qiulaidongfeng <2645477756@qq.com> Reviewed-by: Cherry Mui TryBot-Result: Gopher Robot --- go/analysis/passes/errorsas/errorsas.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/go/analysis/passes/errorsas/errorsas.go b/go/analysis/passes/errorsas/errorsas.go index 43996b80a5a..7f62ad4c825 100644 --- a/go/analysis/passes/errorsas/errorsas.go +++ b/go/analysis/passes/errorsas/errorsas.go @@ -66,9 +66,6 @@ func run(pass *analysis.Pass) (interface{}, error) { var errorType = types.Universe.Lookup("error").Type() -// pointerToInterfaceOrError reports whether the type of e is a pointer to an interface or a type implementing error, -// or is the empty interface. - // checkAsTarget reports an error if the second argument to errors.As is invalid. func checkAsTarget(pass *analysis.Pass, e ast.Expr) error { t := pass.TypesInfo.Types[e].Type From 1762c80ff22626c02decf68a2807db29be163f57 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 10 Jul 2023 11:37:29 -0400 Subject: [PATCH 059/100] internal/gopathwalk: use filepath.WalkDir instead of internal/fastwalk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit golang.org/x/tools/internal/fastwalk adds a lot of complexity to avoid unnecessary calls to os.Stat. It also adds very limited concurrency (a maximum of 4 goroutines). filepath.WalkDir, added in Go 1.16, also avoids unnecessary calls to os.Stat, and is part of the standard library (so it adds much less complexity to x/tools). Switching to that substantially simplifies the implementation of internal/gopathwalk, at the cost of losing the small amount of concurrency added by fastwalk. The internal/gopathwalk package does not appear to have any benchmarks of its own, but the benchmark impact shows a ~14.5% increase in the imports.ScanModCache benchmark on Linux with a warm filesystem cache, but that can be reduced to ~5.5% by also removing an apparently unnecessary call to os.SameFile (in followup CL 508507). To me, the decrease in code complexity seems worth the slight increase in latency. ~/x/tools$ benchstat cl508505.txt cl508506.txt goos: linux goarch: amd64 pkg: golang.org/x/tools/internal/imports cpu: AMD EPYC 7B12 │ cl508505.txt │ cl508506.txt │ │ sec/op │ sec/op vs base │ ScanModCache-24 231.0m ± 1% 264.4m ± 1% +14.47% (p=0.000 n=10) Fixes golang/go#58035. Change-Id: Iffcdab4e9aea11c0f0d7a87904935635e20b2fe7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/508506 Auto-Submit: Bryan Mills Reviewed-by: Heschi Kreinick LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- internal/fastwalk/fastwalk.go | 196 -------------- internal/fastwalk/fastwalk_darwin.go | 119 --------- internal/fastwalk/fastwalk_dirent_fileno.go | 14 - internal/fastwalk/fastwalk_dirent_ino.go | 15 -- .../fastwalk/fastwalk_dirent_namlen_bsd.go | 14 - .../fastwalk/fastwalk_dirent_namlen_linux.go | 29 -- internal/fastwalk/fastwalk_portable.go | 41 --- internal/fastwalk/fastwalk_test.go | 251 ------------------ internal/fastwalk/fastwalk_unix.go | 153 ----------- internal/gopathwalk/walk.go | 103 +++++-- 10 files changed, 87 insertions(+), 848 deletions(-) delete mode 100644 internal/fastwalk/fastwalk.go delete mode 100644 internal/fastwalk/fastwalk_darwin.go delete mode 100644 internal/fastwalk/fastwalk_dirent_fileno.go delete mode 100644 internal/fastwalk/fastwalk_dirent_ino.go delete mode 100644 internal/fastwalk/fastwalk_dirent_namlen_bsd.go delete mode 100644 internal/fastwalk/fastwalk_dirent_namlen_linux.go delete mode 100644 internal/fastwalk/fastwalk_portable.go delete mode 100644 internal/fastwalk/fastwalk_test.go delete mode 100644 internal/fastwalk/fastwalk_unix.go diff --git a/internal/fastwalk/fastwalk.go b/internal/fastwalk/fastwalk.go deleted file mode 100644 index c40c7e93106..00000000000 --- a/internal/fastwalk/fastwalk.go +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2016 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 fastwalk provides a faster version of [filepath.Walk] for file system -// scanning tools. -package fastwalk - -import ( - "errors" - "os" - "path/filepath" - "runtime" - "sync" -) - -// ErrTraverseLink is used as a return value from WalkFuncs to indicate that the -// symlink named in the call may be traversed. -var ErrTraverseLink = errors.New("fastwalk: traverse symlink, assuming target is a directory") - -// ErrSkipFiles is a used as a return value from WalkFuncs to indicate that the -// callback should not be called for any other files in the current directory. -// Child directories will still be traversed. -var ErrSkipFiles = errors.New("fastwalk: skip remaining files in directory") - -// Walk is a faster implementation of [filepath.Walk]. -// -// [filepath.Walk]'s design necessarily calls [os.Lstat] on each file, -// even if the caller needs less info. -// Many tools need only the type of each file. -// On some platforms, this information is provided directly by the readdir -// system call, avoiding the need to stat each file individually. -// fastwalk_unix.go contains a fork of the syscall routines. -// -// See golang.org/issue/16399. -// -// Walk walks the file tree rooted at root, calling walkFn for -// each file or directory in the tree, including root. -// -// If Walk returns [filepath.SkipDir], the directory is skipped. -// -// Unlike [filepath.Walk]: -// - file stat calls must be done by the user. -// The only provided metadata is the file type, which does not include -// any permission bits. -// - multiple goroutines stat the filesystem concurrently. The provided -// walkFn must be safe for concurrent use. -// - Walk can follow symlinks if walkFn returns the TraverseLink -// sentinel error. It is the walkFn's responsibility to prevent -// Walk from going into symlink cycles. -func Walk(root string, walkFn func(path string, typ os.FileMode) error) error { - // TODO(bradfitz): make numWorkers configurable? We used a - // minimum of 4 to give the kernel more info about multiple - // things we want, in hopes its I/O scheduling can take - // advantage of that. Hopefully most are in cache. Maybe 4 is - // even too low of a minimum. Profile more. - numWorkers := 4 - if n := runtime.NumCPU(); n > numWorkers { - numWorkers = n - } - - // Make sure to wait for all workers to finish, otherwise - // walkFn could still be called after returning. This Wait call - // runs after close(e.donec) below. - var wg sync.WaitGroup - defer wg.Wait() - - w := &walker{ - fn: walkFn, - enqueuec: make(chan walkItem, numWorkers), // buffered for performance - workc: make(chan walkItem, numWorkers), // buffered for performance - donec: make(chan struct{}), - - // buffered for correctness & not leaking goroutines: - resc: make(chan error, numWorkers), - } - defer close(w.donec) - - for i := 0; i < numWorkers; i++ { - wg.Add(1) - go w.doWork(&wg) - } - todo := []walkItem{{dir: root}} - out := 0 - for { - workc := w.workc - var workItem walkItem - if len(todo) == 0 { - workc = nil - } else { - workItem = todo[len(todo)-1] - } - select { - case workc <- workItem: - todo = todo[:len(todo)-1] - out++ - case it := <-w.enqueuec: - todo = append(todo, it) - case err := <-w.resc: - out-- - if err != nil { - return err - } - if out == 0 && len(todo) == 0 { - // It's safe to quit here, as long as the buffered - // enqueue channel isn't also readable, which might - // happen if the worker sends both another unit of - // work and its result before the other select was - // scheduled and both w.resc and w.enqueuec were - // readable. - select { - case it := <-w.enqueuec: - todo = append(todo, it) - default: - return nil - } - } - } - } -} - -// doWork reads directories as instructed (via workc) and runs the -// user's callback function. -func (w *walker) doWork(wg *sync.WaitGroup) { - defer wg.Done() - for { - select { - case <-w.donec: - return - case it := <-w.workc: - select { - case <-w.donec: - return - case w.resc <- w.walk(it.dir, !it.callbackDone): - } - } - } -} - -type walker struct { - fn func(path string, typ os.FileMode) error - - donec chan struct{} // closed on fastWalk's return - workc chan walkItem // to workers - enqueuec chan walkItem // from workers - resc chan error // from workers -} - -type walkItem struct { - dir string - callbackDone bool // callback already called; don't do it again -} - -func (w *walker) enqueue(it walkItem) { - select { - case w.enqueuec <- it: - case <-w.donec: - } -} - -func (w *walker) onDirEnt(dirName, baseName string, typ os.FileMode) error { - joined := dirName + string(os.PathSeparator) + baseName - if typ == os.ModeDir { - w.enqueue(walkItem{dir: joined}) - return nil - } - - err := w.fn(joined, typ) - if typ == os.ModeSymlink { - if err == ErrTraverseLink { - // Set callbackDone so we don't call it twice for both the - // symlink-as-symlink and the symlink-as-directory later: - w.enqueue(walkItem{dir: joined, callbackDone: true}) - return nil - } - if err == filepath.SkipDir { - // Permit SkipDir on symlinks too. - return nil - } - } - return err -} - -func (w *walker) walk(root string, runUserCallback bool) error { - if runUserCallback { - err := w.fn(root, os.ModeDir) - if err == filepath.SkipDir { - return nil - } - if err != nil { - return err - } - } - - return readDir(root, w.onDirEnt) -} diff --git a/internal/fastwalk/fastwalk_darwin.go b/internal/fastwalk/fastwalk_darwin.go deleted file mode 100644 index 0ca55e0d56f..00000000000 --- a/internal/fastwalk/fastwalk_darwin.go +++ /dev/null @@ -1,119 +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 darwin && cgo -// +build darwin,cgo - -package fastwalk - -/* -#include - -// fastwalk_readdir_r wraps readdir_r so that we don't have to pass a dirent** -// result pointer which triggers CGO's "Go pointer to Go pointer" check unless -// we allocat the result dirent* with malloc. -// -// fastwalk_readdir_r returns 0 on success, -1 upon reaching the end of the -// directory, or a positive error number to indicate failure. -static int fastwalk_readdir_r(DIR *fd, struct dirent *entry) { - struct dirent *result; - int ret = readdir_r(fd, entry, &result); - if (ret == 0 && result == NULL) { - ret = -1; // EOF - } - return ret; -} -*/ -import "C" - -import ( - "os" - "syscall" - "unsafe" -) - -func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error { - fd, err := openDir(dirName) - if err != nil { - return &os.PathError{Op: "opendir", Path: dirName, Err: err} - } - defer C.closedir(fd) - - skipFiles := false - var dirent syscall.Dirent - for { - ret := int(C.fastwalk_readdir_r(fd, (*C.struct_dirent)(unsafe.Pointer(&dirent)))) - if ret != 0 { - if ret == -1 { - break // EOF - } - if ret == int(syscall.EINTR) { - continue - } - return &os.PathError{Op: "readdir", Path: dirName, Err: syscall.Errno(ret)} - } - if dirent.Ino == 0 { - continue - } - typ := dtToType(dirent.Type) - if skipFiles && typ.IsRegular() { - continue - } - name := (*[len(syscall.Dirent{}.Name)]byte)(unsafe.Pointer(&dirent.Name))[:] - name = name[:dirent.Namlen] - for i, c := range name { - if c == 0 { - name = name[:i] - break - } - } - // Check for useless names before allocating a string. - if string(name) == "." || string(name) == ".." { - continue - } - if err := fn(dirName, string(name), typ); err != nil { - if err != ErrSkipFiles { - return err - } - skipFiles = true - } - } - - return nil -} - -func dtToType(typ uint8) os.FileMode { - switch typ { - case syscall.DT_BLK: - return os.ModeDevice - case syscall.DT_CHR: - return os.ModeDevice | os.ModeCharDevice - case syscall.DT_DIR: - return os.ModeDir - case syscall.DT_FIFO: - return os.ModeNamedPipe - case syscall.DT_LNK: - return os.ModeSymlink - case syscall.DT_REG: - return 0 - case syscall.DT_SOCK: - return os.ModeSocket - } - return ^os.FileMode(0) -} - -// openDir wraps opendir(3) and handles any EINTR errors. The returned *DIR -// needs to be closed with closedir(3). -func openDir(path string) (*C.DIR, error) { - name, err := syscall.BytePtrFromString(path) - if err != nil { - return nil, err - } - for { - fd, err := C.opendir((*C.char)(unsafe.Pointer(name))) - if err != syscall.EINTR { - return fd, err - } - } -} diff --git a/internal/fastwalk/fastwalk_dirent_fileno.go b/internal/fastwalk/fastwalk_dirent_fileno.go deleted file mode 100644 index d58595dbd3f..00000000000 --- a/internal/fastwalk/fastwalk_dirent_fileno.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2016 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 freebsd || openbsd || netbsd -// +build freebsd openbsd netbsd - -package fastwalk - -import "syscall" - -func direntInode(dirent *syscall.Dirent) uint64 { - return uint64(dirent.Fileno) -} diff --git a/internal/fastwalk/fastwalk_dirent_ino.go b/internal/fastwalk/fastwalk_dirent_ino.go deleted file mode 100644 index d3922890b0b..00000000000 --- a/internal/fastwalk/fastwalk_dirent_ino.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2016 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 (linux || (darwin && !cgo)) && !appengine -// +build linux darwin,!cgo -// +build !appengine - -package fastwalk - -import "syscall" - -func direntInode(dirent *syscall.Dirent) uint64 { - return dirent.Ino -} diff --git a/internal/fastwalk/fastwalk_dirent_namlen_bsd.go b/internal/fastwalk/fastwalk_dirent_namlen_bsd.go deleted file mode 100644 index 38a4db6af3a..00000000000 --- a/internal/fastwalk/fastwalk_dirent_namlen_bsd.go +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build (darwin && !cgo) || freebsd || openbsd || netbsd -// +build darwin,!cgo freebsd openbsd netbsd - -package fastwalk - -import "syscall" - -func direntNamlen(dirent *syscall.Dirent) uint64 { - return uint64(dirent.Namlen) -} diff --git a/internal/fastwalk/fastwalk_dirent_namlen_linux.go b/internal/fastwalk/fastwalk_dirent_namlen_linux.go deleted file mode 100644 index c82e57df85e..00000000000 --- a/internal/fastwalk/fastwalk_dirent_namlen_linux.go +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build linux && !appengine -// +build linux,!appengine - -package fastwalk - -import ( - "bytes" - "syscall" - "unsafe" -) - -func direntNamlen(dirent *syscall.Dirent) uint64 { - const fixedHdr = uint16(unsafe.Offsetof(syscall.Dirent{}.Name)) - nameBuf := (*[unsafe.Sizeof(dirent.Name)]byte)(unsafe.Pointer(&dirent.Name[0])) - const nameBufLen = uint16(len(nameBuf)) - limit := dirent.Reclen - fixedHdr - if limit > nameBufLen { - limit = nameBufLen - } - nameLen := bytes.IndexByte(nameBuf[:limit], 0) - if nameLen < 0 { - panic("failed to find terminating 0 byte in dirent") - } - return uint64(nameLen) -} diff --git a/internal/fastwalk/fastwalk_portable.go b/internal/fastwalk/fastwalk_portable.go deleted file mode 100644 index 27e860243e1..00000000000 --- a/internal/fastwalk/fastwalk_portable.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2016 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 appengine || (!linux && !darwin && !freebsd && !openbsd && !netbsd) -// +build appengine !linux,!darwin,!freebsd,!openbsd,!netbsd - -package fastwalk - -import ( - "os" -) - -// readDir calls fn for each directory entry in dirName. -// It does not descend into directories or follow symlinks. -// If fn returns a non-nil error, readDir returns with that error -// immediately. -func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error { - fis, err := os.ReadDir(dirName) - if err != nil { - return err - } - skipFiles := false - for _, fi := range fis { - info, err := fi.Info() - if err != nil { - return err - } - if info.Mode().IsRegular() && skipFiles { - continue - } - if err := fn(dirName, fi.Name(), info.Mode()&os.ModeType); err != nil { - if err == ErrSkipFiles { - skipFiles = true - continue - } - return err - } - } - return nil -} diff --git a/internal/fastwalk/fastwalk_test.go b/internal/fastwalk/fastwalk_test.go deleted file mode 100644 index b5c82bc5293..00000000000 --- a/internal/fastwalk/fastwalk_test.go +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2016 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 fastwalk_test - -import ( - "bytes" - "flag" - "fmt" - "os" - "path/filepath" - "reflect" - "runtime" - "sort" - "strings" - "sync" - "testing" - - "golang.org/x/tools/internal/fastwalk" -) - -func formatFileModes(m map[string]os.FileMode) string { - var keys []string - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - var buf bytes.Buffer - for _, k := range keys { - fmt.Fprintf(&buf, "%-20s: %v\n", k, m[k]) - } - return buf.String() -} - -func testFastWalk(t *testing.T, files map[string]string, callback func(path string, typ os.FileMode) error, want map[string]os.FileMode) { - tempdir, err := os.MkdirTemp("", "test-fast-walk") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempdir) - - symlinks := map[string]string{} - for path, contents := range files { - file := filepath.Join(tempdir, "/src", path) - if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { - t.Fatal(err) - } - var err error - if strings.HasPrefix(contents, "LINK:") { - symlinks[file] = filepath.FromSlash(strings.TrimPrefix(contents, "LINK:")) - } else { - err = os.WriteFile(file, []byte(contents), 0644) - } - if err != nil { - t.Fatal(err) - } - } - - // Create symlinks after all other files. Otherwise, directory symlinks on - // Windows are unusable (see https://golang.org/issue/39183). - for file, dst := range symlinks { - err = os.Symlink(dst, file) - if err != nil { - if writeErr := os.WriteFile(file, []byte(dst), 0644); writeErr == nil { - // Couldn't create symlink, but could write the file. - // Probably this filesystem doesn't support symlinks. - // (Perhaps we are on an older Windows and not running as administrator.) - t.Skipf("skipping because symlinks appear to be unsupported: %v", err) - } - } - } - - got := map[string]os.FileMode{} - var mu sync.Mutex - err = fastwalk.Walk(tempdir, func(path string, typ os.FileMode) error { - mu.Lock() - defer mu.Unlock() - if !strings.HasPrefix(path, tempdir) { - t.Errorf("bogus prefix on %q, expect %q", path, tempdir) - } - key := filepath.ToSlash(strings.TrimPrefix(path, tempdir)) - if old, dup := got[key]; dup { - t.Errorf("callback called twice for key %q: %v -> %v", key, old, typ) - } - got[key] = typ - return callback(path, typ) - }) - - if err != nil { - t.Fatalf("callback returned: %v", err) - } - if !reflect.DeepEqual(got, want) { - t.Errorf("walk mismatch.\n got:\n%v\nwant:\n%v", formatFileModes(got), formatFileModes(want)) - } -} - -func TestFastWalk_Basic(t *testing.T) { - testFastWalk(t, map[string]string{ - "foo/foo.go": "one", - "bar/bar.go": "two", - "skip/skip.go": "skip", - }, - func(path string, typ os.FileMode) error { - return nil - }, - map[string]os.FileMode{ - "": os.ModeDir, - "/src": os.ModeDir, - "/src/bar": os.ModeDir, - "/src/bar/bar.go": 0, - "/src/foo": os.ModeDir, - "/src/foo/foo.go": 0, - "/src/skip": os.ModeDir, - "/src/skip/skip.go": 0, - }) -} - -func TestFastWalk_LongFileName(t *testing.T) { - longFileName := strings.Repeat("x", 255) - - testFastWalk(t, map[string]string{ - longFileName: "one", - }, - func(path string, typ os.FileMode) error { - return nil - }, - map[string]os.FileMode{ - "": os.ModeDir, - "/src": os.ModeDir, - "/src/" + longFileName: 0, - }, - ) -} - -func TestFastWalk_Symlink(t *testing.T) { - testFastWalk(t, map[string]string{ - "foo/foo.go": "one", - "bar/bar.go": "LINK:../foo/foo.go", - "symdir": "LINK:foo", - "broken/broken.go": "LINK:../nonexistent", - }, - func(path string, typ os.FileMode) error { - return nil - }, - map[string]os.FileMode{ - "": os.ModeDir, - "/src": os.ModeDir, - "/src/bar": os.ModeDir, - "/src/bar/bar.go": os.ModeSymlink, - "/src/foo": os.ModeDir, - "/src/foo/foo.go": 0, - "/src/symdir": os.ModeSymlink, - "/src/broken": os.ModeDir, - "/src/broken/broken.go": os.ModeSymlink, - }) -} - -func TestFastWalk_SkipDir(t *testing.T) { - testFastWalk(t, map[string]string{ - "foo/foo.go": "one", - "bar/bar.go": "two", - "skip/skip.go": "skip", - }, - func(path string, typ os.FileMode) error { - if typ == os.ModeDir && strings.HasSuffix(path, "skip") { - return filepath.SkipDir - } - return nil - }, - map[string]os.FileMode{ - "": os.ModeDir, - "/src": os.ModeDir, - "/src/bar": os.ModeDir, - "/src/bar/bar.go": 0, - "/src/foo": os.ModeDir, - "/src/foo/foo.go": 0, - "/src/skip": os.ModeDir, - }) -} - -func TestFastWalk_SkipFiles(t *testing.T) { - // Directory iteration order is undefined, so there's no way to know - // which file to expect until the walk happens. Rather than mess - // with the test infrastructure, just mutate want. - var mu sync.Mutex - want := map[string]os.FileMode{ - "": os.ModeDir, - "/src": os.ModeDir, - "/src/zzz": os.ModeDir, - "/src/zzz/c.go": 0, - } - - testFastWalk(t, map[string]string{ - "a_skipfiles.go": "a", - "b_skipfiles.go": "b", - "zzz/c.go": "c", - }, - func(path string, typ os.FileMode) error { - if strings.HasSuffix(path, "_skipfiles.go") { - mu.Lock() - defer mu.Unlock() - want["/src/"+filepath.Base(path)] = 0 - return fastwalk.ErrSkipFiles - } - return nil - }, - want) - if len(want) != 5 { - t.Errorf("saw too many files: wanted 5, got %v (%v)", len(want), want) - } -} - -func TestFastWalk_TraverseSymlink(t *testing.T) { - testFastWalk(t, map[string]string{ - "foo/foo.go": "one", - "bar/bar.go": "two", - "skip/skip.go": "skip", - "symdir": "LINK:foo", - }, - func(path string, typ os.FileMode) error { - if typ == os.ModeSymlink { - return fastwalk.ErrTraverseLink - } - return nil - }, - map[string]os.FileMode{ - "": os.ModeDir, - "/src": os.ModeDir, - "/src/bar": os.ModeDir, - "/src/bar/bar.go": 0, - "/src/foo": os.ModeDir, - "/src/foo/foo.go": 0, - "/src/skip": os.ModeDir, - "/src/skip/skip.go": 0, - "/src/symdir": os.ModeSymlink, - "/src/symdir/foo.go": 0, - }) -} - -var benchDir = flag.String("benchdir", runtime.GOROOT(), "The directory to scan for BenchmarkFastWalk") - -func BenchmarkFastWalk(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - err := fastwalk.Walk(*benchDir, func(path string, typ os.FileMode) error { return nil }) - if err != nil { - b.Fatal(err) - } - } -} diff --git a/internal/fastwalk/fastwalk_unix.go b/internal/fastwalk/fastwalk_unix.go deleted file mode 100644 index f12f1a734cc..00000000000 --- a/internal/fastwalk/fastwalk_unix.go +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2016 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 (linux || freebsd || openbsd || netbsd || (darwin && !cgo)) && !appengine -// +build linux freebsd openbsd netbsd darwin,!cgo -// +build !appengine - -package fastwalk - -import ( - "fmt" - "os" - "syscall" - "unsafe" -) - -const blockSize = 8 << 10 - -// unknownFileMode is a sentinel (and bogus) os.FileMode -// value used to represent a syscall.DT_UNKNOWN Dirent.Type. -const unknownFileMode os.FileMode = os.ModeNamedPipe | os.ModeSocket | os.ModeDevice - -func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error { - fd, err := open(dirName, 0, 0) - if err != nil { - return &os.PathError{Op: "open", Path: dirName, Err: err} - } - defer syscall.Close(fd) - - // The buffer must be at least a block long. - buf := make([]byte, blockSize) // stack-allocated; doesn't escape - bufp := 0 // starting read position in buf - nbuf := 0 // end valid data in buf - skipFiles := false - for { - if bufp >= nbuf { - bufp = 0 - nbuf, err = readDirent(fd, buf) - if err != nil { - return os.NewSyscallError("readdirent", err) - } - if nbuf <= 0 { - return nil - } - } - consumed, name, typ := parseDirEnt(buf[bufp:nbuf]) - bufp += consumed - if name == "" || name == "." || name == ".." { - continue - } - // Fallback for filesystems (like old XFS) that don't - // support Dirent.Type and have DT_UNKNOWN (0) there - // instead. - if typ == unknownFileMode { - fi, err := os.Lstat(dirName + "/" + name) - if err != nil { - // It got deleted in the meantime. - if os.IsNotExist(err) { - continue - } - return err - } - typ = fi.Mode() & os.ModeType - } - if skipFiles && typ.IsRegular() { - continue - } - if err := fn(dirName, name, typ); err != nil { - if err == ErrSkipFiles { - skipFiles = true - continue - } - return err - } - } -} - -func parseDirEnt(buf []byte) (consumed int, name string, typ os.FileMode) { - // golang.org/issue/37269 - dirent := &syscall.Dirent{} - copy((*[unsafe.Sizeof(syscall.Dirent{})]byte)(unsafe.Pointer(dirent))[:], buf) - if v := unsafe.Offsetof(dirent.Reclen) + unsafe.Sizeof(dirent.Reclen); uintptr(len(buf)) < v { - panic(fmt.Sprintf("buf size of %d smaller than dirent header size %d", len(buf), v)) - } - if len(buf) < int(dirent.Reclen) { - panic(fmt.Sprintf("buf size %d < record length %d", len(buf), dirent.Reclen)) - } - consumed = int(dirent.Reclen) - if direntInode(dirent) == 0 { // File absent in directory. - return - } - switch dirent.Type { - case syscall.DT_REG: - typ = 0 - case syscall.DT_DIR: - typ = os.ModeDir - case syscall.DT_LNK: - typ = os.ModeSymlink - case syscall.DT_BLK: - typ = os.ModeDevice - case syscall.DT_FIFO: - typ = os.ModeNamedPipe - case syscall.DT_SOCK: - typ = os.ModeSocket - case syscall.DT_UNKNOWN: - typ = unknownFileMode - default: - // Skip weird things. - // It's probably a DT_WHT (http://lwn.net/Articles/325369/) - // or something. Revisit if/when this package is moved outside - // of goimports. goimports only cares about regular files, - // symlinks, and directories. - return - } - - nameBuf := (*[unsafe.Sizeof(dirent.Name)]byte)(unsafe.Pointer(&dirent.Name[0])) - nameLen := direntNamlen(dirent) - - // Special cases for common things: - if nameLen == 1 && nameBuf[0] == '.' { - name = "." - } else if nameLen == 2 && nameBuf[0] == '.' && nameBuf[1] == '.' { - name = ".." - } else { - name = string(nameBuf[:nameLen]) - } - return -} - -// According to https://golang.org/doc/go1.14#runtime -// A consequence of the implementation of preemption is that on Unix systems, including Linux and macOS -// systems, programs built with Go 1.14 will receive more signals than programs built with earlier releases. -// -// This causes syscall.Open and syscall.ReadDirent sometimes fail with EINTR errors. -// We need to retry in this case. -func open(path string, mode int, perm uint32) (fd int, err error) { - for { - fd, err := syscall.Open(path, mode, perm) - if err != syscall.EINTR { - return fd, err - } - } -} - -func readDirent(fd int, buf []byte) (n int, err error) { - for { - nbuf, err := syscall.ReadDirent(fd, buf) - if err != syscall.EINTR { - return nbuf, err - } - } -} diff --git a/internal/gopathwalk/walk.go b/internal/gopathwalk/walk.go index 452e342c559..bca83d1a11a 100644 --- a/internal/gopathwalk/walk.go +++ b/internal/gopathwalk/walk.go @@ -9,13 +9,12 @@ package gopathwalk import ( "bufio" "bytes" + "io/fs" "log" "os" "path/filepath" "strings" "time" - - "golang.org/x/tools/internal/fastwalk" ) // Options controls the behavior of a Walk call. @@ -78,14 +77,25 @@ func walkDir(root Root, add func(Root, string), skip func(root Root, dir string) if opts.Logf != nil { opts.Logf("scanning %s", root.Path) } + w := &walker{ - root: root, - add: add, - skip: skip, - opts: opts, + root: root, + add: add, + skip: skip, + opts: opts, + added: make(map[string]bool), } w.init() - if err := fastwalk.Walk(root.Path, w.walk); err != nil { + + // Add a trailing path separator to cause filepath.WalkDir to traverse symlinks. + path := root.Path + if len(path) == 0 { + path = "." + string(filepath.Separator) + } else if !os.IsPathSeparator(path[len(path)-1]) { + path = path + string(filepath.Separator) + } + + if err := filepath.WalkDir(path, w.walk); err != nil { logf := opts.Logf if logf == nil { logf = log.Printf @@ -106,6 +116,8 @@ type walker struct { opts Options // Options passed to Walk by the user. ignoredDirs []os.FileInfo // The ignored directories, loaded from .goimportsignore files. + + added map[string]bool } // init initializes the walker based on its Options @@ -164,6 +176,13 @@ func (w *walker) getIgnoredDirs(path string) []string { // shouldSkipDir reports whether the file should be skipped or not. func (w *walker) shouldSkipDir(fi os.FileInfo, dir string) bool { for _, ignoredDir := range w.ignoredDirs { + // TODO(bcmills): Given that we already ought to be preserving the + // user-provided paths for directories encountered via symlinks, and that we + // don't expect GOROOT/src or GOPATH/src to itself contain symlinks, + // os.SameFile seems needlessly expensive here — it forces the caller to + // obtain an os.FileInfo instead of a potentially much cheaper fs.DirEntry. + // + // Can we drop this and use a lexical comparison instead? if os.SameFile(fi, ignoredDir) { return true } @@ -176,20 +195,25 @@ func (w *walker) shouldSkipDir(fi os.FileInfo, dir string) bool { } // walk walks through the given path. -func (w *walker) walk(path string, typ os.FileMode) error { +func (w *walker) walk(path string, d fs.DirEntry, err error) error { + typ := d.Type() if typ.IsRegular() { + if !strings.HasSuffix(path, ".go") { + return nil + } + dir := filepath.Dir(path) if dir == w.root.Path && (w.root.Type == RootGOROOT || w.root.Type == RootGOPATH) { // Doesn't make sense to have regular files // directly in your $GOPATH/src or $GOROOT/src. - return fastwalk.ErrSkipFiles - } - if !strings.HasSuffix(path, ".go") { return nil } - w.add(w.root, dir) - return fastwalk.ErrSkipFiles + if !w.added[dir] { + w.add(w.root, dir) + w.added[dir] = true + } + return nil } if typ == os.ModeDir { base := filepath.Base(path) @@ -199,20 +223,67 @@ func (w *walker) walk(path string, typ os.FileMode) error { (!w.opts.ModulesEnabled && base == "node_modules") { return filepath.SkipDir } - fi, err := os.Lstat(path) + fi, err := d.Info() if err == nil && w.shouldSkipDir(fi, path) { return filepath.SkipDir } return nil } - if typ == os.ModeSymlink { + if typ == os.ModeSymlink && err == nil { + // TODO(bcmills): 'go list all' itself ignores symlinks within GOROOT/src + // and GOPATH/src. Do we really need to traverse them here? If so, why? + + if os.IsPathSeparator(path[len(path)-1]) { + // The OS was supposed to resolve a directory symlink but didn't. + // + // On macOS this may be caused by a known libc/kernel bug; + // see https://go.dev/issue/59586. + // + // On Windows before Go 1.21, this may be caused by a bug in + // os.Lstat (fixed in https://go.dev/cl/463177). + // + // In either case, we can work around the bug by walking this level + // explicitly: first the symlink target itself, then its contents. + + fi, err := os.Stat(path) + if err != nil || !fi.IsDir() { + return nil + } + err = w.walk(path, fs.FileInfoToDirEntry(fi), nil) + if err == filepath.SkipDir { + return nil + } else if err != nil { + return err + } + + ents, _ := os.ReadDir(path) // ignore error if unreadable + for _, d := range ents { + nextPath := filepath.Join(path, d.Name()) + var err error + if d.IsDir() { + err = filepath.WalkDir(nextPath, w.walk) + } else { + err = w.walk(nextPath, d, nil) + if err == filepath.SkipDir { + break + } + } + if err != nil { + return err + } + } + return nil + } + base := filepath.Base(path) if strings.HasPrefix(base, ".#") { // Emacs noise. return nil } if w.shouldTraverse(path) { - return fastwalk.ErrTraverseLink + // Add a trailing separator to traverse the symlink. + nextPath := path + string(filepath.Separator) + return filepath.WalkDir(nextPath, w.walk) } } return nil From 3aa6cfd8275b3ffb86d3529e578f74f743189a94 Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Mon, 10 Jul 2023 12:47:11 -0400 Subject: [PATCH 060/100] internal/gopathwalk: check ignored directories lexically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously we used os.SameFile, but that is needlessly expensive when we expect the original (user-provided) paths to be preserved during the walk: the user-provided list of ignored directories is relative. Because 'go list' does not traverse symlinks, we do not expect a user's source roots to contain symlinks for Go package source either, and in the rare even that the tree contains a directory that resides within a non-ignored subdirectory symlink, the user can explicitly include all of the relative paths by which the directory may be encountered. This reduces benchmark latency by ~7.5% compared to CL 508506, bringing the overall latency to +~6% over the previous approach using internal/fastwalk. ~/x/tools$ benchstat cl508506.txt cl508507.txt goos: linux goarch: amd64 pkg: golang.org/x/tools/internal/imports cpu: AMD EPYC 7B12 │ cl508506.txt │ cl508507.txt │ │ sec/op │ sec/op vs base │ ScanModCache-24 264.4m ± 1% 244.6m ± 1% -7.49% (p=0.000 n=10) ~/x/tools$ benchstat cl508505.txt cl508507.txt goos: linux goarch: amd64 pkg: golang.org/x/tools/internal/imports cpu: AMD EPYC 7B12 │ cl508505.txt │ cl508507.txt │ │ sec/op │ sec/op vs base │ ScanModCache-24 231.0m ± 1% 244.6m ± 1% +5.90% (p=0.000 n=10) For golang/go#58035. Change-Id: I937faf7793b8fad10a88b2fdc21fa4e4001c7246 Reviewed-on: https://go-review.googlesource.com/c/tools/+/508507 Auto-Submit: Bryan Mills Reviewed-by: Heschi Kreinick LUCI-TryBot-Result: Go LUCI --- internal/gopathwalk/walk.go | 34 +++++++++++--------------------- internal/gopathwalk/walk_test.go | 7 +------ 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/internal/gopathwalk/walk.go b/internal/gopathwalk/walk.go index bca83d1a11a..f79dd8cc3f5 100644 --- a/internal/gopathwalk/walk.go +++ b/internal/gopathwalk/walk.go @@ -115,7 +115,7 @@ type walker struct { skip func(Root, string) bool // The callback that will be invoked for every dir. dir is skipped if it returns true. opts Options // Options passed to Walk by the user. - ignoredDirs []os.FileInfo // The ignored directories, loaded from .goimportsignore files. + ignoredDirs []string added map[string]bool } @@ -133,13 +133,9 @@ func (w *walker) init() { for _, p := range ignoredPaths { full := filepath.Join(w.root.Path, p) - if fi, err := os.Stat(full); err == nil { - w.ignoredDirs = append(w.ignoredDirs, fi) - if w.opts.Logf != nil { - w.opts.Logf("Directory added to ignore list: %s", full) - } - } else if w.opts.Logf != nil { - w.opts.Logf("Error statting ignored directory: %v", err) + w.ignoredDirs = append(w.ignoredDirs, full) + if w.opts.Logf != nil { + w.opts.Logf("Directory added to ignore list: %s", full) } } } @@ -174,16 +170,9 @@ func (w *walker) getIgnoredDirs(path string) []string { } // shouldSkipDir reports whether the file should be skipped or not. -func (w *walker) shouldSkipDir(fi os.FileInfo, dir string) bool { +func (w *walker) shouldSkipDir(dir string) bool { for _, ignoredDir := range w.ignoredDirs { - // TODO(bcmills): Given that we already ought to be preserving the - // user-provided paths for directories encountered via symlinks, and that we - // don't expect GOROOT/src or GOPATH/src to itself contain symlinks, - // os.SameFile seems needlessly expensive here — it forces the caller to - // obtain an os.FileInfo instead of a potentially much cheaper fs.DirEntry. - // - // Can we drop this and use a lexical comparison instead? - if os.SameFile(fi, ignoredDir) { + if dir == ignoredDir { return true } } @@ -223,8 +212,7 @@ func (w *walker) walk(path string, d fs.DirEntry, err error) error { (!w.opts.ModulesEnabled && base == "node_modules") { return filepath.SkipDir } - fi, err := d.Info() - if err == nil && w.shouldSkipDir(fi, path) { + if w.shouldSkipDir(path) { return filepath.SkipDir } return nil @@ -293,6 +281,10 @@ func (w *walker) walk(path string, d fs.DirEntry, err error) error { // should be followed. It makes sure symlinks were never visited // before to avoid symlink loops. func (w *walker) shouldTraverse(path string) bool { + if w.shouldSkipDir(path) { + return false + } + ts, err := os.Stat(path) if err != nil { logf := w.opts.Logf @@ -305,9 +297,7 @@ func (w *walker) shouldTraverse(path string) bool { if !ts.IsDir() { return false } - if w.shouldSkipDir(ts, filepath.Dir(path)) { - return false - } + // Check for symlink loops by statting each directory component // and seeing if any are the same file as ts. for { diff --git a/internal/gopathwalk/walk_test.go b/internal/gopathwalk/walk_test.go index 58abdcff6b3..e46196b720b 100644 --- a/internal/gopathwalk/walk_test.go +++ b/internal/gopathwalk/walk_test.go @@ -74,13 +74,8 @@ func TestShouldTraverse(t *testing.T) { }, } for i, tt := range tests { - fi, err := os.Stat(filepath.Join(dir, tt.dir, tt.file)) - if err != nil { - t.Errorf("%d. Stat = %v", i, err) - continue - } var w walker - got := w.shouldTraverse(filepath.Join(dir, tt.dir, fi.Name())) + got := w.shouldTraverse(filepath.Join(dir, tt.dir, tt.file)) if got != tt.want { t.Errorf("%d. shouldTraverse(%q, %q) = %v; want %v", i, tt.dir, tt.file, got, tt.want) } From 7ca319ecbb35d33f13d216afc818e58ba84e9b14 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 27 Oct 2023 15:48:40 -0400 Subject: [PATCH 061/100] go/ssa: split 'create' and 'build' for every Function This change splits the logic to create a Function and build its body, using these strategies, each a method of *builder: buildFromSyntax buildWrapper buildThunk buildBound buildPackageInit buildInstantiationWrapper buildParamsOnly Most creation operations no longer require Program.methodsMu. (There was at least one case of "Requires" function calling an "Acquires" function in the previous code.) Also: - comment each Function{..} literal to ease skimming. - rename makeX -> createX for each function that no longer builds the body. - builder.iterate does the fixed-point iteration. - anonymous functions are built directly without the unnecessary create.Add. Change-Id: I9c88a3d8b90ffdaf151616d4032b5d70ce9707a0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538276 LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King --- go/ssa/builder.go | 292 ++++++++++++++++++------------------- go/ssa/create.go | 6 +- go/ssa/func.go | 5 +- go/ssa/instantiate.go | 33 +++-- go/ssa/instantiate_test.go | 2 + go/ssa/methods.go | 7 +- go/ssa/ssa.go | 3 +- go/ssa/wrappers.go | 105 +++++++------ 8 files changed, 231 insertions(+), 222 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 9af8321cee0..4cbe97aca05 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -75,27 +75,34 @@ package ssa // parent. Anonymous functions also track where they are referred to in their // parent function. // -// Happens-before: +// Create and build: // -// The above discussion leads to the following happens-before relation for -// the BUILD and CREATE phases. -// The happens-before relation (with X= b.created.Len() -} - // Build calls Package.Build for each package in prog. // Building occurs in parallel unless the BuildSerially mode flag was set. // @@ -2528,11 +2529,10 @@ func (p *Package) build() { return // synthetic package, e.g. "testmain" } - // Ensure we have runtime type info for all exported members. - // Additionally filter for just concrete types that can be runtime types. + // Gather runtime types for exported members with ground types. // - // TODO(adonovan): ideally belongs in memberFromObject, but - // that would require package creation in topological order. + // (We can't do this in memberFromObject because it would + // require package creation in topological order.) for name, mem := range p.Members { isGround := func(m Member) bool { switch m := m.(type) { @@ -2553,19 +2553,34 @@ func (p *Package) build() { } b := builder{created: &p.created} - init := p.init - init.startBody() + b.iterate() + + // We no longer need transient information: ASTs or go/types deductions. + p.info = nil + p.created = nil + p.files = nil + p.initVersion = nil + + if p.Prog.mode&SanityCheckFunctions != 0 { + sanityCheckPackage(p) + } +} + +// buildPackageInit builds fn.Body for the synthetic package initializer. +func (b *builder) buildPackageInit(fn *Function) { + p := fn.Pkg + fn.startBody() var done *BasicBlock if p.Prog.mode&BareInits == 0 { // Make init() skip if package is already initialized. initguard := p.Var("init$guard") - doinit := init.newBasicBlock("init.start") - done = init.newBasicBlock("init.done") - emitIf(init, emitLoad(init, initguard), done, doinit) - init.currentBlock = doinit - emitStore(init, initguard, vTrue, token.NoPos) + doinit := fn.newBasicBlock("init.start") + done = fn.newBasicBlock("init.done") + emitIf(fn, emitLoad(fn, initguard), done, doinit) + fn.currentBlock = doinit + emitStore(fn, initguard, vTrue, token.NoPos) // Call the init() function of each package we import. for _, pkg := range p.Pkg.Imports() { @@ -2575,9 +2590,9 @@ func (p *Package) build() { } var v Call v.Call.Value = prereq.init - v.Call.pos = init.pos + v.Call.pos = fn.pos v.setType(types.NewTuple()) - init.emit(&v) + fn.emit(&v) } } @@ -2587,16 +2602,16 @@ func (p *Package) build() { } for _, varinit := range p.info.InitOrder { - if init.Prog.mode&LogSource != 0 { + if fn.Prog.mode&LogSource != 0 { fmt.Fprintf(os.Stderr, "build global initializer %v @ %s\n", varinit.Lhs, p.Prog.Fset.Position(varinit.Rhs.Pos())) } // Initializers for global vars are evaluated in dependency // order, but may come from arbitrary files of the package // with different versions, so we transiently update - // init.goversion for each one. (Since init is a synthetic + // fn.goversion for each one. (Since init is a synthetic // function it has no syntax of its own that needs a version.) - init.goversion = p.initVersion[varinit.Rhs] + fn.goversion = p.initVersion[varinit.Rhs] if len(varinit.Lhs) == 1 { // 1:1 initialization: var x, y = a(), b() var lval lvalue @@ -2605,19 +2620,19 @@ func (p *Package) build() { } else { lval = blank{} } - b.assign(init, lval, varinit.Rhs, true, nil) + b.assign(fn, lval, varinit.Rhs, true, nil) } else { // n:1 initialization: var x, y := f() - tuple := b.exprN(init, varinit.Rhs) + tuple := b.exprN(fn, varinit.Rhs) for i, v := range varinit.Lhs { if v.Name() == "_" { continue } - emitStore(init, p.objects[v].(*Global), emitExtract(init, tuple, i), v.Pos()) + emitStore(fn, p.objects[v].(*Global), emitExtract(fn, tuple, i), v.Pos()) } } } - init.goversion = "" // The rest of the init function is synthetic. No syntax => no goversion. + fn.goversion = "" // The rest of the init function is synthetic. No syntax => no goversion. // Call all of the declared init() functions in source order. for _, file := range p.files { @@ -2625,9 +2640,9 @@ func (p *Package) build() { if decl, ok := decl.(*ast.FuncDecl); ok { id := decl.Name if !isBlankIdent(id) && id.Name == "init" && decl.Recv == nil { - fn := p.objects[p.info.Defs[id]].(*Function) + declaredInit := p.objects[p.info.Defs[id]].(*Function) var v Call - v.Call.Value = fn + v.Call.Value = declaredInit v.setType(types.NewTuple()) p.init.emit(&v) } @@ -2637,38 +2652,9 @@ func (p *Package) build() { // Finish up init(). if p.Prog.mode&BareInits == 0 { - emitJump(init, done) - init.currentBlock = done - } - init.emit(new(Return)) - init.finishBody() - init.done() - - // Build all CREATEd functions and add runtime types. - // These Functions include package-level functions, init functions, methods, and synthetic (including unreachable/blank ones). - // Builds any functions CREATEd while building this package. - // - // Initially the created functions for the package are: - // [init, decl0, ... , declN] - // Where decl0, ..., declN are declared functions in source order, but it's not significant. - // - // As these are built, more functions (function literals, wrappers, etc.) can be CREATEd. - // Iterate until we reach a fixed point. - // - // Wait for init() to be BUILT as that cannot be built by buildFunction(). - // - for !b.done() { - b.buildCreated() // build any CREATEd and not BUILT function. May add runtime types. - b.needsRuntimeTypes() // Add all of the runtime type information. May CREATE Functions. - } - - // We no longer need transient information: ASTs or go/types deductions. - p.info = nil - p.created = nil - p.files = nil - p.initVersion = nil - - if p.Prog.mode&SanityCheckFunctions != 0 { - sanityCheckPackage(p) + emitJump(fn, done) + fn.currentBlock = done } + fn.emit(new(Return)) + fn.finishBody() } diff --git a/go/ssa/create.go b/go/ssa/create.go index fb59fe341dc..386fe0ad76a 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -97,10 +97,12 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion tparams = sigparams } + /* declared function/method (from syntax or export data) */ fn := &Function{ name: name, object: obj, Signature: sig, + build: (*builder).buildFromSyntax, syntax: syntax, pos: obj.Pos(), Pkg: pkg, @@ -112,6 +114,7 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion pkg.created.Add(fn) if syntax == nil { fn.Synthetic = "loaded from gc object file" + fn.build = (*builder).buildParamsOnly } if tparams.Len() > 0 { fn.Prog.createInstanceSet(fn) @@ -206,13 +209,14 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * initVersion: make(map[ast.Expr]string), } - // Add init() function. + /* synthesized package initializer */ p.init = &Function{ name: "init", Signature: new(types.Signature), Synthetic: "package initializer", Pkg: p, Prog: prog, + build: (*builder).buildPackageInit, info: p.info, goversion: "", // See Package.build() for details. } diff --git a/go/ssa/func.go b/go/ssa/func.go index 0e37b68bd04..f4a911b03f7 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -318,7 +318,8 @@ func (f *Function) finishBody() { numberRegisters(f) // uses f.namedRegisters } -// After this, function is done with BUILD phase. +// done marks the building of f's SSA body complete, +// along with any nested functions, and optionally prints them. func (f *Function) done() { assert(f.parent == nil, "done called on an anonymous function") @@ -328,7 +329,7 @@ func (f *Function) done() { visit(anon) // anon is done building before f. } - f.built = true // function is done with BUILD phase + f.build = nil // function is built if f.Prog.mode&PrintFunctions != 0 { printMu.Lock() diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index 3760e9884ed..e5520b74de4 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -17,6 +17,8 @@ import ( // Thread-safe. // // This is an experimental interface! It may change without warning. +// +// Acquires prog.methodsMu. func (prog *Program) _Instances(fn *Function) []*Function { if fn.typeparams.Len() == 0 || len(fn.typeargs) > 0 { return nil @@ -116,10 +118,10 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa return inst } - // CREATE instance/instantiation wrapper + // Create instance or instantiation wrapper. var syntax ast.Node if insts.syntax != nil { - syntax = insts.syntax + syntax = ast.Node(insts.syntax) } var sig *types.Signature @@ -141,26 +143,33 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa sig = prog.canon.Type(instance).(*types.Signature) } - var synthetic string - var subst *subster - - concrete := !parameterized.anyParameterized(targs) - - if prog.mode&InstantiateGenerics != 0 && concrete { + var ( + synthetic string + subst *subster + build buildFunc + ) + if prog.mode&InstantiateGenerics != 0 && !parameterized.anyParameterized(targs) { synthetic = fmt.Sprintf("instance of %s", fn.Name()) - scope := typeparams.OriginMethod(obj).Scope() - subst = makeSubster(prog.ctxt, scope, fn.typeparams, targs, false) + if syntax != nil { + scope := typeparams.OriginMethod(obj).Scope() + subst = makeSubster(prog.ctxt, scope, fn.typeparams, targs, false) + build = (*builder).buildFromSyntax + } else { + build = (*builder).buildParamsOnly + } } else { synthetic = fmt.Sprintf("instantiation wrapper of %s", fn.Name()) + build = (*builder).buildInstantiationWrapper } - name := fmt.Sprintf("%s%s", fn.Name(), targs) // may not be unique + /* generic instance or instantiation wrapper */ instance := &Function{ - name: name, + name: fmt.Sprintf("%s%s", fn.Name(), targs), // may not be unique object: obj, Signature: sig, Synthetic: synthetic, syntax: syntax, + build: build, topLevelOrigin: fn, pos: obj.Pos(), Pkg: nil, diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index cd33e7e659e..9f345cbc13e 100644 --- a/go/ssa/instantiate_test.go +++ b/go/ssa/instantiate_test.go @@ -135,6 +135,8 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) t.Errorf("Expected instances of %s to be %v. got %v", meth, want, instances) } + // TODO(adonovan): tests should not rely on unexported functions. + // build and sanity check manually created instance. var b builder b.buildFunction(instance) diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 29449837138..fb98f1ba2bd 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -48,10 +48,7 @@ func (prog *Program) MethodValue(sel *types.Selection) *Function { if m == nil { return nil // abstract method (generic) } - for !b.done() { - b.buildCreated() - b.needsRuntimeTypes() - } + b.iterate() return m } @@ -107,7 +104,7 @@ func (prog *Program) addMethod(mset *methodSet, sel *types.Selection, cr *creato needsPromotion := len(sel.index) > 1 needsIndirection := !ptrObj && ptrRecv if needsPromotion || needsIndirection { - fn = makeWrapper(prog, sel, cr) + fn = createWrapper(prog, sel, cr) } else { fn = prog.originFunc(obj) if fn.typeparams.Len() > 0 { // instantiate diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 325493ced2b..f6c0c2088c7 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -30,6 +30,7 @@ type Program struct { canon *canonizer // type canonicalization map ctxt *typeparams.Context // cache for type checking instantiations + // TODO(adonovan): split this mutex. methodsMu sync.Mutex // guards the following maps: methodSets typeutil.Map // maps type to its concrete methodSet runtimeTypes typeutil.Map // types for which rtypes are needed @@ -311,6 +312,7 @@ type Function struct { Synthetic string // provenance of synthetic function; "" for true source functions syntax ast.Node // *ast.Func{Decl,Lit}; replaced with simple ast.Node after build, unless debug mode + build buildFunc // algorithm to build function body (nil => built) parent *Function // enclosing function if anon; nil if global Pkg *Package // enclosing package; nil for shared funcs (wrappers and error.Error) Prog *Program // enclosing program @@ -324,7 +326,6 @@ type Function struct { Recover *BasicBlock // optional; control transfers here after recovered panic AnonFuncs []*Function // anonymous functions directly beneath this one referrers []Instruction // referring instructions (iff Parent() != nil) - built bool // function has completed both CREATE and BUILD phase. anonIdx int32 // position of a nested function in parent's AnonFuncs. fn.Parent()!=nil => fn.Parent().AnonFunc[fn.anonIdx] == fn. typeparams *typeparams.TypeParamList // type parameters of this function. typeparams.Len() > 0 => generic or instance of generic function diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index 77979ec02c1..bd6257aaf99 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -28,7 +28,7 @@ import ( // -- wrappers ----------------------------------------------------------- -// makeWrapper returns a synthetic method that delegates to the +// createWrapper returns a synthetic method that delegates to the // declared method denoted by meth.Obj(), first performing any // necessary pointer indirections or field selections implied by meth. // @@ -40,21 +40,17 @@ import ( // - optional implicit field selections // - meth.Obj() may denote a concrete or an interface method // - the result may be a thunk or a wrapper. -// -// EXCLUSIVE_LOCKS_REQUIRED(prog.methodsMu) -func makeWrapper(prog *Program, sel *selection, cr *creator) *Function { +func createWrapper(prog *Program, sel *selection, cr *creator) *Function { obj := sel.obj.(*types.Func) // the declared function sig := sel.typ.(*types.Signature) // type of this wrapper var recv *types.Var // wrapper's receiver or thunk's params[0] name := obj.Name() var description string - var start int // first regular param if sel.kind == types.MethodExpr { name += "$thunk" description = "thunk" recv = sig.Params().At(0) - start = 1 } else { description = "wrapper" recv = sig.Recv() @@ -62,8 +58,9 @@ func makeWrapper(prog *Program, sel *selection, cr *creator) *Function { description = fmt.Sprintf("%s for %s", description, sel.obj) if prog.mode&LogSource != 0 { - defer logStack("make %s to (%s)", description, recv.Type())() + defer logStack("create %s to (%s)", description, recv.Type())() } + /* method wrapper */ fn := &Function{ name: name, method: sel, @@ -73,36 +70,52 @@ func makeWrapper(prog *Program, sel *selection, cr *creator) *Function { Prog: prog, pos: obj.Pos(), // wrappers have no syntax + build: (*builder).buildWrapper, info: nil, goversion: "", } cr.Add(fn) + return fn +} + +// buildWrapper builds fn.Body for a method wrapper. +// Acquires fn.Prog.methodsMu. +func (b *builder) buildWrapper(fn *Function) { + var recv *types.Var // wrapper's receiver or thunk's params[0] + var start int // first regular param + if fn.method.kind == types.MethodExpr { + recv = fn.Signature.Params().At(0) + start = 1 + } else { + recv = fn.Signature.Recv() + } + fn.startBody() fn.addSpilledParam(recv) createParams(fn, start) - indices := sel.index + indices := fn.method.index var v Value = fn.Locals[0] // spilled receiver - srdt, ptrRecv := deptr(sel.recv) + srdt, ptrRecv := deptr(fn.method.recv) if ptrRecv { v = emitLoad(fn, v) // For simple indirection wrappers, perform an informative nil-check: // "value method (T).f called using nil *T pointer" - _, ptrObj := deptr(recvType(obj)) + _, ptrObj := deptr(recvType(fn.object)) if len(indices) == 1 && !ptrObj { var c Call c.Call.Value = &Builtin{ name: "ssa:wrapnilchk", sig: types.NewSignature(nil, - types.NewTuple(anonVar(sel.recv), anonVar(tString), anonVar(tString)), - types.NewTuple(anonVar(sel.recv)), false), + types.NewTuple(anonVar(fn.method.recv), anonVar(tString), anonVar(tString)), + types.NewTuple(anonVar(fn.method.recv)), false), } c.Call.Args = []Value{ v, stringConst(srdt.String()), - stringConst(sel.obj.Name()), + stringConst(fn.method.obj.Name()), } c.setType(v.Type()) v = fn.emit(&c) @@ -124,18 +137,20 @@ func makeWrapper(prog *Program, sel *selection, cr *creator) *Function { // address of implicit C field. var c Call - if r := recvType(obj); !types.IsInterface(r) { // concrete method + if r := recvType(fn.object); !types.IsInterface(r) { // concrete method if _, ptrObj := deptr(r); !ptrObj { v = emitLoad(fn, v) } - callee := prog.originFunc(obj) + callee := fn.Prog.originFunc(fn.object) if callee.typeparams.Len() > 0 { - callee = prog.lookupOrCreateInstance(callee, receiverTypeArgs(obj), cr) + fn.Prog.methodsMu.Lock() + callee = fn.Prog.lookupOrCreateInstance(callee, receiverTypeArgs(fn.object), b.created) + fn.Prog.methodsMu.Unlock() } c.Call.Value = callee c.Call.Args = append(c.Call.Args, v) } else { - c.Call.Method = obj + c.Call.Method = fn.object c.Call.Value = emitLoad(fn, v) // interface (possibly a typeparam) } for _, arg := range fn.Params[1:] { @@ -143,8 +158,6 @@ func makeWrapper(prog *Program, sel *selection, cr *creator) *Function { } emitTailCall(fn, &c) fn.finishBody() - fn.done() - return fn } // createParams creates parameters for wrapper method fn based on its @@ -159,7 +172,7 @@ func createParams(fn *Function, start int) { // -- bounds ----------------------------------------------------------- -// makeBound returns a bound method wrapper (or "bound"), a synthetic +// createBound returns a bound method wrapper (or "bound"), a synthetic // function that delegates to a concrete or interface method denoted // by obj. The resulting function has no receiver, but has one free // variable which will be used as the method's receiver in the @@ -178,19 +191,15 @@ func createParams(fn *Function, start int) { // // f := func() { return t.meth() } // -// Unlike makeWrapper, makeBound need perform no indirection or field +// Unlike createWrapper, createBound need perform no indirection or field // selections because that can be done before the closure is // constructed. -// -// EXCLUSIVE_LOCKS_ACQUIRED(meth.Prog.methodsMu) -func makeBound(prog *Program, obj *types.Func, cr *creator) *Function { - prog.methodsMu.Lock() - defer prog.methodsMu.Unlock() - +func createBound(prog *Program, obj *types.Func, cr *creator) *Function { description := fmt.Sprintf("bound method wrapper for %s", obj) if prog.mode&LogSource != 0 { defer logStack("%s", description)() } + /* bound method wrapper */ fn := &Function{ name: obj.Name() + "$bound", object: obj, @@ -199,41 +208,46 @@ func makeBound(prog *Program, obj *types.Func, cr *creator) *Function { Prog: prog, pos: obj.Pos(), // wrappers have no syntax + build: (*builder).buildBound, info: nil, goversion: "", } + fn.FreeVars = []*FreeVar{{name: "recv", typ: recvType(obj), parent: fn}} // (cyclic) cr.Add(fn) + return fn +} - fv := &FreeVar{name: "recv", typ: recvType(obj), parent: fn} - fn.FreeVars = []*FreeVar{fv} +// buildBound builds fn.Body for a bound method closure. +// Acquires fn.Prog.methodsMu. +func (b *builder) buildBound(fn *Function) { fn.startBody() createParams(fn, 0) var c Call - if !types.IsInterface(recvType(obj)) { // concrete - callee := prog.originFunc(obj) + recv := fn.FreeVars[0] + if !types.IsInterface(recvType(fn.object)) { // concrete + callee := fn.Prog.originFunc(fn.object) if callee.typeparams.Len() > 0 { - callee = prog.lookupOrCreateInstance(callee, receiverTypeArgs(obj), cr) + fn.Prog.methodsMu.Lock() + callee = fn.Prog.lookupOrCreateInstance(callee, receiverTypeArgs(fn.object), b.created) + fn.Prog.methodsMu.Unlock() } c.Call.Value = callee - c.Call.Args = []Value{fv} + c.Call.Args = []Value{recv} } else { - c.Call.Method = obj - c.Call.Value = fv // interface (possibly a typeparam) + c.Call.Method = fn.object + c.Call.Value = recv // interface (possibly a typeparam) } for _, arg := range fn.Params { c.Call.Args = append(c.Call.Args, arg) } emitTailCall(fn, &c) fn.finishBody() - fn.done() - - return fn } // -- thunks ----------------------------------------------------------- -// makeThunk returns a thunk, a synthetic function that delegates to a +// createThunk returns a thunk, a synthetic function that delegates to a // concrete or interface method denoted by sel.obj. The resulting // function has no receiver, but has an additional (first) regular // parameter. @@ -249,17 +263,12 @@ func makeBound(prog *Program, obj *types.Func, cr *creator) *Function { // f is a synthetic wrapper defined as if by: // // f := func(t T) { return t.meth() } -// -// EXCLUSIVE_LOCKS_ACQUIRED(meth.Prog.methodsMu) -func makeThunk(prog *Program, sel *selection, cr *creator) *Function { +func createThunk(prog *Program, sel *selection, cr *creator) *Function { if sel.kind != types.MethodExpr { panic(sel) } - prog.methodsMu.Lock() - defer prog.methodsMu.Unlock() - - fn := makeWrapper(prog, sel, cr) + fn := createWrapper(prog, sel, cr) if fn.Signature.Recv() != nil { panic(fn) // unexpected receiver } @@ -295,10 +304,10 @@ func toSelection(sel *types.Selection) *selection { // -- instantiations -------------------------------------------------- -// buildInstantiationWrapper creates a body for an instantiation +// buildInstantiationWrapper builds the body of an instantiation // wrapper fn. The body calls the original generic function, // bracketed by ChangeType conversions on its arguments and results. -func buildInstantiationWrapper(fn *Function) { +func (b *builder) buildInstantiationWrapper(fn *Function) { orig := fn.topLevelOrigin sig := fn.Signature From 11828ffcc987d0d5c9f54412bb05588e4e708c8e Mon Sep 17 00:00:00 2001 From: "Hana (Hyang-Ah) Kim" Date: Sun, 15 Oct 2023 22:23:45 -0400 Subject: [PATCH 062/100] gopls/internal/lsp: add OnSave diagnostics Gopls publishes diagnostics as soon as it observes issues, even while the user is in the middle of typing resulting in transient errors. Some users find this behavior rather distracting. 'diagnosticsDelay' may help avoid wasted work, but time-based decision does not always match users' expectation on when they want to see diagnostics updates. Historically, vscode-go offered two additional ways to diagnose code. * On save * Manual trigger They were implemented by running the go compiler and vet/lint tools outside gopls. Now we are working to move all code analysis logic into the language server (golang/vscode-go#2799). We need replacement for these features (golang/vscode-go#50). This change introduces a new gopls setting 'diagnosticsTrigger'. The default is 'Edit'. The additional option is 'Save', which is implemented by preventing file modification events from triggering diagnosis. This helps migrating users of the legacy "Build/Vet On Save" mode. For users of the manual trigger mode, we can consider to add the "Manual" mode and expose a custom LSP command that triggers diagnosis when we see the demand. Alternatives I explored: * Pull Model Diagnostics - LSP 3.17 introduced client-initiated diagnostics supporta The idea fits well with 'on save' or 'manual trigger' features, but I am afraid this requires non-trivial amount of work in gopls side. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_pullDiagnostics Moreover, the state of client side implementations is unclear. For example, VS Code does not seem to support all features listed in the DiagnosticClientCapability yet. The interaction between DocumentDiagnostics and WorkspaceDiagnostics, and how mature the vscode implementation is unclear to me at this moment. * Emulate from Client-side - I attempted to buffer diagnostics reports in the LSP client middleware layer, and make them visible only when files are saved. That adds a non-trivial amount of TS/JS code on the extension side which defeats the purpose of our deprecation work. Moreover, that causes the diagnostics diff to be computed in one more place (in addition to VSCode side and Gopls side), and adds complexities. I also think some users in other editors want this feature. For golang/vscode-go#50 Change-Id: If07d3446bee7bed90851ad2272d82d163ae586cd Reviewed-on: https://go-review.googlesource.com/c/tools/+/534861 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/doc/settings.md | 14 +++++++ gopls/internal/lsp/diagnostics.go | 5 ++- gopls/internal/lsp/source/api_json.go | 18 ++++++++ gopls/internal/lsp/source/options.go | 23 +++++++++++ gopls/internal/lsp/text_synchronization.go | 2 +- .../regtest/diagnostics/diagnostics_test.go | 41 +++++++++++++++++++ 6 files changed, 101 insertions(+), 2 deletions(-) diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index eca3ee803d2..47cd211d898 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -354,6 +354,20 @@ This option must be set to a valid duration string, for example `"250ms"`. Default: `"1s"`. +##### **diagnosticsTrigger** *enum* + +**This setting is experimental and may be deleted.** + +diagnosticsTrigger controls when to run diagnostics. + +Must be one of: + +* `"Edit"`: Trigger diagnostics on file edit and save. (default) +* `"Save"`: Trigger diagnostics only on file save. Events like initial workspace load +or configuration change will still trigger diagnostics. + +Default: `"Edit"`. + ##### **analysisProgressReporting** *bool* analysisProgressReporting controls whether gopls sends progress diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 4fbfd0acec3..c19788fbc54 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -155,9 +155,12 @@ func computeDiagnosticHash(diags ...*source.Diagnostic) string { return fmt.Sprintf("%x", h.Sum(nil)) } -func (s *Server) diagnoseSnapshots(snapshots map[source.Snapshot][]span.URI, onDisk bool) { +func (s *Server) diagnoseSnapshots(snapshots map[source.Snapshot][]span.URI, onDisk bool, cause ModificationSource) { var diagnosticWG sync.WaitGroup for snapshot, uris := range snapshots { + if snapshot.Options().DiagnosticsTrigger == source.DiagnosticsOnSave && cause == FromDidChange { + continue // user requested to update the diagnostics only on save. do not diagnose yet. + } diagnosticWG.Add(1) go func(snapshot source.Snapshot, uris []span.URI) { defer diagnosticWG.Done() diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 0ad9f1d3c53..429453edf42 100644 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -557,6 +557,24 @@ var GeneratedAPIJSON = &APIJSON{ Status: "advanced", Hierarchy: "ui.diagnostic", }, + { + Name: "diagnosticsTrigger", + Type: "enum", + Doc: "diagnosticsTrigger controls when to run diagnostics.\n", + EnumValues: []EnumValue{ + { + Value: "\"Edit\"", + Doc: "`\"Edit\"`: Trigger diagnostics on file edit and save. (default)\n", + }, + { + Value: "\"Save\"", + Doc: "`\"Save\"`: Trigger diagnostics only on file save. Events like initial workspace load\nor configuration change will still trigger diagnostics.\n", + }, + }, + Default: "\"Edit\"", + Status: "experimental", + Hierarchy: "ui.diagnostic", + }, { Name: "analysisProgressReporting", Type: "bool", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index e03c4c614b8..cb8f0811076 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -137,6 +137,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options { }, Vulncheck: ModeVulncheckOff, DiagnosticsDelay: 1 * time.Second, + DiagnosticsTrigger: DiagnosticsOnEdit, AnalysisProgressReporting: true, }, InlayHintOptions: InlayHintOptions{}, @@ -468,6 +469,9 @@ type DiagnosticOptions struct { // This option must be set to a valid duration string, for example `"250ms"`. DiagnosticsDelay time.Duration `status:"advanced"` + // DiagnosticsTrigger controls when to run diagnostics. + DiagnosticsTrigger DiagnosticsTrigger `status:"experimental"` + // AnalysisProgressReporting controls whether gopls sends progress // notifications when construction of its index of analysis facts is taking a // long time. Cancelling these notifications will cancel the indexing task, @@ -810,6 +814,17 @@ const ( // TODO: VulncheckRequire, VulncheckCallgraph ) +type DiagnosticsTrigger string + +const ( + // Trigger diagnostics on file edit and save. (default) + DiagnosticsOnEdit DiagnosticsTrigger = "Edit" + // Trigger diagnostics only on file save. Events like initial workspace load + // or configuration change will still trigger diagnostics. + DiagnosticsOnSave DiagnosticsTrigger = "Save" + // TODO: support "Manual"? +) + type OptionResults []OptionResult type OptionResult struct { @@ -1244,6 +1259,14 @@ func (o *Options) set(name string, value interface{}, seen map[string]struct{}) case "diagnosticsDelay": result.setDuration(&o.DiagnosticsDelay) + case "diagnosticsTrigger": + if s, ok := result.asOneOf( + string(DiagnosticsOnEdit), + string(DiagnosticsOnSave), + ); ok { + o.DiagnosticsTrigger = DiagnosticsTrigger(s) + } + case "analysisProgressReporting": result.setBool(&o.AnalysisProgressReporting) diff --git a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go index 0dddab2b14c..bccba846110 100644 --- a/gopls/internal/lsp/text_synchronization.go +++ b/gopls/internal/lsp/text_synchronization.go @@ -289,7 +289,7 @@ func (s *Server) didModifyFiles(ctx context.Context, modifications []source.File wg.Add(1) go func() { - s.diagnoseSnapshots(snapshots, onDisk) + s.diagnoseSnapshots(snapshots, onDisk, cause) release() wg.Done() }() diff --git a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go index f5aa240e49d..5849c9b5b0d 100644 --- a/gopls/internal/regtest/diagnostics/diagnostics_test.go +++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go @@ -2112,3 +2112,44 @@ func (B) New() {} ) }) } + +func TestDiagnosticsOnlyOnSaveFile(t *testing.T) { + const onlyMod = ` +-- go.mod -- +module mod.com + +go 1.12 +-- main.go -- +package main + +func main() { + Foo() +} +-- foo.go -- +package main + +func Foo() {} +` + WithOptions( + Settings{ + "diagnosticsTrigger": "Save", + }, + ).Run(t, onlyMod, func(t *testing.T, env *Env) { + env.OpenFile("foo.go") + env.RegexpReplace("foo.go", "(Foo)", "Bar") // Makes reference to Foo undefined/undeclared. + env.AfterChange(NoDiagnostics()) // No diagnostics update until file save. + + env.SaveBuffer("foo.go") + // Compiler's error message about undeclared names vary depending on the version, + // but must be explicit about the problematic name. + env.AfterChange(Diagnostics(env.AtRegexp("main.go", "Foo"), WithMessage("Foo"))) + + env.OpenFile("main.go") + env.RegexpReplace("main.go", "(Foo)", "Bar") + // No diagnostics update until file save. That results in outdated diagnostic. + env.AfterChange(Diagnostics(env.AtRegexp("main.go", "Bar"), WithMessage("Foo"))) + + env.SaveBuffer("main.go") + env.AfterChange(NoDiagnostics()) + }) +} From f4cf2220849f7117113cb0b19a49d3f382d6e79a Mon Sep 17 00:00:00 2001 From: Tim King Date: Thu, 26 Oct 2023 15:09:47 -0700 Subject: [PATCH 063/100] go/ssa: new for var semantics Adds support for the new *ast.ForStmt semantics based on the goversion. Fixes golang/go#63374 Change-Id: I52cad4af08287552dfa1b20adb300c5201a63421 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538055 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI TryBot-Result: Gopher Robot Run-TryBot: Tim King --- go/ssa/builder.go | 131 ++++++ go/ssa/func.go | 15 +- go/ssa/interp/interp_go122_test.go | 5 +- go/ssa/interp/interp_test.go | 1 + .../interp/testdata/forvarlifetime_go122.go | 402 +++++++++++++++++ go/ssa/interp/testdata/forvarlifetime_old.go | 410 ++++++++++++++++++ .../interp/testdata/rangevarlifetime_go122.go | 146 ++++++- .../interp/testdata/rangevarlifetime_old.go | 146 ++++++- 8 files changed, 1251 insertions(+), 5 deletions(-) create mode 100644 go/ssa/interp/testdata/forvarlifetime_go122.go create mode 100644 go/ssa/interp/testdata/forvarlifetime_old.go diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 4cbe97aca05..dae0eb187fd 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -1784,6 +1784,18 @@ func (b *builder) selectStmt(fn *Function, s *ast.SelectStmt, label *lblock) { // forStmt emits to fn code for the for statement s, optionally // labelled by label. func (b *builder) forStmt(fn *Function, s *ast.ForStmt, label *lblock) { + // Use forStmtGo122 instead if it applies. + if s.Init != nil { + if assign, ok := s.Init.(*ast.AssignStmt); ok && assign.Tok == token.DEFINE { + major, minor := parseGoVersion(fn.goversion) + afterGo122 := major >= 1 && minor >= 22 + if afterGo122 { + b.forStmtGo122(fn, s, label) + return + } + } + } + // ...init... // jump loop // loop: @@ -1836,6 +1848,125 @@ func (b *builder) forStmt(fn *Function, s *ast.ForStmt, label *lblock) { fn.currentBlock = done } +// forStmtGo122 emits to fn code for the for statement s, optionally +// labelled by label. s must define its variables. +// +// This allocates once per loop iteration. This is only correct in +// GoVersions >= go1.22. +func (b *builder) forStmtGo122(fn *Function, s *ast.ForStmt, label *lblock) { + // i_outer = alloc[T] + // *i_outer = ...init... // under objects[i] = i_outer + // jump loop + // loop: + // i = phi [head: i_outer, loop: i_next] + // ...cond... // under objects[i] = i + // if cond goto body else done + // body: + // ...body... // under objects[i] = i (same as loop) + // jump post + // post: + // tmp = *i + // i_next = alloc[T] + // *i_next = tmp + // ...post... // under objects[i] = i_next + // goto loop + // done: + + init := s.Init.(*ast.AssignStmt) + + pre := fn.currentBlock // current block before starting + loop := fn.newBasicBlock("for.loop") // target of back-edge + body := fn.newBasicBlock("for.body") + post := fn.newBasicBlock("for.post") // target of 'continue' + done := fn.newBasicBlock("for.done") // target of 'break' + + // For each of the n loop variables, we create three SSA values, + // outer[i], phi[i], and next[i] in pre, loop, and post. + // There is no limit on n. + lhss := init.Lhs + vars := make([]*types.Var, len(lhss)) + outers := make([]Value, len(vars)) + phis := make([]Value, len(vars)) + nexts := make([]Value, len(vars)) + for i, lhs := range lhss { + v := identVar(fn, lhs.(*ast.Ident)) + typ := fn.typ(v.Type()) + + fn.currentBlock = pre + outer := emitLocal(fn, typ, v.Pos(), v.Name()) + + fn.currentBlock = loop + phi := &Phi{Comment: v.Name()} + phi.pos = v.Pos() + phi.typ = outer.Type() + fn.emit(phi) + + fn.currentBlock = post + // If next is is local, it reuses the address and zeroes the old value. + // Load before the Alloc. + load := emitLoad(fn, phi) + next := emitLocal(fn, typ, v.Pos(), v.Name()) + emitStore(fn, next, load, token.NoPos) + + phi.Edges = []Value{outer, next} // pre edge is emitted before post edge. + + vars[i] = v + outers[i] = outer + phis[i] = phi + nexts[i] = next + } + + varsCurrentlyReferTo := func(vals []Value) { + for i, v := range vars { + fn.vars[v] = vals[i] + } + } + + // ...init... under fn.objects[v] = i_outer + fn.currentBlock = pre + varsCurrentlyReferTo(outers) + const isDef = false // assign to already-allocated outers + b.assignStmt(fn, lhss, init.Rhs, isDef) + if label != nil { + label._break = done + label._continue = post + } + emitJump(fn, loop) + + // ...cond... under fn.objects[v] = i + fn.currentBlock = loop + varsCurrentlyReferTo(phis) + if s.Cond != nil { + b.cond(fn, s.Cond, body, done) + } else { + emitJump(fn, body) + } + + // ...body... under fn.objects[v] = i + fn.currentBlock = body + fn.targets = &targets{ + tail: fn.targets, + _break: done, + _continue: post, + } + b.stmt(fn, s.Body) + fn.targets = fn.targets.tail + emitJump(fn, post) + + // ...post... under fn.objects[v] = i_next + varsCurrentlyReferTo(nexts) + fn.currentBlock = post + if s.Post != nil { + b.stmt(fn, s.Post) + } + emitJump(fn, loop) // back-edge + fn.currentBlock = done + + // TODO(taking): Optimizations for when local variables can be fused. + // Principled approach is: hoist i_next, fuse i_outer and i_next, eliminate redundant phi, and ssa-lifting. + // Unclear if we want to do any of this in general or only for range/for-loops with new lifetimes. +} + // rangeIndexed emits to fn the header for an integer-indexed loop // over array, *array or slice value x. // The v result is defined only if tv is non-nil. diff --git a/go/ssa/func.go b/go/ssa/func.go index f4a911b03f7..173ebfd4814 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -382,10 +382,21 @@ func (f *Function) debugInfo() bool { // that is local to function f or one of its enclosing functions. // If escaping, the reference comes from a potentially escaping pointer // expression and the referent must be heap-allocated. +// We assume the referent is a *Alloc or *Phi. +// (The only Phis at this stage are those created directly by go1.22 "for" loops.) func (f *Function) lookup(obj *types.Var, escaping bool) Value { if v, ok := f.vars[obj]; ok { - if alloc, ok := v.(*Alloc); ok && escaping { - alloc.Heap = true + if escaping { + switch v := v.(type) { + case *Alloc: + v.Heap = true + case *Phi: + for _, edge := range v.Edges { + if alloc, ok := edge.(*Alloc); ok { + alloc.Heap = true + } + } + } } return v // function-local var (address) } diff --git a/go/ssa/interp/interp_go122_test.go b/go/ssa/interp/interp_go122_test.go index 8a37e58e0d0..dbaeb67bae0 100644 --- a/go/ssa/interp/interp_go122_test.go +++ b/go/ssa/interp/interp_go122_test.go @@ -17,7 +17,10 @@ import ( ) func init() { - testdataTests = append(testdataTests, "rangevarlifetime_go122.go") + testdataTests = append(testdataTests, + "rangevarlifetime_go122.go", + "forvarlifetime_go122.go", + ) } // TestExperimentRange tests files in testdata with GOEXPERIMENT=range set. diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index 01219ca2397..7e12dd84131 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go @@ -121,6 +121,7 @@ var testdataTests = []string{ "deepequal.go", "defer.go", "fieldprom.go", + "forvarlifetime_old.go", "ifaceconv.go", "ifaceprom.go", "initorder.go", diff --git a/go/ssa/interp/testdata/forvarlifetime_go122.go b/go/ssa/interp/testdata/forvarlifetime_go122.go new file mode 100644 index 00000000000..94c425f7deb --- /dev/null +++ b/go/ssa/interp/testdata/forvarlifetime_go122.go @@ -0,0 +1,402 @@ +// 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.22 + +package main + +import ( + "reflect" +) + +func main() { + test_init() + bound() + manyvars() + nocond() + nopost() + address_sequences() + post_escapes() + + // Clones from cmd/compile/internal/loopvar/testdata . + for_complicated_esc_address() + for_esc_address() + for_esc_closure() + for_esc_method() +} + +// After go1.22, each i will have a distinct address and value. +var distinct = func(m, n int) []*int { + var r []*int + for i := m; i <= n; i++ { + r = append(r, &i) + } + return r +}(3, 5) + +func test_init() { + if len(distinct) != 3 { + panic(distinct) + } + for i, v := range []int{3, 4, 5} { + if v != *(distinct[i]) { + panic(distinct) + } + } +} + +func bound() { + b := func(k int) func() int { + var f func() int + for i := 0; i < k; i++ { + f = func() int { return i } // address before post updates i. So last value in the body. + } + return f + } + + if got := b(0); got != nil { + panic(got) + } + if got := b(5); got() != 4 { + panic(got()) + } +} + +func manyvars() { + // Tests declaring many variables and having one in the middle escape. + var f func() int + for i, j, k, l, m, n, o, p := 7, 6, 5, 4, 3, 2, 1, 0; p < 6; l, p = l+1, p+1 { + _, _, _, _, _, _, _, _ = i, j, k, l, m, n, o, p + f = func() int { return l } // address *before* post updates l + } + if f() != 9 { // l == p+4 + panic(f()) + } +} + +func nocond() { + var c, b, e *int + for p := 0; ; p++ { + if p%7 == 0 { + c = &p + continue + } else if p == 20 { + b = &p + break + } + e = &p + } + + if *c != 14 { + panic(c) + } + if *b != 20 { + panic(b) + } + if *e != 19 { + panic(e) + } +} + +func nopost() { + var first, last *int + for p := 0; p < 20; { + if first == nil { + first = &p + } + last = &p + + p++ + } + + if *first != 1 { + panic(first) + } + if *last != 20 { + panic(last) + } +} + +func address_sequences() { + var c, b, p []*int + + cond := func(x *int) bool { + c = append(c, x) + return *x < 5 + } + body := func(x *int) { + b = append(b, x) + } + post := func(x *int) { + p = append(p, x) + (*x)++ + } + for i := 0; cond(&i); post(&i) { + body(&i) + } + + if c[0] == c[1] { + panic(c) + } + + if !reflect.DeepEqual(c[:5], b) { + panic(c) + } + + if !reflect.DeepEqual(c[1:], p) { + panic(c) + } + + if !reflect.DeepEqual(b[1:], p[:4]) { + panic(b) + } +} + +func post_escapes() { + var p []*int + post := func(x *int) { + p = append(p, x) + (*x)++ + } + + for i := 0; i < 5; post(&i) { + } + + var got []int + for _, x := range p { + got = append(got, *x) + } + if want := []int{1, 2, 3, 4, 5}; !reflect.DeepEqual(got, want) { + panic(got) + } +} + +func for_complicated_esc_address() { + // Clone of for_complicated_esc_adress.go + ss, sa := shared(23) + ps, pa := private(23) + es, ea := experiment(23) + + if ss != ps || ss != es || ea != pa || sa == pa { + println("shared s, a", ss, sa, "; private, s, a", ps, pa, "; experiment s, a", es, ea) + panic("for_complicated_esc_address") + } +} + +func experiment(x int) (int, int) { + sum := 0 + var is []*int + for i := x; i != 1; i = i / 2 { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + i = i*3 + 1 + if i&1 == 0 { + is = append(is, &i) + for i&2 == 0 { + i = i >> 1 + } + } else { + i = i + i + } + } + + asum := 0 + for _, pi := range is { + asum += *pi + } + + return sum, asum +} + +func private(x int) (int, int) { + sum := 0 + var is []*int + I := x + for ; I != 1; I = I / 2 { + i := I + for j := 0; j < 10; j++ { + if i == j { // 10 skips + I = i + continue + } + sum++ + } + i = i*3 + 1 + if i&1 == 0 { + is = append(is, &i) + for i&2 == 0 { + i = i >> 1 + } + } else { + i = i + i + } + I = i + } + + asum := 0 + for _, pi := range is { + asum += *pi + } + + return sum, asum +} + +func shared(x int) (int, int) { + sum := 0 + var is []*int + i := x + for ; i != 1; i = i / 2 { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + i = i*3 + 1 + if i&1 == 0 { + is = append(is, &i) + for i&2 == 0 { + i = i >> 1 + } + } else { + i = i + i + } + } + + asum := 0 + for _, pi := range is { + asum += *pi + } + return sum, asum +} + +func for_esc_address() { + // Clone of for_esc_address.go + sum := 0 + var is []*int + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, &i) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected", 90, ", saw", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, pi := range is { + sum += *pi + } + if sum != 0+2+4+6+8 { + println("wrong sum, expected ", 20, ", saw ", sum) + bug = true + } + if bug { + panic("for_esc_address") + } +} + +func for_esc_closure() { + var is []func() int + + // Clone of for_esc_closure.go + sum := 0 + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, func() int { + if i%17 == 15 { + i++ + } + return i + }) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected ", 90, ", saw", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, f := range is { + sum += f() + } + if sum != 0+2+4+6+8 { + println("wrong sum, expected ", 20, ", saw ", sum) + bug = true + } + if bug { + panic("for_esc_closure") + } +} + +type I int + +func (x *I) method() int { + return int(*x) +} + +func for_esc_method() { + // Clone of for_esc_method.go + var is []func() int + sum := 0 + for i := I(0); int(i) < 10; i++ { + for j := 0; j < 10; j++ { + if int(i) == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, i.method) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected ", 90, ", saw ", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, m := range is { + sum += m() + } + if sum != 0+2+4+6+8 { + println("wrong sum, expected ", 20, ", saw ", sum) + bug = true + } + if bug { + panic("for_esc_method") + } +} diff --git a/go/ssa/interp/testdata/forvarlifetime_old.go b/go/ssa/interp/testdata/forvarlifetime_old.go new file mode 100644 index 00000000000..a89790568e2 --- /dev/null +++ b/go/ssa/interp/testdata/forvarlifetime_old.go @@ -0,0 +1,410 @@ +// 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 + +// goversion can be pinned to anything strictly before 1.22. + +package main + +import ( + "reflect" +) + +func main() { + test_init() + bound() + manyvars() + nocond() + nopost() + address_sequences() + post_escapes() + + // Clones from cmd/compile/internal/loopvar/testdata . + for_complicated_esc_address() + for_esc_address() + for_esc_closure() + for_esc_method() +} + +// pre-go1.22 all of i will have the same address and the value of 6. +var same = func(m, n int) []*int { + var r []*int + for i := m; i <= n; i++ { + r = append(r, &i) + } + return r +}(3, 5) + +func test_init() { + if len(same) != 3 { + panic(same) + } + for i := range same { + for j := range same { + if !(same[i] == same[j]) { + panic(same) + } + } + } + for i := range same { + if *(same[i]) != 6 { + panic(same) + } + } +} + +func bound() { + b := func(k int) func() int { + var f func() int + for i := 0; i < k; i++ { + f = func() int { return i } // shared address will equal k. + } + return f + } + + if got := b(0); got != nil { + panic(got) + } + if got := b(5); got() != 5 { + panic(got()) + } +} + +func manyvars() { + // Tests declaring many variables and having one in the middle escape. + var f func() int + for i, j, k, l, m, n, o, p := 7, 6, 5, 4, 3, 2, 1, 0; p < 6; l, p = l+1, p+1 { + _, _, _, _, _, _, _, _ = i, j, k, l, m, n, o, p + f = func() int { return l } // address *after* post updates l + } + if f() != 10 { // l == p+4 + panic(f()) + } +} + +func nocond() { + var c, b, e *int + for p := 0; ; p++ { + if p%7 == 0 { + c = &p + continue + } else if p == 20 { + b = &p + break + } + e = &p + } + + if *c != 20 { + panic(c) + } + if *b != 20 { + panic(b) + } + if *e != 20 { + panic(e) + } +} + +func nopost() { + var first, last *int + for p := 0; p < 20; { + if first == nil { + first = &p + } + last = &p + + p++ + } + + if *first != 20 { + panic(first) + } + if *last != 20 { + panic(last) + } +} + +func address_sequences() { + var c, b, p []*int + + cond := func(x *int) bool { + c = append(c, x) + return *x < 5 + } + body := func(x *int) { + b = append(b, x) + } + post := func(x *int) { + p = append(p, x) + (*x)++ + } + for i := 0; cond(&i); post(&i) { + body(&i) + } + + if c[0] != c[1] { + panic(c) + } + + if !reflect.DeepEqual(c[:5], b) { + panic(c) + } + + if !reflect.DeepEqual(c[1:], p) { + panic(c) + } + + if !reflect.DeepEqual(b[1:], p[:4]) { + panic(b) + } +} + +func post_escapes() { + var p []*int + post := func(x *int) { + p = append(p, x) + (*x)++ + } + + for i := 0; i < 5; post(&i) { + } + + var got []int + for _, x := range p { + got = append(got, *x) + } + if want := []int{5, 5, 5, 5, 5}; !reflect.DeepEqual(got, want) { + panic(got) + } +} + +func for_complicated_esc_address() { + // Clone of for_complicated_esc_adress.go + ss, sa := shared(23) + ps, pa := private(23) + es, ea := experiment(23) + + if ss != ps || ss != es || sa != ea || pa != 188 { + println("shared s, a", ss, sa, "; private, s, a", ps, pa, "; experiment s, a", es, ea) + panic("for_complicated_esc_address") + } +} + +func experiment(x int) (int, int) { + sum := 0 + var is []*int + for i := x; i != 1; i = i / 2 { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + i = i*3 + 1 + if i&1 == 0 { + is = append(is, &i) + for i&2 == 0 { + i = i >> 1 + } + } else { + i = i + i + } + } + + asum := 0 + for _, pi := range is { + asum += *pi + } + + return sum, asum +} + +func private(x int) (int, int) { + sum := 0 + var is []*int + I := x + for ; I != 1; I = I / 2 { + i := I + for j := 0; j < 10; j++ { + if i == j { // 10 skips + I = i + continue + } + sum++ + } + i = i*3 + 1 + if i&1 == 0 { + is = append(is, &i) + for i&2 == 0 { + i = i >> 1 + } + } else { + i = i + i + } + I = i + } + + asum := 0 + for _, pi := range is { + asum += *pi + } + + return sum, asum +} + +func shared(x int) (int, int) { + sum := 0 + var is []*int + i := x + for ; i != 1; i = i / 2 { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + i = i*3 + 1 + if i&1 == 0 { + is = append(is, &i) + for i&2 == 0 { + i = i >> 1 + } + } else { + i = i + i + } + } + + asum := 0 + for _, pi := range is { + asum += *pi + } + return sum, asum +} + +func for_esc_address() { + // Clone of for_esc_address.go + sum := 0 + var is []*int + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, &i) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected", 90, ", saw", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, pi := range is { + sum += *pi + } + if sum != 10+10+10+10+10 { + println("wrong sum, expected ", 50, ", saw ", sum) + bug = true + } + if bug { + panic("for_esc_address") + } +} + +func for_esc_closure() { + // Clone of for_esc_closure.go + var is []func() int + sum := 0 + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, func() int { + if i%17 == 15 { + i++ + } + return i + }) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected ", 90, ", saw", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, f := range is { + sum += f() + } + if sum != 10+10+10+10+10 { + println("wrong sum, expected ", 50, ", saw ", sum) + bug = true + } + if bug { + panic("for_esc_closure") + } +} + +type I int + +func (x *I) method() int { + return int(*x) +} + +func for_esc_method() { + // Clone of for_esc_method.go + sum := 0 + var is []func() int + for i := I(0); int(i) < 10; i++ { + for j := 0; j < 10; j++ { + if int(i) == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, i.method) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected ", 90, ", saw ", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, m := range is { + sum += m() + } + if sum != 10+10+10+10+10 { + println("wrong sum, expected ", 50, ", saw ", sum) + bug = true + } + if bug { + panic("for_esc_method") + } +} diff --git a/go/ssa/interp/testdata/rangevarlifetime_go122.go b/go/ssa/interp/testdata/rangevarlifetime_go122.go index 4e0ec852048..950f63e7aa2 100644 --- a/go/ssa/interp/testdata/rangevarlifetime_go122.go +++ b/go/ssa/interp/testdata/rangevarlifetime_go122.go @@ -1,7 +1,20 @@ +// 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.22 package main +func main() { + test_init() + + // Clones from cmd/compile/internal/loopvar/testdata . + range_esc_address() + range_esc_closure() + range_esc_method() +} + // After go1.22, each i will have a distinct address. var distinct = func(a [3]int) []*int { var r []*int @@ -11,7 +24,7 @@ var distinct = func(a [3]int) []*int { return r }([3]int{}) -func main() { +func test_init() { if len(distinct) != 3 { panic(distinct) } @@ -21,3 +34,134 @@ func main() { } } } + +func range_esc_address() { + // Clone of range_esc_address.go + ints := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + sum := 0 + var is []*int + for _, i := range ints { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, &i) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected", 90, ", saw ", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, pi := range is { + sum += *pi + } + if sum != 0+2+4+6+8 { + println("wrong sum, expected", 20, ", saw", sum) + bug = true + } + if bug { + panic("range_esc_address") + } +} + +func range_esc_closure() { + // Clone of range_esc_closure.go + var ints = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + var is []func() int + + sum := 0 + for _, i := range ints { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, func() int { + if i%17 == 15 { + i++ + } + return i + }) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected", 90, ", saw", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, f := range is { + sum += f() + } + if sum != 0+2+4+6+8 { + println("wrong sum, expected ", 20, ", saw ", sum) + bug = true + } + if bug { + panic("range_esc_closure") + } +} + +type I int + +func (x *I) method() int { + return int(*x) +} + +func range_esc_method() { + // Clone of range_esc_method.go + var ints = []I{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + sum := 0 + var is []func() int + for _, i := range ints { + for j := 0; j < 10; j++ { + if int(i) == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, i.method) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected", 90, ", saw", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, m := range is { + sum += m() + } + if sum != 0+2+4+6+8 { + println("wrong sum, expected ", 20, ", saw ", sum) + bug = true + } + if bug { + panic("range_esc_method") + } +} diff --git a/go/ssa/interp/testdata/rangevarlifetime_old.go b/go/ssa/interp/testdata/rangevarlifetime_old.go index f8c82b156b0..345d2a9b205 100644 --- a/go/ssa/interp/testdata/rangevarlifetime_old.go +++ b/go/ssa/interp/testdata/rangevarlifetime_old.go @@ -1,9 +1,22 @@ +// 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 // goversion can be pinned to anything strictly before 1.22. package main +func main() { + test_init() + + // Clones from cmd/compile/internal/loopvar/testdata . + range_esc_address() + range_esc_closure() + range_esc_method() +} + // pre-go1.22 all of i will have the same address. var same = func(a [3]int) []*int { var r []*int @@ -13,7 +26,7 @@ var same = func(a [3]int) []*int { return r }([3]int{}) -func main() { +func test_init() { if len(same) != 3 { panic(same) } @@ -30,3 +43,134 @@ func main() { } } } + +func range_esc_address() { + // Clone of range_esc_address.go + ints := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + sum := 0 + var is []*int + for _, i := range ints { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, &i) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected", 90, ", saw ", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, pi := range is { + sum += *pi + } + if sum != 9+9+9+9+9 { + println("wrong sum, expected", 45, ", saw", sum) + bug = true + } + if bug { + panic("range_esc_address") + } +} + +func range_esc_closure() { + // Clone of range_esc_closure.go + var ints = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + var is []func() int + + sum := 0 + for _, i := range ints { + for j := 0; j < 10; j++ { + if i == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, func() int { + if i%17 == 15 { + i++ + } + return i + }) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected", 90, ", saw", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, f := range is { + sum += f() + } + if sum != 9+9+9+9+9 { + println("wrong sum, expected ", 45, ", saw ", sum) + bug = true + } + if bug { + panic("range_esc_closure") + } +} + +type I int + +func (x *I) method() int { + return int(*x) +} + +func range_esc_method() { + // Clone of range_esc_method.go + var ints = []I{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + sum := 0 + var is []func() int + for _, i := range ints { + for j := 0; j < 10; j++ { + if int(i) == j { // 10 skips + continue + } + sum++ + } + if i&1 == 0 { + is = append(is, i.method) + } + } + + bug := false + if sum != 100-10 { + println("wrong sum, expected", 90, ", saw", sum) + bug = true + } + if len(is) != 5 { + println("wrong iterations, expected ", 5, ", saw", len(is)) + bug = true + } + sum = 0 + for _, m := range is { + sum += m() + } + if sum != 9+9+9+9+9 { + println("wrong sum, expected ", 45, ", saw ", sum) + bug = true + } + if bug { + panic("range_esc_method") + } +} From 2bc9e1664ef14fa9d49b4292217886301b5e9331 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sun, 29 Oct 2023 07:54:42 -0400 Subject: [PATCH 064/100] go/ssa: TestStdlib: report count of syntactic functions too The existing AllFunctions measure includes the result of (what turns out to be) a rather complex and underspecified transitive visitation. This change adds a SrcFunctions metric that corresponds to FuncDecl and FuncLit syntax, which is important for the performance of go/analysis/passes/buildssa. Also, add TestNetHTTP which exercises SSA building of a package without its dependencies, which corresponds to the package-at-a-time workloads seen in go/analysis. Change-Id: If32ea01b7f9d6a0ee886b062ed9218e867a103c2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538435 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King --- go/ssa/stdlib_test.go | 63 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go index 11782f778fe..05436a470be 100644 --- a/go/ssa/stdlib_test.go +++ b/go/ssa/stdlib_test.go @@ -17,6 +17,7 @@ package ssa_test import ( "go/ast" "go/token" + "go/types" "runtime" "testing" "time" @@ -34,7 +35,24 @@ func bytesAllocated() uint64 { return stats.Alloc } +// TestStdlib loads the entire standard library and its tools. +// +// Apart from a small number of internal packages that are not +// returned by the 'std' query, the set is essentially transitively +// closed, so marginal per-dependency costs are invisible. func TestStdlib(t *testing.T) { + testLoad(t, 500, "std", "cmd") +} + +// TestNetHTTP builds a single SSA package but not its dependencies. +// It may help reveal costs related to dependencies (e.g. unnecessary building). +func TestNetHTTP(t *testing.T) { + testLoad(t, 120, "net/http") +} + +func testLoad(t *testing.T, minPkgs int, patterns ...string) { + // Note: most of the commentary below applies to TestStdlib. + if testing.Short() { t.Skip("skipping in short mode; too slow (https://golang.org/issue/14113)") // ~5s } @@ -45,7 +63,7 @@ func TestStdlib(t *testing.T) { alloc0 := bytesAllocated() cfg := &packages.Config{Mode: packages.LoadSyntax} - pkgs, err := packages.Load(cfg, "std", "cmd") + pkgs, err := packages.Load(cfg, patterns...) if err != nil { t.Fatal(err) } @@ -69,9 +87,10 @@ func TestStdlib(t *testing.T) { t3 := time.Now() alloc3 := bytesAllocated() + // Sanity check to ensure we haven't dropped large numbers of packages. numPkgs := len(prog.AllPackages()) - if want := 140; numPkgs < want { - t.Errorf("Loaded only %d packages, want at least %d", numPkgs, want) + if numPkgs < minPkgs { + t.Errorf("Loaded only %d packages, want at least %d", numPkgs, minPkgs) } // Keep pkgs reachable until after we've measured memory usage. @@ -79,6 +98,7 @@ func TestStdlib(t *testing.T) { panic("unreachable") } + srcFuncs := srcFunctions(prog, pkgs) allFuncs := ssautil.AllFunctions(prog) // The assertion below is not valid if the program contains @@ -138,8 +158,43 @@ func TestStdlib(t *testing.T) { // SSA stats: t.Log("#Packages: ", numPkgs) - t.Log("#Functions: ", len(allFuncs)) + t.Log("#SrcFunctions: ", len(srcFuncs)) + t.Log("#AllFunctions: ", len(allFuncs)) t.Log("#Instructions: ", numInstrs) t.Log("#MB AST+types: ", int64(alloc1-alloc0)/1e6) t.Log("#MB SSA: ", int64(alloc3-alloc1)/1e6) } + +// srcFunctions gathers all ssa.Functions corresponding to syntax. +// (Includes generics but excludes instances and all wrappers.) +// +// This is essentially identical to the SrcFunctions logic in +// go/analysis/passes/buildssa. +func srcFunctions(prog *ssa.Program, pkgs []*packages.Package) (res []*ssa.Function) { + var addSrcFunc func(fn *ssa.Function) + addSrcFunc = func(fn *ssa.Function) { + res = append(res, fn) + for _, anon := range fn.AnonFuncs { + addSrcFunc(anon) + } + } + for _, pkg := range pkgs { + for _, file := range pkg.Syntax { + for _, decl := range file.Decls { + if decl, ok := decl.(*ast.FuncDecl); ok { + // TODO(adonovan): construct ssa.Functions for blank-named + // functions too, to avoid annoying edge cases like this. + if decl.Name.Name == "_" { + continue + } + obj := pkg.TypesInfo.Defs[decl.Name].(*types.Func) + if obj == nil { + panic("nil *Func") + } + addSrcFunc(prog.FuncValue(obj)) + } + } + } + } + return res +} From 75ff53bc6b14b0a09f5d4b838ea47ae679d748ba Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 30 Oct 2023 18:46:03 -0400 Subject: [PATCH 065/100] go/ssa: build methods of RuntimeTypes lazily This change causes the builder to no longer eagerly create the complete method set for each type in Program.RuntimeTypes. Instead, methods are created on demand, such as during the traversal performed by ssautil.AllFunctions. We expect this to reduce the number of wrappers created in single-package scenarios such as go/analysis/passes/buildssa. Each time the builder encounters a MakeInterface whose operand is a non-interface ground type, it adds the type, along with all types derivable from it via reflection, to the set Program.runtimeTypes. This set is guarded by a dedicated mutex. (The derivation algorithm, forEachReachable, is abstracted from the builder and some mistakes re: tuples and signatures are corrected. We may want to share it with go/callgraph/rta.) Since an encounter of a new runtime type no longer causes creation of methods, we can simplify the builder iteration substantially. For now, we preserve the existing behavior of RuntimeTypes, which requires two kludges: 1. the existing kludge in Package.Build to add types of exported package members; 2. a kludge added to RuntimeTypes to subtract interface types and types without methods. We will remove both kludges in the following change, which will clarify and simplify the definition of RuntimeTypes. Also, remove two dead functions. Change-Id: Ied157d08096e9f8c92671c266738e0ba26ba1acb Reviewed-on: https://go-review.googlesource.com/c/tools/+/538358 LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King --- go/ssa/builder.go | 61 +++------- go/ssa/create.go | 13 +-- go/ssa/emit.go | 54 ++------- go/ssa/func.go | 32 +----- go/ssa/instantiate.go | 8 +- go/ssa/instantiate_test.go | 11 -- go/ssa/methods.go | 229 ++++++++++++++++++------------------- go/ssa/ssa.go | 6 +- go/ssa/util.go | 18 --- 9 files changed, 157 insertions(+), 275 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index dae0eb187fd..df733548896 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -157,7 +157,6 @@ type builder struct { // Invariant: 0 <= rtypes <= finished <= created.Len() created *creator // functions created during building finished int // Invariant: create[i].built holds for i in [0,finished) - rtypes int // Invariant: all of the runtime types for create[i] have been added for i in [0,rtypes) } // cond emits to fn code to evaluate boolean condition e and jump @@ -2508,23 +2507,13 @@ start: // A buildFunc is a strategy for building the SSA body for a function. type buildFunc = func(*builder, *Function) -// iterate causes all created but unbuilt functions to be built. -// -// Since building may visit new types, and gathering methods for new -// types may create functions, this process must be iterated until it +// iterate causes all created but unbuilt functions to be built. As +// this may create new methods, the process is iterated until it // converges. func (b *builder) iterate() { - for b.rtypes < b.created.Len() { - // Build any created but unbuilt functions. - // May visit new runtime types. - for ; b.finished < b.created.Len(); b.finished++ { - fn := b.created.At(b.finished) - b.buildFunction(fn) - } - - // Gather methods for new runtime types. - // May create new functions. - b.needsRuntimeTypes() + for ; b.finished < b.created.Len(); b.finished++ { + fn := b.created.At(b.finished) + b.buildFunction(fn) } } @@ -2597,30 +2586,17 @@ func (b *builder) buildFromSyntax(fn *Function) { fn.finishBody() } -// Adds any needed runtime type information for the created functions. +// addRuntimeType records t as a runtime type, +// along with all types derivable from it using reflection. // -// May add newly CREATEd functions that may need to be built or runtime type information. -// -// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodsMu) -func (b *builder) needsRuntimeTypes() { - if b.created.Len() == 0 { - return - } - prog := b.created.At(0).Prog - - var rtypes []types.Type - for ; b.rtypes < b.finished; b.rtypes++ { - fn := b.created.At(b.rtypes) - rtypes = append(rtypes, mayNeedRuntimeTypes(fn)...) - } - - // Calling prog.needMethodsOf(T) on a basic type T is a no-op. - // Filter out the basic types to reduce acquiring prog.methodsMu. - rtypes = nonbasicTypes(rtypes) - - for _, T := range rtypes { - prog.needMethodsOf(T, b.created) - } +// Acquires prog.runtimeTypesMu. +func addRuntimeType(prog *Program, t types.Type) { + prog.runtimeTypesMu.Lock() + forEachReachable(&prog.MethodSets, t, func(t types.Type) bool { + prev, _ := prog.runtimeTypes.Set(t, true).(bool) + return !prev // already seen? + }) + prog.runtimeTypesMu.Unlock() } // Build calls Package.Build for each package in prog. @@ -2662,8 +2638,7 @@ func (p *Package) build() { // Gather runtime types for exported members with ground types. // - // (We can't do this in memberFromObject because it would - // require package creation in topological order.) + // TODO(adonovan): remove this hack in a follow-up (see CL 538357). for name, mem := range p.Members { isGround := func(m Member) bool { switch m := m.(type) { @@ -2676,7 +2651,9 @@ func (p *Package) build() { return true // *NamedConst, *Global } if ast.IsExported(name) && isGround(mem) { - p.Prog.needMethodsOf(mem.Type(), &p.created) + p.Prog.methodsMu.Lock() + addRuntimeType(p.Prog, mem.Type()) + p.Prog.methodsMu.Unlock() } } if p.Prog.mode&LogSource != 0 { diff --git a/go/ssa/create.go b/go/ssa/create.go index 386fe0ad76a..7bfcbb586fb 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -15,7 +15,6 @@ import ( "os" "sync" - "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" ) @@ -23,7 +22,7 @@ import ( // // mode controls diagnostics and checking during SSA construction. func NewProgram(fset *token.FileSet, mode BuilderMode) *Program { - prog := &Program{ + return &Program{ Fset: fset, imported: make(map[string]*Package), packages: make(map[*types.Package]*Package), @@ -33,12 +32,6 @@ func NewProgram(fset *token.FileSet, mode BuilderMode) *Program { instances: make(map[*Function]*instanceSet), parameterized: tpWalker{seen: make(map[types.Type]bool)}, } - - h := typeutil.MakeHasher() // protected by methodsMu, in effect - prog.methodSets.SetHasher(h) - prog.runtimeTypes.SetHasher(h) - - return prog } // memberFromObject populates package pkg with a member for the @@ -196,7 +189,7 @@ func (c *creator) Len() int { return len(*c) } // subsequent call to ImportedPackage(pkg.Path()). // // The real work of building SSA form for each function is not done -// until a subsequent call to Package.Build(). +// until a subsequent call to Package.Build. func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info *types.Info, importable bool) *Package { p := &Package{ Prog: prog, @@ -218,7 +211,7 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * Prog: prog, build: (*builder).buildPackageInit, info: p.info, - goversion: "", // See Package.build() for details. + goversion: "", // See Package.build for details. } p.Members[p.init.name] = p.init p.created.Add(p.init) diff --git a/go/ssa/emit.go b/go/ssa/emit.go index 132f9766e4f..3c66c40e158 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -207,6 +207,8 @@ func isValuePreserving(ut_src, ut_dst types.Type) bool { // and returns the converted value. Implicit conversions are required // by language assignability rules in assignments, parameter passing, // etc. +// +// Acquires f.Prog.methodsMu < f.Prog.runtimeTypesMu. func emitConv(f *Function, val Value, typ types.Type) Value { t_src := val.Type() @@ -243,6 +245,16 @@ func emitConv(f *Function, val Value, typ types.Type) Value { val = emitConv(f, val, types.Default(ut_src)) } + // Record the types of operands to MakeInterface, if + // non-parameterized, as they are the set of runtime types. + // TODO(taking): simplify the locking. + t := val.Type() + f.Prog.methodsMu.Lock() // for isParameterized + if f.typeparams.Len() == 0 || !f.Prog.parameterized.isParameterized(t) { + addRuntimeType(f.Prog, t) + } + f.Prog.methodsMu.Unlock() + mi := &MakeInterface{X: val} mi.setType(typ) return f.emit(mi) @@ -574,48 +586,6 @@ func emitFieldSelection(f *Function, v Value, index int, wantAddr bool, id *ast. return v } -// emitSliceToArray emits to f code to convert a slice value to an array value. -// -// Precondition: all types in type set of typ are arrays and convertible to all -// types in the type set of val.Type(). -func emitSliceToArray(f *Function, val Value, typ types.Type) Value { - // Emit the following: - // if val == nil && len(typ) == 0 { - // ptr = &[0]T{} - // } else { - // ptr = SliceToArrayPointer(val) - // } - // v = *ptr - - ptype := types.NewPointer(typ) - p := &SliceToArrayPointer{X: val} - p.setType(ptype) - ptr := f.emit(p) - - nilb := f.newBasicBlock("slicetoarray.nil") - nonnilb := f.newBasicBlock("slicetoarray.nonnil") - done := f.newBasicBlock("slicetoarray.done") - - cond := emitCompare(f, token.EQL, ptr, zeroConst(ptype), token.NoPos) - emitIf(f, cond, nilb, nonnilb) - f.currentBlock = nilb - - zero := emitLocal(f, f.typ(typ), token.NoPos, "phizero") - emitJump(f, done) - f.currentBlock = nonnilb - - emitJump(f, done) - f.currentBlock = done - - phi := &Phi{Edges: []Value{zero, ptr}, Comment: "slicetoarray"} - phi.pos = val.Pos() - phi.setType(typ) - x := f.emit(phi) - unOp := &UnOp{Op: token.MUL, X: x} - unOp.setType(typ) - return f.emit(unOp) -} - // createRecoverBlock emits to f a block of code to return after a // recovered panic, and sets f.Recover to it. // diff --git a/go/ssa/func.go b/go/ssa/func.go index 173ebfd4814..04476474ec8 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -239,36 +239,6 @@ func buildReferrers(f *Function) { } } -// mayNeedRuntimeTypes returns all of the types in the body of fn that might need runtime types. -// -// EXCLUSIVE_LOCKS_ACQUIRED(meth.Prog.methodsMu) -func mayNeedRuntimeTypes(fn *Function) []types.Type { - // Collect all types that may need rtypes, i.e. those that flow into an interface. - var ts []types.Type - for _, bb := range fn.Blocks { - for _, instr := range bb.Instrs { - if mi, ok := instr.(*MakeInterface); ok { - ts = append(ts, mi.X.Type()) - } - } - } - - // Types that contain a parameterized type are considered to not be runtime types. - if fn.typeparams.Len() == 0 { - return ts // No potentially parameterized types. - } - // Filter parameterized types, in place. - fn.Prog.methodsMu.Lock() - defer fn.Prog.methodsMu.Unlock() - filtered := ts[:0] - for _, t := range ts { - if !fn.Prog.parameterized.isParameterized(t) { - filtered = append(filtered, t) - } - } - return filtered -} - // finishBody() finalizes the contents of the function after SSA code generation of its body. // // The function is not done being built until done() is called. @@ -309,7 +279,7 @@ func (f *Function) finishBody() { lift(f) } - // clear remaining stateful variables + // clear remaining builder state f.namedResults = nil // (used by lifting) f.info = nil f.subst = nil diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index e5520b74de4..7b004ac9494 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -56,7 +56,7 @@ func (insts *instanceSet) list() []*Function { // // Precondition: fn is a package level declaration (function or method). // -// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodMu) +// Acquires prog.methodsMu. func (prog *Program) createInstanceSet(fn *Function) { assert(fn.typeparams.Len() > 0 && len(fn.typeargs) == 0, "Can only create instance sets for generic functions") @@ -80,7 +80,7 @@ func (prog *Program) createInstanceSet(fn *Function) { // // Any CREATEd instance is added to cr. // -// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodMu) +// Acquires prog.methodMu. func (prog *Program) needsInstance(fn *Function, targs []types.Type, cr *creator) *Function { prog.methodsMu.Lock() defer prog.methodsMu.Unlock() @@ -92,13 +92,15 @@ func (prog *Program) needsInstance(fn *Function, targs []types.Type, cr *creator // // Any CREATEd instance is added to cr. // -// EXCLUSIVE_LOCKS_REQUIRED(prog.methodMu) +// Requires prog.methodMu. func (prog *Program) lookupOrCreateInstance(fn *Function, targs []types.Type, cr *creator) *Function { return prog.instances[fn].lookupOrCreate(targs, &prog.parameterized, cr) } // lookupOrCreate returns the instantiation of insts.fn using targs. // If the instantiation is created, this is added to cr. +// +// Requires prog.methodMu. func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWalker, cr *creator) *Function { if insts.instances == nil { insts.instances = make(map[*typeList]*Function) diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index 9f345cbc13e..a333b23ead1 100644 --- a/go/ssa/instantiate_test.go +++ b/go/ssa/instantiate_test.go @@ -350,14 +350,3 @@ func Foo[T any, S any](t T, s S) { }) } } - -// instancesStr returns a sorted slice of string -// representation of instances. -func instancesStr(instances []*Function) []string { - var is []string - for _, i := range instances { - is = append(is, fmt.Sprintf("%v", i)) - } - sort.Strings(is) - return is -} diff --git a/go/ssa/methods.go b/go/ssa/methods.go index fb98f1ba2bd..e50de839a66 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -10,6 +10,7 @@ import ( "fmt" "go/types" + "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" ) @@ -21,7 +22,7 @@ import ( // // Thread-safe. // -// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodsMu) +// Acquires prog.methodsMu. func (prog *Program) MethodValue(sel *types.Selection) *Function { if sel.Kind() != types.MethodVal { panic(fmt.Sprintf("MethodValue(%s) kind != MethodVal", sel)) @@ -70,7 +71,7 @@ type methodSet struct { } // Precondition: T is a concrete type, e.g. !isInterface(T) and not parameterized. -// EXCLUSIVE_LOCKS_REQUIRED(prog.methodsMu) +// Requires prog.methodsMu. func (prog *Program) createMethodSet(T types.Type) *methodSet { if prog.mode&SanityCheckFunctions != 0 { if types.IsInterface(T) || prog.parameterized.isParameterized(T) { @@ -87,7 +88,7 @@ func (prog *Program) createMethodSet(T types.Type) *methodSet { // Adds any created functions to cr. // Precondition: T is a concrete type, e.g. !isInterface(T) and not parameterized. -// EXCLUSIVE_LOCKS_REQUIRED(prog.methodsMu) +// Requires prog.methodsMu. func (prog *Program) addMethod(mset *methodSet, sel *types.Selection, cr *creator) *Function { if sel.Kind() == types.MethodExpr { panic(sel) @@ -126,17 +127,23 @@ func (prog *Program) addMethod(mset *methodSet, sel *types.Selection, cr *creato // // Thread-safe. // -// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodsMu) +// Acquires prog.runtimeTypesMu. func (prog *Program) RuntimeTypes() []types.Type { - prog.methodsMu.Lock() - defer prog.methodsMu.Unlock() - - var res []types.Type - prog.methodSets.Iterate(func(T types.Type, v interface{}) { - if v.(*methodSet).complete { - res = append(res, T) + prog.runtimeTypesMu.Lock() + rtypes := prog.runtimeTypes.Keys() + prog.runtimeTypesMu.Unlock() + + // Remove interfaces and types with empty method sets, + // so as not to change the historic behavior-yet. + // TODO(adonovan): change it in the next CL 538357, + // when we remove the kludge in Package.Build, keeping + // the observable semantic changes together. + res := rtypes[:0] + for _, t := range rtypes { + if !types.IsInterface(t) && prog.MethodSets.MethodSet(t).Len() > 0 { + res = append(res, t) } - }) + } return res } @@ -149,124 +156,114 @@ func (prog *Program) declaredFunc(obj *types.Func) *Function { panic("no concrete method: " + obj.String()) } -// needMethodsOf ensures that runtime type information (including the -// complete method set) is available for the specified type T and all -// its subcomponents. -// -// needMethodsOf must be called for at least every type that is an -// operand of some MakeInterface instruction, and for the type of -// every exported package member. -// -// Adds any created functions to cr. -// -// Precondition: T is not a method signature (*Signature with Recv()!=nil). -// Precondition: T is not parameterized. +// forEachReachable calls f for type T and each type reachable from +// its type through reflection. // -// Thread-safe. (Called via Package.build from multiple builder goroutines.) -// -// TODO(adonovan): make this faster. It accounts for 20% of SSA build time. -// -// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodsMu) -func (prog *Program) needMethodsOf(T types.Type, cr *creator) { - prog.methodsMu.Lock() - prog.needMethods(T, false, cr) - prog.methodsMu.Unlock() -} - -// Precondition: T is not a method signature (*Signature with Recv()!=nil). -// Precondition: T is not parameterized. -// Recursive case: skip => don't create methods for T. +// The function f must use memoization to break cycles and +// return false when the type has already been visited. // -// EXCLUSIVE_LOCKS_REQUIRED(prog.methodsMu) -func (prog *Program) needMethods(T types.Type, skip bool, cr *creator) { - // Each package maintains its own set of types it has visited. - if prevSkip, ok := prog.runtimeTypes.At(T).(bool); ok { - // needMethods(T) was previously called - if !prevSkip || skip { - return // already seen, with same or false 'skip' value - } - } - prog.runtimeTypes.Set(T, skip) - - tmset := prog.MethodSets.MethodSet(T) - - if !skip && !types.IsInterface(T) && tmset.Len() > 0 { - // Create methods of T. - mset := prog.createMethodSet(T) - if !mset.complete { - mset.complete = true - n := tmset.Len() - for i := 0; i < n; i++ { - prog.addMethod(mset, tmset.At(i), cr) +// TODO(adonovan): publish in typeutil and share with go/callgraph/rta. +func forEachReachable(msets *typeutil.MethodSetCache, T types.Type, f func(types.Type) bool) { + var visit func(T types.Type, skip bool) + visit = func(T types.Type, skip bool) { + if !skip { + if !f(T) { + return } } - } - // Recursion over signatures of each method. - for i := 0; i < tmset.Len(); i++ { - sig := tmset.At(i).Type().(*types.Signature) - prog.needMethods(sig.Params(), false, cr) - prog.needMethods(sig.Results(), false, cr) - } + // Recursion over signatures of each method. + tmset := msets.MethodSet(T) + for i := 0; i < tmset.Len(); i++ { + sig := tmset.At(i).Type().(*types.Signature) + // It is tempting to call visit(sig, false) + // but, as noted in golang.org/cl/65450043, + // the Signature.Recv field is ignored by + // types.Identical and typeutil.Map, which + // is confusing at best. + // + // More importantly, the true signature rtype + // reachable from a method using reflection + // has no receiver but an extra ordinary parameter. + // For the Read method of io.Reader we want: + // func(Reader, []byte) (int, error) + // but here sig is: + // func([]byte) (int, error) + // with .Recv = Reader (though it is hard to + // notice because it doesn't affect Signature.String + // or types.Identical). + // + // TODO(adonovan): construct and visit the correct + // non-method signature with an extra parameter + // (though since unnamed func types have no methods + // there is essentially no actual demand for this). + // + // TODO(adonovan): document whether or not it is + // safe to skip non-exported methods (as RTA does). + visit(sig.Params(), true) // skip the Tuple + visit(sig.Results(), true) // skip the Tuple + } - switch t := T.(type) { - case *types.Basic: - // nop + switch T := T.(type) { + case *types.Basic: + // nop - case *types.Interface: - // nop---handled by recursion over method set. + case *types.Interface: + // nop---handled by recursion over method set. - case *types.Pointer: - prog.needMethods(t.Elem(), false, cr) + case *types.Pointer: + visit(T.Elem(), false) - case *types.Slice: - prog.needMethods(t.Elem(), false, cr) + case *types.Slice: + visit(T.Elem(), false) - case *types.Chan: - prog.needMethods(t.Elem(), false, cr) + case *types.Chan: + visit(T.Elem(), false) - case *types.Map: - prog.needMethods(t.Key(), false, cr) - prog.needMethods(t.Elem(), false, cr) + case *types.Map: + visit(T.Key(), false) + visit(T.Elem(), false) - case *types.Signature: - if t.Recv() != nil { - panic(fmt.Sprintf("Signature %s has Recv %s", t, t.Recv())) - } - prog.needMethods(t.Params(), false, cr) - prog.needMethods(t.Results(), false, cr) - - case *types.Named: - // A pointer-to-named type can be derived from a named - // type via reflection. It may have methods too. - prog.needMethods(types.NewPointer(T), false, cr) - - // Consider 'type T struct{S}' where S has methods. - // Reflection provides no way to get from T to struct{S}, - // only to S, so the method set of struct{S} is unwanted, - // so set 'skip' flag during recursion. - prog.needMethods(t.Underlying(), true, cr) - - case *types.Array: - prog.needMethods(t.Elem(), false, cr) - - case *types.Struct: - for i, n := 0, t.NumFields(); i < n; i++ { - prog.needMethods(t.Field(i).Type(), false, cr) - } - - case *types.Tuple: - for i, n := 0, t.Len(); i < n; i++ { - prog.needMethods(t.At(i).Type(), false, cr) - } + case *types.Signature: + if T.Recv() != nil { + panic(fmt.Sprintf("Signature %s has Recv %s", T, T.Recv())) + } + visit(T.Params(), true) // skip the Tuple + visit(T.Results(), true) // skip the Tuple + + case *types.Named: + // A pointer-to-named type can be derived from a named + // type via reflection. It may have methods too. + visit(types.NewPointer(T), false) + + // Consider 'type T struct{S}' where S has methods. + // Reflection provides no way to get from T to struct{S}, + // only to S, so the method set of struct{S} is unwanted, + // so set 'skip' flag during recursion. + visit(T.Underlying(), true) // skip the unnamed type + + case *types.Array: + visit(T.Elem(), false) + + case *types.Struct: + for i, n := 0, T.NumFields(); i < n; i++ { + // TODO(adonovan): document whether or not + // it is safe to skip non-exported fields. + visit(T.Field(i).Type(), false) + } - case *typeparams.TypeParam: - panic(T) // type parameters are always abstract. + case *types.Tuple: + for i, n := 0, T.Len(); i < n; i++ { + visit(T.At(i).Type(), false) + } - case *typeparams.Union: - // nop + case *typeparams.TypeParam, *typeparams.Union: + // Type parameters cannot be reached from ground types. + panic(T) - default: - panic(T) + default: + panic(T) + } } + visit(T, false) } diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index f6c0c2088c7..0b16d9b5519 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -33,9 +33,11 @@ type Program struct { // TODO(adonovan): split this mutex. methodsMu sync.Mutex // guards the following maps: methodSets typeutil.Map // maps type to its concrete methodSet - runtimeTypes typeutil.Map // types for which rtypes are needed instances map[*Function]*instanceSet // instances of generic functions - parameterized tpWalker // determines whether a type reaches a type parameter. + parameterized tpWalker // memoization of whether a type reaches a type parameter. + + runtimeTypesMu sync.Mutex + runtimeTypes typeutil.Map // set of runtime types (from MakeInterface) } // A Package is a single analyzed Go package containing Members for diff --git a/go/ssa/util.go b/go/ssa/util.go index 68cc971b3ee..f1d7a43a4bc 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -180,24 +180,6 @@ func makeLen(T types.Type) *Builtin { } } -// nonbasicTypes returns a list containing all of the types T in ts that are non-basic. -func nonbasicTypes(ts []types.Type) []types.Type { - if len(ts) == 0 { - return nil - } - added := make(map[types.Type]bool) // additionally filter duplicates - var filtered []types.Type - for _, T := range ts { - if !isBasic(T) { - if !added[T] { - added[T] = true - filtered = append(filtered, T) - } - } - } - return filtered -} - // 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 { From 9e94edb6139d29b241744a97ac806623c608cb8b Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 28 Oct 2023 15:23:47 -0400 Subject: [PATCH 066/100] go/ssa: clarify Program.RuntimeTypes This change clarifies the definition of RuntimeTypes so that it corresponds exactly to the set of types of MakeInterface operations, plus any types derivable from them using reflection. This is the set of types whose methods may be live even if they are never referenced by SSA IR, because of dynamic interface or reflective calls. Previously, Package.build would spuriously include among RuntimeTypes the type of any exported package member of ground, non-interface type, plus derivations. I believe the reason for this was to make the output of ssautil.AllFunctions correspond more intuitively to what some clients want. This CL moves the hack into AllFunctions, where it belongs. Unfortunately it requires privileged access to Package to ascertain whether the package was loaded from syntax. The number of SrcFunctions and AllFunctions reported by TestStdlib is unchanged. TODO: - update TestRuntimeTypes. Change-Id: I3c650e0368618729fe79271cfd8d7490f0187a93 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538357 LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King --- go/ssa/builder.go | 21 ------ go/ssa/builder_test.go | 48 +++++++------- go/ssa/create.go | 1 + go/ssa/methods.go | 29 +++----- go/ssa/ssa.go | 1 + go/ssa/ssautil/visit.go | 144 +++++++++++++++++++++++++++++++--------- go/ssa/util.go | 3 + 7 files changed, 151 insertions(+), 96 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index df733548896..c86ce4f8535 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -2635,27 +2635,6 @@ func (p *Package) build() { if p.info == nil { return // synthetic package, e.g. "testmain" } - - // Gather runtime types for exported members with ground types. - // - // TODO(adonovan): remove this hack in a follow-up (see CL 538357). - for name, mem := range p.Members { - isGround := func(m Member) bool { - switch m := m.(type) { - case *Type: - named, _ := m.Type().(*types.Named) - return named == nil || typeparams.ForNamed(named) == nil - case *Function: - return m.typeparams.Len() == 0 - } - return true // *NamedConst, *Global - } - if ast.IsExported(name) && isGround(mem) { - p.Prog.methodsMu.Lock() - addRuntimeType(p.Prog, mem.Type()) - p.Prog.methodsMu.Unlock() - } - } if p.Prog.mode&LogSource != 0 { defer logStack("build %s", p)() } diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index 4c3a35f01a7..df8763aaec0 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -59,7 +59,7 @@ func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "input.go", input, 0) if err != nil { - t.Error(err) + t.Fatal(err) return } @@ -69,7 +69,7 @@ func main() { mainPkg, _, err := ssautil.BuildPackage(&types.Config{Importer: importer.Default()}, fset, types.NewPackage("main", ""), []*ast.File{f}, mode) if err != nil { - t.Error(err) + t.Fatal(err) return } @@ -169,12 +169,15 @@ func main() { func TestRuntimeTypes(t *testing.T) { testenv.NeedsGoBuild(t) // for importer.Default() + // TODO(adonovan): these test cases don't really make logical + // sense any more. Rethink. + tests := []struct { input string want []string }{ - // An exported package-level type is needed. - {`package A; type T struct{}; func (T) f() {}`, + // An package-level type is needed. + {`package A; type T struct{}; func (T) f() {}; var x any = T{}`, []string{"*p.T", "p.T"}, }, // An unexported package-level type is not needed. @@ -182,20 +185,20 @@ func TestRuntimeTypes(t *testing.T) { nil, }, // Subcomponents of type of exported package-level var are needed. - {`package C; import "bytes"; var V struct {*bytes.Buffer}`, + {`package C; import "bytes"; var V struct {*bytes.Buffer}; var x any = &V`, []string{"*bytes.Buffer", "*struct{*bytes.Buffer}", "struct{*bytes.Buffer}"}, }, // Subcomponents of type of unexported package-level var are not needed. - {`package D; import "bytes"; var v struct {*bytes.Buffer}`, - nil, + {`package D; import "bytes"; var v struct {*bytes.Buffer}; var x any = v`, + []string{"*bytes.Buffer", "struct{*bytes.Buffer}"}, }, // Subcomponents of type of exported package-level function are needed. - {`package E; import "bytes"; func F(struct {*bytes.Buffer}) {}`, + {`package E; import "bytes"; func F(struct {*bytes.Buffer}) {}; var v any = F`, []string{"*bytes.Buffer", "struct{*bytes.Buffer}"}, }, // Subcomponents of type of unexported package-level function are not needed. - {`package F; import "bytes"; func f(struct {*bytes.Buffer}) {}`, - nil, + {`package F; import "bytes"; func f(struct {*bytes.Buffer}) {}; var v any = f`, + []string{"*bytes.Buffer", "struct{*bytes.Buffer}"}, }, // Subcomponents of type of exported method of uninstantiated unexported type are not needed. {`package G; import "bytes"; type x struct{}; func (x) G(struct {*bytes.Buffer}) {}; var v x`, @@ -206,7 +209,7 @@ func TestRuntimeTypes(t *testing.T) { []string{"*bytes.Buffer", "*p.x", "p.x", "struct{*bytes.Buffer}"}, }, // Subcomponents of type of unexported method are not needed. - {`package I; import "bytes"; type X struct{}; func (X) G(struct {*bytes.Buffer}) {}`, + {`package I; import "bytes"; type X struct{}; func (X) G(struct {*bytes.Buffer}) {}; var x any = X{}`, []string{"*bytes.Buffer", "*p.X", "p.X", "struct{*bytes.Buffer}"}, }, // Local types aren't needed. @@ -225,18 +228,10 @@ func TestRuntimeTypes(t *testing.T) { {`package M; import "bytes"; var _ interface{} = struct{*bytes.Buffer}{}`, nil, }, - } - - if typeparams.Enabled { - tests = append(tests, []struct { - input string - want []string - }{ - // MakeInterface does not create runtime type for parameterized types. - {`package N; var g interface{}; func f[S any]() { var v []S; g = v }; `, - nil, - }, - }...) + // MakeInterface does not create runtime type for parameterized types. + {`package N; var g interface{}; func f[S any]() { var v []S; g = v }; `, + nil, + }, } for _, test := range tests { // Parse the file. @@ -259,6 +254,9 @@ func TestRuntimeTypes(t *testing.T) { var typstrs []string for _, T := range ssapkg.Prog.RuntimeTypes() { + if types.IsInterface(T) || types.NewMethodSet(T).Len() == 0 { + continue // skip interfaces and types without methods + } typstrs = append(typstrs, T.String()) } sort.Strings(typstrs) @@ -925,7 +923,7 @@ func TestIssue58491(t *testing.T) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "p.go", src, 0) if err != nil { - t.Error(err) + t.Fatal(err) } files := []*ast.File{f} @@ -978,7 +976,7 @@ func TestIssue58491Rec(t *testing.T) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "p.go", src, 0) if err != nil { - t.Error(err) + t.Fatal(err) } files := []*ast.File{f} diff --git a/go/ssa/create.go b/go/ssa/create.go index 7bfcbb586fb..14367bff6c6 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -196,6 +196,7 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * Members: make(map[string]Member), objects: make(map[types.Object]Member), Pkg: pkg, + syntax: info != nil, // transient values (CREATE and BUILD phases) info: info, files: files, diff --git a/go/ssa/methods.go b/go/ssa/methods.go index e50de839a66..26c26bd9746 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -121,30 +121,23 @@ func (prog *Program) addMethod(mset *methodSet, sel *types.Selection, cr *creato return fn } -// RuntimeTypes returns a new unordered slice containing all -// concrete types in the program for which a complete (non-empty) -// method set is required at run-time. +// RuntimeTypes returns a new unordered slice containing all types in +// the program for which a runtime type is required. +// +// A runtime type is required for any non-parameterized, non-interface +// type that is converted to an interface, or for any type (including +// interface types) derivable from one through reflection. +// +// The methods of such types may be reachable through reflection or +// interface calls even if they are never called directly. // // Thread-safe. // // Acquires prog.runtimeTypesMu. func (prog *Program) RuntimeTypes() []types.Type { prog.runtimeTypesMu.Lock() - rtypes := prog.runtimeTypes.Keys() - prog.runtimeTypesMu.Unlock() - - // Remove interfaces and types with empty method sets, - // so as not to change the historic behavior-yet. - // TODO(adonovan): change it in the next CL 538357, - // when we remove the kludge in Package.Build, keeping - // the observable semantic changes together. - res := rtypes[:0] - for _, t := range rtypes { - if !types.IsInterface(t) && prog.MethodSets.MethodSet(t).Len() > 0 { - res = append(res, t) - } - } - return res + defer prog.runtimeTypesMu.Unlock() + return prog.runtimeTypes.Keys() } // declaredFunc returns the concrete function/method denoted by obj. diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 0b16d9b5519..4889a129058 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -55,6 +55,7 @@ type Package struct { objects map[types.Object]Member // mapping of package objects to members (incl. methods). Contains *NamedConst, *Global, *Function. init *Function // Func("init"); the package's init function debug bool // include full debug info in this package + syntax bool // package was loaded from syntax // The following fields are set transiently, then cleared // after building. diff --git a/go/ssa/ssautil/visit.go b/go/ssa/ssautil/visit.go index 5f27050b022..3bd26a0bf4c 100644 --- a/go/ssa/ssautil/visit.go +++ b/go/ssa/ssautil/visit.go @@ -4,7 +4,15 @@ package ssautil // import "golang.org/x/tools/go/ssa/ssautil" -import "golang.org/x/tools/go/ssa" +import ( + "go/ast" + "go/types" + + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/typeparams" + + _ "unsafe" // for linkname hack +) // This file defines utilities for visiting the SSA representation of // a Program. @@ -18,50 +26,113 @@ import "golang.org/x/tools/go/ssa" // synthetic wrappers. // // Precondition: all packages are built. +// +// TODO(adonovan): this function is underspecified. It doesn't +// actually work like a linker, which computes reachability from main +// using something like go/callgraph/cha (without materializing the +// call graph). In fact, it treats all public functions and all +// methods of public non-parameterized types as roots, even though +// they may be unreachable--but only in packages created from syntax. +// +// I think we should deprecate AllFunctions function in favor of two +// clearly defined ones: +// +// 1. The first would efficiently compute CHA reachability from a set +// of main packages, making it suitable for a whole-program +// analysis context with InstantiateGenerics, in conjunction with +// Program.Build. +// +// 2. The second would return only the set of functions corresponding +// to source Func{Decl,Lit} syntax, like SrcFunctions in +// go/analysis/passes/buildssa; this is suitable for +// package-at-a-time (or handful of packages) context. +// ssa.Package could easily expose it as a field. +// +// We could add them unexported for now and use them via the linkname hack. func AllFunctions(prog *ssa.Program) map[*ssa.Function]bool { - visit := visitor{ - prog: prog, - seen: make(map[*ssa.Function]bool), + seen := make(map[*ssa.Function]bool) + + var function func(fn *ssa.Function) + function = func(fn *ssa.Function) { + if !seen[fn] { + seen[fn] = true + var buf [10]*ssa.Value // avoid alloc in common case + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + for _, op := range instr.Operands(buf[:0]) { + if fn, ok := (*op).(*ssa.Function); ok { + function(fn) + } + } + } + } + } } - visit.program() - return visit.seen -} -type visitor struct { - prog *ssa.Program - seen map[*ssa.Function]bool -} + // TODO(adonovan): opt: provide a way to share a builder + // across a sequence of MethodValue calls. -func (visit *visitor) program() { - for _, pkg := range visit.prog.AllPackages() { - for _, mem := range pkg.Members { - if fn, ok := mem.(*ssa.Function); ok { - visit.function(fn) + methodsOf := func(T types.Type) { + if !types.IsInterface(T) { + mset := prog.MethodSets.MethodSet(T) + for i := 0; i < mset.Len(); i++ { + function(prog.MethodValue(mset.At(i))) } } } - for _, T := range visit.prog.RuntimeTypes() { - mset := visit.prog.MethodSets.MethodSet(T) - for i, n := 0, mset.Len(); i < n; i++ { - visit.function(visit.prog.MethodValue(mset.At(i))) + + // Historically, Program.RuntimeTypes used to include the type + // of any exported member of a package loaded from syntax that + // has a non-parameterized (ground) type, plus all types + // reachable from that type using reflection, even though + // these runtime types may not be required for them. + // + // Rather than break existing programs that rely on + // AllFunctions visiting extra methods that are unreferenced + // by IR and unreachable via reflection, we moved the logic + // here, unprincipled though it is. + // (See doc comment for better ideas.) + // + // Nonetheless, after the move, we no longer visit every + // method of any type recursively reachable from T, only the + // methods of T and *T themselves, and we only apply this to + // named types T, and not to the type of every exported + // package member. + exportedTypeHack := func(t *ssa.Type) { + if isSyntactic(t.Package()) && + ast.IsExported(t.Name()) && + !types.IsInterface(t.Type()) { + // Consider only named types. + // (Ignore aliases and unsafe.Pointer.) + if named, ok := t.Type().(*types.Named); ok { + if typeparams.ForNamed(named) == nil { + methodsOf(named) // T + methodsOf(types.NewPointer(named)) // *T + } + } } } -} -func (visit *visitor) function(fn *ssa.Function) { - if !visit.seen[fn] { - visit.seen[fn] = true - var buf [10]*ssa.Value // avoid alloc in common case - for _, b := range fn.Blocks { - for _, instr := range b.Instrs { - for _, op := range instr.Operands(buf[:0]) { - if fn, ok := (*op).(*ssa.Function); ok { - visit.function(fn) - } - } + for _, pkg := range prog.AllPackages() { + for _, mem := range pkg.Members { + switch mem := mem.(type) { + case *ssa.Function: + // Visit all package-level declared functions. + function(mem) + + case *ssa.Type: + exportedTypeHack(mem) } } } + + // Visit all methods of types for which runtime types were + // materialized, as they are reachable through reflection. + for _, T := range prog.RuntimeTypes() { + methodsOf(T) + } + + return seen } // MainPackages returns the subset of the specified packages @@ -76,3 +147,12 @@ func MainPackages(pkgs []*ssa.Package) []*ssa.Package { } return mains } + +// TODO(adonovan): propose a principled API for this. One possibility +// is a new field, Package.SrcFunctions []*Function, which would +// contain the list of SrcFunctions described in point 2 of the +// AllFunctions doc comment, or nil if the package is not from syntax. +// But perhaps overloading nil vs empty slice is too subtle. +// +//go:linkname isSyntactic golang.org/x/tools/go/ssa.isSyntactic +func isSyntactic(pkg *ssa.Package) bool diff --git a/go/ssa/util.go b/go/ssa/util.go index f1d7a43a4bc..c78cdbf13b3 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -366,3 +366,6 @@ func (canon *canonizer) instantiateMethod(m *types.Func, targs []types.Type, ctx obj, _, _ := types.LookupFieldOrMethod(rep, true, m.Pkg(), m.Name()) return obj.(*types.Func) } + +// Exposed to ssautil using the linkname hack. +func isSyntactic(pkg *Package) bool { return pkg.syntax } From ee250403ca38c153b52c5d8d83ce3bf8887c12af Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Mon, 30 Oct 2023 09:31:15 -0400 Subject: [PATCH 067/100] gopls/internal/regtest/marker: port all prepare markers For golang/go#54845 Change-Id: Ic9190dbd525d2f2d0b9101fda5cee2ce8dc369b7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538795 LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Findley Reviewed-by: Alan Donovan --- gopls/internal/lsp/lsp_test.go | 45 -------------- gopls/internal/lsp/regtest/marker.go | 32 ++++++++-- gopls/internal/lsp/testdata/good/good0.go | 6 -- gopls/internal/lsp/testdata/good/good1.go | 21 ------- .../rename/issue43616/issue43616.go.golden | 13 ---- .../rename/issue43616/issue43616.go.in | 7 --- .../internal/lsp/testdata/summary.txt.golden | 5 +- gopls/internal/lsp/tests/tests.go | 23 ------- .../marker/testdata/rename/issue43616.txt | 19 ++++++ .../marker/testdata/rename/prepare.txt | 62 +++++++++++++++++++ 10 files changed, 109 insertions(+), 124 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/good/good0.go delete mode 100644 gopls/internal/lsp/testdata/good/good1.go delete mode 100644 gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.golden delete mode 100644 gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.in create mode 100644 gopls/internal/regtest/marker/testdata/rename/issue43616.txt create mode 100644 gopls/internal/regtest/marker/testdata/rename/prepare.txt diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 55d5b7572f8..5a5f8562bf9 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -501,51 +501,6 @@ func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { } } -func (r *runner) PrepareRename(t *testing.T, src span.Span, want *source.PrepareItem) { - m, err := r.data.Mapper(src.URI()) - if err != nil { - t.Fatal(err) - } - loc, err := m.SpanLocation(src) - if err != nil { - t.Fatalf("failed for %v: %v", src, err) - } - params := &protocol.PrepareRenameParams{ - TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } - got, err := r.server.PrepareRename(context.Background(), params) - if err != nil { - t.Errorf("prepare rename failed for %v: got error: %v", src, err) - return - } - - // TODO(rfindley): can we consolidate on a single representation for - // PrepareRename results, and use cmp.Diff here? - - // PrepareRename may fail with no error if there was no object found at the - // position. - if got == nil { - if want.Text != "" { // expected an ident. - t.Errorf("prepare rename failed for %v: got nil", src) - } - return - } - if got.Range.Start == got.Range.End { - // Special case for 0-length ranges. Marks can't specify a 0-length range, - // so just compare the start. - if got.Range.Start != want.Range.Start { - t.Errorf("prepare rename failed: incorrect point, got %v want %v", got.Range.Start, want.Range.Start) - } - } else { - if got.Range != want.Range { - t.Errorf("prepare rename failed: incorrect range got %v want %v", got.Range, want.Range) - } - } - if got.Placeholder != want.Text { - t.Errorf("prepare rename failed: incorrect text got %v want %v", got.Placeholder, want.Text) - } -} - func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[span.URI][]byte, error) { res := make(map[span.URI][]byte) for _, docEdits := range edits { diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index 9f71ba26fa9..e2b1ffb3c5c 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -218,6 +218,11 @@ var update = flag.Bool("update", false, "if set, update test data during marker // - loc(name, location): specifies the name for a location in the source. These // locations may be referenced by other markers. // +// - preparerename(src, spn, placeholder): asserts that a textDocument/prepareRename +// request at the src location expands to the spn location, with given +// placeholder. If placeholder is "", this is treated as a negative +// assertion and prepareRename should return nil. +// // - rename(location, new, golden): specifies a renaming of the // identifier at the specified location to the new name. // The golden directory contains the transformed files. @@ -369,21 +374,15 @@ var update = flag.Bool("update", false, "if set, update test data during marker // // Existing marker tests (in ../testdata) to port: // - CallHierarchy -// - CompletionItems // - Completions // - CompletionSnippets -// - DeepCompletions -// - FuzzyCompletions // - CaseSensitiveCompletions // - RankCompletions // - SemanticTokens // - FunctionExtractions // - MethodExtractions // - Renames -// - PrepareRenames // - InlayHints -// - Signatures -// - Links // - SelectionRanges func RunMarkerTests(t *testing.T, dir string) { // The marker tests must be able to run go/packages.Load. @@ -716,6 +715,7 @@ var actionMarkerFuncs = map[string]func(marker){ "highlight": actionMarkerFunc(highlightMarker), "hover": actionMarkerFunc(hoverMarker), "implementation": actionMarkerFunc(implementationMarker), + "preparerename": actionMarkerFunc(prepareRenameMarker), "rank": actionMarkerFunc(rankMarker), "rankl": actionMarkerFunc(ranklMarker), "refs": actionMarkerFunc(refsMarker), @@ -2126,6 +2126,26 @@ func implementationMarker(mark marker, src protocol.Location, want ...protocol.L } } +func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder string) { + params := &protocol.PrepareRenameParams{ + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), + } + got, err := mark.run.env.Editor.Server.PrepareRename(mark.run.env.Ctx, params) + if err != nil { + mark.run.env.T.Fatal(err) + } + if placeholder == "" { + if got != nil { + mark.errorf("PrepareRename(...) = %v, want nil", got) + } + return + } + want := &protocol.PrepareRename2Gn{Range: spn.Range, Placeholder: placeholder} + if diff := cmp.Diff(want, got); diff != "" { + mark.errorf("mismatching PrepareRename result:\n%s", diff) + } +} + // symbolMarker implements the @symbol marker. func symbolMarker(mark marker, golden *Golden) { // Retrieve information about all symbols in this file. diff --git a/gopls/internal/lsp/testdata/good/good0.go b/gopls/internal/lsp/testdata/good/good0.go deleted file mode 100644 index 666171b6724..00000000000 --- a/gopls/internal/lsp/testdata/good/good0.go +++ /dev/null @@ -1,6 +0,0 @@ -package good - -func stuff() { //@item(good_stuff, "stuff", "func()", "func"),prepare("stu", "stuff", "stuff") - x := 5 - random2(x) //@prepare("dom", "random2", "random2") -} diff --git a/gopls/internal/lsp/testdata/good/good1.go b/gopls/internal/lsp/testdata/good/good1.go deleted file mode 100644 index 7d39629a727..00000000000 --- a/gopls/internal/lsp/testdata/good/good1.go +++ /dev/null @@ -1,21 +0,0 @@ -package good - -import ( - "golang.org/lsptests/types" //@item(types_import, "types", "\"golang.org/lsptests/types\"", "package") -) - -func random() int { //@item(good_random, "random", "func() int", "func") - _ = "random() int" //@prepare("random", "", "") - y := 6 + 7 //@prepare("7", "", "") - return y //@prepare("return", "","") -} - -func random2(y int) int { //@item(good_random2, "random2", "func(y int) int", "func"),item(good_y_param, "y", "int", "var") - //@complete("", good_y_param, types_import, good_random, good_random2, good_stuff) - var b types.Bob = &types.X{} //@prepare("ypes","types", "types") - if _, ok := b.(*types.X); ok { //@complete("X", X_struct, Y_struct, Bob_interface, CoolAlias) - _ = 0 // suppress "empty branch" diagnostic - } - - return y -} diff --git a/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.golden b/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.golden deleted file mode 100644 index 34d03ba7aa6..00000000000 --- a/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.golden +++ /dev/null @@ -1,13 +0,0 @@ --- bar-rename -- -package issue43616 - -type bar int //@rename("foo","bar"),prepare("oo","foo","foo") - -var x struct{ bar } //@rename("foo","baz") - -var _ = x.bar //@rename("foo","quux") - --- baz-rename -- -can't rename embedded fields: rename the type directly or name the field --- quux-rename -- -can't rename embedded fields: rename the type directly or name the field diff --git a/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.in b/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.in deleted file mode 100644 index aaad531b732..00000000000 --- a/gopls/internal/lsp/testdata/rename/issue43616/issue43616.go.in +++ /dev/null @@ -1,7 +0,0 @@ -package issue43616 - -type foo int //@rename("foo","bar"),prepare("oo","foo","foo") - -var x struct{ foo } //@rename("foo","baz") - -var _ = x.foo //@rename("foo","quux") diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index b7c54e7009a..c6776b47340 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,13 +1,12 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 177 +CompletionsCount = 175 CompletionSnippetCount = 27 RankedCompletionsCount = 151 SemanticTokenCount = 3 SuggestedFixCount = 80 MethodExtractionCount = 8 InlayHintsCount = 5 -RenamesCount = 48 -PrepareRenamesCount = 7 +RenamesCount = 45 SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 4350b24ae76..631d873ac01 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -60,7 +60,6 @@ type SemanticTokens = []span.Span type SuggestedFixes = map[span.Span][]SuggestedFix type MethodExtractions = map[span.Span]span.Span type Renames = map[span.Span]string -type PrepareRenames = map[span.Span]*source.PrepareItem type InlayHints = []span.Span type AddImport = map[span.URI]string type SelectionRanges = []span.Span @@ -78,7 +77,6 @@ type Data struct { MethodExtractions MethodExtractions Renames Renames InlayHints InlayHints - PrepareRenames PrepareRenames AddImport AddImport SelectionRanges SelectionRanges @@ -109,7 +107,6 @@ type Tests interface { MethodExtraction(*testing.T, span.Span, span.Span) InlayHints(*testing.T, span.Span) Rename(*testing.T, span.Span, string) - PrepareRename(*testing.T, span.Span, *source.PrepareItem) AddImport(*testing.T, span.URI, string) SelectionRanges(*testing.T, span.Span) } @@ -213,7 +210,6 @@ func load(t testing.TB, mode string, dir string) *Data { CompletionSnippets: make(CompletionSnippets), RankCompletions: make(RankCompletions), Renames: make(Renames), - PrepareRenames: make(PrepareRenames), SuggestedFixes: make(SuggestedFixes), MethodExtractions: make(MethodExtractions), AddImport: make(AddImport), @@ -356,7 +352,6 @@ func load(t testing.TB, mode string, dir string) *Data { "semantic": datum.collectSemanticTokens, "inlayHint": datum.collectInlayHints, "rename": datum.collectRenames, - "prepare": datum.collectPrepareRenames, "suggestedfix": datum.collectSuggestedFixes, "extractmethod": datum.collectMethodExtractions, "incomingcalls": datum.collectIncomingCalls, @@ -531,16 +526,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("PrepareRenames", func(t *testing.T) { - t.Helper() - for src, want := range data.PrepareRenames { - t.Run(SpanName(src), func(t *testing.T) { - t.Helper() - tests.PrepareRename(t, src, want) - }) - } - }) - t.Run("AddImport", func(t *testing.T) { t.Helper() for uri, exp := range data.AddImport { @@ -598,7 +583,6 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "MethodExtractionCount = %v\n", len(data.MethodExtractions)) fmt.Fprintf(buf, "InlayHintsCount = %v\n", len(data.InlayHints)) fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames)) - fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames)) fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges)) want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { @@ -775,13 +759,6 @@ func (data *Data) collectRenames(src span.Span, newText string) { data.Renames[src] = newText } -func (data *Data) collectPrepareRenames(src, spn span.Span, placeholder string) { - data.PrepareRenames[src] = &source.PrepareItem{ - Range: data.mustRange(spn), - Text: placeholder, - } -} - // mustRange converts spn into a protocol.Range, panicking on any error. func (data *Data) mustRange(spn span.Span) protocol.Range { m, err := data.Mapper(spn.URI()) diff --git a/gopls/internal/regtest/marker/testdata/rename/issue43616.txt b/gopls/internal/regtest/marker/testdata/rename/issue43616.txt new file mode 100644 index 00000000000..3ff2ee37e27 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/rename/issue43616.txt @@ -0,0 +1,19 @@ +This test verifies the fix for golang/go#43616: renaming mishandles embedded +fields. + +-- p.go -- +package issue43616 + +type foo int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo") + +var x struct{ foo } //@renameerr("foo", "baz", "rename the type directly") + +var _ = x.foo //@renameerr("foo", "quux", "rename the type directly") +-- @fooToBar/p.go -- +package issue43616 + +type bar int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo") + +var x struct{ bar } //@renameerr("foo", "baz", "rename the type directly") + +var _ = x.bar //@renameerr("foo", "quux", "rename the type directly") diff --git a/gopls/internal/regtest/marker/testdata/rename/prepare.txt b/gopls/internal/regtest/marker/testdata/rename/prepare.txt new file mode 100644 index 00000000000..cd8439e41b3 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/rename/prepare.txt @@ -0,0 +1,62 @@ +This test verifies the behavior of textDocument/prepareRename. + +-- settings.json -- +{ + "deepCompletion": false +} + +-- go.mod -- +module golang.org/lsptests + +go 1.18 +-- types/types.go -- +package types + +type CoolAlias = int //@item(CoolAlias, "CoolAlias", "int", "type") + +type X struct { //@item(X_struct, "X", "struct{...}", "struct") + x int +} + +type Y struct { //@item(Y_struct, "Y", "struct{...}", "struct") + y int +} + + +type Bob interface { //@item(Bob_interface, "Bob", "interface{...}", "interface") + Bobby() +} + +func (*X) Bobby() {} +func (*Y) Bobby() {} + +-- good/good0.go -- +package good + +func stuff() { //@item(good_stuff, "stuff", "func()", "func"),preparerename("stu", "stuff", "stuff") + x := 5 + random2(x) //@preparerename("dom", "random2", "random2") +} + +-- good/good1.go -- +package good + +import ( + "golang.org/lsptests/types" //@item(types_import, "types", "\"golang.org/lsptests/types\"", "package") +) + +func random() int { //@item(good_random, "random", "func() int", "func") + _ = "random() int" //@preparerename("random", "", "") + y := 6 + 7 //@preparerename("7", "", "") + return y //@preparerename("return", "","") +} + +func random2(y int) int { //@item(good_random2, "random2", "func(y int) int", "func"),item(good_y_param, "y", "int", "var") + //@complete("", good_y_param, types_import, good_random, good_random2, good_stuff) + var b types.Bob = &types.X{} //@preparerename("ypes","types", "types") + if _, ok := b.(*types.X); ok { //@complete("X", X_struct, Y_struct, Bob_interface, CoolAlias) + _ = 0 // suppress "empty branch" diagnostic + } + + return y +} From 02f375819828d7c6d7dbbfeafdb774ec7fcf60dd Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 1 Nov 2023 08:57:13 -0400 Subject: [PATCH 068/100] gopls/internal/regtest/marker: port five arbitrary completion tests Port the append, assign, comment, danglingstmt, and interfacerank completion tests. Aside from copying the code and adding some minor documentation, the only change is to remove the redundant argument in the snippet markers. In the old marker framework every completion test resets options, so porting them to the new marker framework (which has static options) is blocking work on zero-config gopls, where one of the implementation steps is to make options immutable on the View. For golang/go#57979 For golang/go#54845 Change-Id: I25559dd53932efca20851e4e31f97311f70bb734 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538797 Reviewed-by: Alan Donovan Auto-Submit: Robert Findley LUCI-TryBot-Result: Go LUCI --- .../lsp/testdata/append/append2.go.in | 5 - .../testdata/assign/internal/secret/secret.go | 3 - .../lsp/testdata/danglingstmt/dangling_for.go | 9 - .../danglingstmt/dangling_for_init.go | 9 - .../danglingstmt/dangling_for_init_cond.go | 9 - .../dangling_for_init_cond_post.go | 9 - .../lsp/testdata/danglingstmt/dangling_if.go | 9 - .../testdata/danglingstmt/dangling_if_eof.go | 8 - .../testdata/danglingstmt/dangling_if_init.go | 9 - .../danglingstmt/dangling_if_init_cond.go | 9 - .../danglingstmt/dangling_multiline_if.go | 10 -- .../danglingstmt/dangling_selector_1.go | 7 - .../danglingstmt/dangling_selector_2.go | 10 -- .../danglingstmt/dangling_switch_init.go | 9 - .../danglingstmt/dangling_switch_init_tag.go | 9 - .../internal/lsp/testdata/summary.txt.golden | 6 +- .../marker/testdata/completion/append.txt} | 26 ++- .../marker/testdata/completion/assign.txt} | 21 +++ .../marker/testdata/completion/comment.txt} | 11 ++ .../testdata/completion/danglingstmt.txt | 158 ++++++++++++++++++ .../testdata/completion/interfacerank.txt} | 13 ++ 21 files changed, 228 insertions(+), 131 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/append/append2.go.in delete mode 100644 gopls/internal/lsp/testdata/assign/internal/secret/secret.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_for.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_for_init.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_if.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_if_eof.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_if_init.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_selector_1.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init.go delete mode 100644 gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go rename gopls/internal/{lsp/testdata/append/append.go => regtest/marker/testdata/completion/append.txt} (81%) rename gopls/internal/{lsp/testdata/assign/assign.go.in => regtest/marker/testdata/completion/assign.txt} (65%) rename gopls/internal/{lsp/testdata/comment_completion/comment_completion.go.in => regtest/marker/testdata/completion/comment.txt} (93%) create mode 100644 gopls/internal/regtest/marker/testdata/completion/danglingstmt.txt rename gopls/internal/{lsp/testdata/interfacerank/interface_rank.go => regtest/marker/testdata/completion/interfacerank.txt} (65%) diff --git a/gopls/internal/lsp/testdata/append/append2.go.in b/gopls/internal/lsp/testdata/append/append2.go.in deleted file mode 100644 index 15bd357b2d6..00000000000 --- a/gopls/internal/lsp/testdata/append/append2.go.in +++ /dev/null @@ -1,5 +0,0 @@ -package append - -func _() { - _ = append(a, struct) //@complete(")") -} \ No newline at end of file diff --git a/gopls/internal/lsp/testdata/assign/internal/secret/secret.go b/gopls/internal/lsp/testdata/assign/internal/secret/secret.go deleted file mode 100644 index 5ee1554dfef..00000000000 --- a/gopls/internal/lsp/testdata/assign/internal/secret/secret.go +++ /dev/null @@ -1,3 +0,0 @@ -package secret - -func Hello() {} \ No newline at end of file diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_for.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for.go deleted file mode 100644 index a16d3bd88fd..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_for.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - for bar //@rank(" //", danglingBar) -} - -func bar() bool { //@item(danglingBar, "bar", "func() bool", "func") - return true -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init.go deleted file mode 100644 index e1130bc23ff..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - for i := bar //@rank(" //", danglingBar2) -} - -func bar2() int { //@item(danglingBar2, "bar2", "func() int", "func") - return 0 -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go deleted file mode 100644 index fb0269f160c..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - for i := bar3(); i > bar //@rank(" //", danglingBar3) -} - -func bar3() int { //@item(danglingBar3, "bar3", "func() int", "func") - return 0 -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go deleted file mode 100644 index 14f78d39288..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_for_init_cond_post.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - for i := bar4(); i > bar4(); i += bar //@rank(" //", danglingBar4) -} - -func bar4() int { //@item(danglingBar4, "bar4", "func() int", "func") - return 0 -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_if.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if.go deleted file mode 100644 index 91f145ada8e..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_if.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - if foo //@rank(" //", danglingFoo) -} - -func foo() bool { //@item(danglingFoo, "foo", "func() bool", "func") - return true -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_if_eof.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_eof.go deleted file mode 100644 index 3454c9fa630..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_if_eof.go +++ /dev/null @@ -1,8 +0,0 @@ -package danglingstmt - -func bar5() bool { //@item(danglingBar5, "bar5", "func() bool", "func") - return true -} - -func _() { - if b //@rank(" //", danglingBar5) diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init.go deleted file mode 100644 index 887c31860a6..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - if i := foo //@rank(" //", danglingFoo2) -} - -func foo2() bool { //@item(danglingFoo2, "foo2", "func() bool", "func") - return true -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go deleted file mode 100644 index 5371283e923..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_if_init_cond.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - if i := 123; foo //@rank(" //", danglingFoo3) -} - -func foo3() bool { //@item(danglingFoo3, "foo3", "func() bool", "func") - return true -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go deleted file mode 100644 index 2213777e148..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_multiline_if.go +++ /dev/null @@ -1,10 +0,0 @@ -package danglingstmt - -func walrus() bool { //@item(danglingWalrus, "walrus", "func() bool", "func") - return true -} - -func _() { - if true && - walrus //@complete(" //", danglingWalrus) -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_1.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_1.go deleted file mode 100644 index 772152f7b4f..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_1.go +++ /dev/null @@ -1,7 +0,0 @@ -package danglingstmt - -func _() { - x. //@rank(" //", danglingI) -} - -var x struct { i int } //@item(danglingI, "i", "int", "field") diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go deleted file mode 100644 index e25b00ce6c3..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_selector_2.go +++ /dev/null @@ -1,10 +0,0 @@ -package danglingstmt - -// TODO: re-enable this test, which was broken when the foo package was removed. -// (we can replicate the relevant definitions in the new marker test) -// import "golang.org/lsptests/foo" - -func _() { - foo. // rank(" //", Foo) - var _ = []string{foo.} // rank("}", Foo) -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init.go deleted file mode 100644 index 15da3ce1046..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - switch i := baz //@rank(" //", danglingBaz) -} - -func baz() int { //@item(danglingBaz, "baz", "func() int", "func") - return 0 -} diff --git a/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go b/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go deleted file mode 100644 index 20b825b2ea6..00000000000 --- a/gopls/internal/lsp/testdata/danglingstmt/dangling_switch_init_tag.go +++ /dev/null @@ -1,9 +0,0 @@ -package danglingstmt - -func _() { - switch i := 0; baz //@rank(" //", danglingBaz2) -} - -func baz2() int { //@item(danglingBaz2, "baz2", "func() int", "func") - return 0 -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index c6776b47340..ed0c15bf89c 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,8 +1,8 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 175 -CompletionSnippetCount = 27 -RankedCompletionsCount = 151 +CompletionsCount = 150 +CompletionSnippetCount = 23 +RankedCompletionsCount = 127 SemanticTokenCount = 3 SuggestedFixCount = 80 MethodExtractionCount = 8 diff --git a/gopls/internal/lsp/testdata/append/append.go b/gopls/internal/regtest/marker/testdata/completion/append.txt similarity index 81% rename from gopls/internal/lsp/testdata/append/append.go rename to gopls/internal/regtest/marker/testdata/completion/append.txt index 2880e59dbf1..b84735bd111 100644 --- a/gopls/internal/lsp/testdata/append/append.go +++ b/gopls/internal/regtest/marker/testdata/completion/append.txt @@ -1,3 +1,14 @@ +This test checks behavior of completion within append expressions. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/append + +go 1.18 + +-- append.go -- package append func foo([]string) {} @@ -15,11 +26,11 @@ func _() { var _ []string = append(oops, a) //@rank(")", appendString, appendInt) foo(append()) //@rank("))", appendStrings, appendInt),rank("))", appendStrings, appendString) - foo(append([]string{}, a)) //@rank("))", appendStrings, appendInt),rank("))", appendString, appendInt),snippet("))", appendStrings, "aStrings...", "aStrings...") + foo(append([]string{}, a)) //@rank("))", appendStrings, appendInt),rank("))", appendString, appendInt),snippet("))", appendStrings, "aStrings...") foo(append([]string{}, "", a)) //@rank("))", appendString, appendInt),rank("))", appendString, appendStrings) // Don't add "..." to append() argument. - bar(append()) //@snippet("))", appendStrings, "aStrings", "aStrings") + bar(append()) //@snippet("))", appendStrings, "aStrings") type baz struct{} baz{} //@item(appendBazLiteral, "baz{}", "", "var") @@ -32,7 +43,14 @@ func _() { b.b = append(b.b, b) //@rank(")", appendBazzy, appendBazLiteral, appendNestedBaz) var aStringsPtr *[]string //@item(appendStringsPtr, "aStringsPtr", "*[]string", "var") - foo(append([]string{}, a)) //@snippet("))", appendStringsPtr, "*aStringsPtr...", "*aStringsPtr...") + foo(append([]string{}, a)) //@snippet("))", appendStringsPtr, "*aStringsPtr...") - foo(append([]string{}, *a)) //@snippet("))", appendStringsPtr, "aStringsPtr...", "aStringsPtr...") + foo(append([]string{}, *a)) //@snippet("))", appendStringsPtr, "aStringsPtr...") +} + +-- append2.go -- +package append + +func _() { + _ = append(a, struct) //@complete(")") } diff --git a/gopls/internal/lsp/testdata/assign/assign.go.in b/gopls/internal/regtest/marker/testdata/completion/assign.txt similarity index 65% rename from gopls/internal/lsp/testdata/assign/assign.go.in rename to gopls/internal/regtest/marker/testdata/completion/assign.txt index 93a622c8326..4f7ea5c72a1 100644 --- a/gopls/internal/lsp/testdata/assign/assign.go.in +++ b/gopls/internal/regtest/marker/testdata/completion/assign.txt @@ -1,3 +1,19 @@ +This test checks that completion considers assignability when ranking results. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/assign + +go 1.18 + +-- settings.json -- +{ + "completeUnimported": false +} + +-- assign.go -- package assign import "golang.org/lsptests/assign/internal/secret" @@ -24,3 +40,8 @@ func _() { fooBar := fooBa //@complete(" //", assignFooBar) } } + +-- internal/secret/secret.go -- +package secret + +func Hello() {} diff --git a/gopls/internal/lsp/testdata/comment_completion/comment_completion.go.in b/gopls/internal/regtest/marker/testdata/completion/comment.txt similarity index 93% rename from gopls/internal/lsp/testdata/comment_completion/comment_completion.go.in rename to gopls/internal/regtest/marker/testdata/completion/comment.txt index dbca0ff1751..68f2c20cdcf 100644 --- a/gopls/internal/lsp/testdata/comment_completion/comment_completion.go.in +++ b/gopls/internal/regtest/marker/testdata/completion/comment.txt @@ -1,3 +1,14 @@ +This test checks behavior of completion within comments. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/comment + +go 1.18 + +-- p.go -- package comment_completion var p bool diff --git a/gopls/internal/regtest/marker/testdata/completion/danglingstmt.txt b/gopls/internal/regtest/marker/testdata/completion/danglingstmt.txt new file mode 100644 index 00000000000..86e79979353 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/danglingstmt.txt @@ -0,0 +1,158 @@ +This test checks that completion works as expected in the presence of +incomplete statements that may affect parser recovery. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/dangling + +go 1.18 + +-- settings.json -- +{ + "completeUnimported": false, + "deepCompletion": false +} + +-- dangling_for.go -- +package danglingstmt + +func _() { + for bar //@rank(" //", danglingBar) +} + +func bar() bool { //@item(danglingBar, "bar", "func() bool", "func") + return true +} + +-- dangling_for_init.go -- +package danglingstmt + +func _() { + for i := bar //@rank(" //", danglingBar2) +} + +func bar2() int { //@item(danglingBar2, "bar2", "func() int", "func") + return 0 +} + +-- dangling_for_init_cond.go -- +package danglingstmt + +func _() { + for i := bar3(); i > bar //@rank(" //", danglingBar3) +} + +func bar3() int { //@item(danglingBar3, "bar3", "func() int", "func") + return 0 +} + +-- dangling_for_init_cond_post.go -- +package danglingstmt + +func _() { + for i := bar4(); i > bar4(); i += bar //@rank(" //", danglingBar4) +} + +func bar4() int { //@item(danglingBar4, "bar4", "func() int", "func") + return 0 +} + +-- dangling_if.go -- +package danglingstmt + +func _() { + if foo //@rank(" //", danglingFoo) +} + +func foo() bool { //@item(danglingFoo, "foo", "func() bool", "func") + return true +} + +-- dangling_if_eof.go -- +package danglingstmt + +func bar5() bool { //@item(danglingBar5, "bar5", "func() bool", "func") + return true +} + +func _() { + if b //@rank(" //", danglingBar5) + +-- dangling_if_init.go -- +package danglingstmt + +func _() { + if i := foo //@rank(" //", danglingFoo2) +} + +func foo2() bool { //@item(danglingFoo2, "foo2", "func() bool", "func") + return true +} + +-- dangling_if_init_cond.go -- +package danglingstmt + +func _() { + if i := 123; foo //@rank(" //", danglingFoo3) +} + +func foo3() bool { //@item(danglingFoo3, "foo3", "func() bool", "func") + return true +} + +-- dangling_multiline_if.go -- +package danglingstmt + +func walrus() bool { //@item(danglingWalrus, "walrus", "func() bool", "func") + return true +} + +func _() { + if true && + walrus //@complete(" //", danglingWalrus) +} + +-- dangling_selector_1.go -- +package danglingstmt + +func _() { + x. //@rank(" //", danglingI) +} + +var x struct { i int } //@item(danglingI, "i", "int", "field") + +-- dangling_selector_2.go -- +package danglingstmt + +// TODO: re-enable this test, which was broken when the foo package was removed. +// (we can replicate the relevant definitions in the new marker test) +// import "golang.org/lsptests/foo" + +func _() { + foo. // rank(" //", Foo) + var _ = []string{foo.} // rank("}", Foo) +} + +-- dangling_switch_init.go -- +package danglingstmt + +func _() { + switch i := baz //@rank(" //", danglingBaz) +} + +func baz() int { //@item(danglingBaz, "baz", "func() int", "func") + return 0 +} + +-- dangling_switch_init_tag.go -- +package danglingstmt + +func _() { + switch i := 0; baz //@rank(" //", danglingBaz2) +} + +func baz2() int { //@item(danglingBaz2, "baz2", "func() int", "func") + return 0 +} diff --git a/gopls/internal/lsp/testdata/interfacerank/interface_rank.go b/gopls/internal/regtest/marker/testdata/completion/interfacerank.txt similarity index 65% rename from gopls/internal/lsp/testdata/interfacerank/interface_rank.go rename to gopls/internal/regtest/marker/testdata/completion/interfacerank.txt index acb5a42e0a6..d1199abebba 100644 --- a/gopls/internal/lsp/testdata/interfacerank/interface_rank.go +++ b/gopls/internal/regtest/marker/testdata/completion/interfacerank.txt @@ -1,3 +1,16 @@ +This test checks that completion ranking accounts for interface assignability. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false, + "deepCompletion": false +} + +-- p.go -- + package interfacerank type foo interface { From c90d0df880d96bcb7826e5d17d0c414ecd8dcf7d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 2 Nov 2023 10:08:11 -0400 Subject: [PATCH 069/100] go/ssa: make isParameterized thread-safe This is another step to breaking up the big Program mutex. Change-Id: Iad0d437d00766c6a132e4401d836a55effc8516d Reviewed-on: https://go-review.googlesource.com/c/tools/+/539255 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- go/ssa/emit.go | 5 ----- go/ssa/methods.go | 19 ++++++++-------- go/ssa/parameterized.go | 48 ++++++++++++++++++++++++++++------------- go/ssa/ssa.go | 9 ++++---- 4 files changed, 47 insertions(+), 34 deletions(-) diff --git a/go/ssa/emit.go b/go/ssa/emit.go index 3c66c40e158..311bf999793 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -207,8 +207,6 @@ func isValuePreserving(ut_src, ut_dst types.Type) bool { // and returns the converted value. Implicit conversions are required // by language assignability rules in assignments, parameter passing, // etc. -// -// Acquires f.Prog.methodsMu < f.Prog.runtimeTypesMu. func emitConv(f *Function, val Value, typ types.Type) Value { t_src := val.Type() @@ -247,13 +245,10 @@ func emitConv(f *Function, val Value, typ types.Type) Value { // Record the types of operands to MakeInterface, if // non-parameterized, as they are the set of runtime types. - // TODO(taking): simplify the locking. t := val.Type() - f.Prog.methodsMu.Lock() // for isParameterized if f.typeparams.Len() == 0 || !f.Prog.parameterized.isParameterized(t) { addRuntimeType(f.Prog, t) } - f.Prog.methodsMu.Unlock() mi := &MakeInterface{X: val} mi.setType(typ) diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 26c26bd9746..5c0c3eaa40c 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -31,25 +31,24 @@ func (prog *Program) MethodValue(sel *types.Selection) *Function { if types.IsInterface(T) { return nil // abstract method (interface, possibly type param) } + + if prog.parameterized.isParameterized(T) { + return nil // abstract method (generic) + } + if prog.mode&LogSource != 0 { defer logStack("MethodValue %s %v", T, sel)() } - var m *Function - b := builder{created: &creator{}} + var cr creator prog.methodsMu.Lock() - // Checks whether a type param is reachable from T. - // This is an expensive check. May need to be optimized later. - if !prog.parameterized.isParameterized(T) { - m = prog.addMethod(prog.createMethodSet(T), sel, b.created) - } + m := prog.addMethod(prog.createMethodSet(T), sel, &cr) prog.methodsMu.Unlock() - if m == nil { - return nil // abstract method (generic) - } + b := builder{created: &cr} b.iterate() + return m } diff --git a/go/ssa/parameterized.go b/go/ssa/parameterized.go index b90ee0e86b5..656417ac8e1 100644 --- a/go/ssa/parameterized.go +++ b/go/ssa/parameterized.go @@ -6,6 +6,7 @@ package ssa import ( "go/types" + "sync" "golang.org/x/tools/internal/typeparams" ) @@ -14,11 +15,24 @@ import ( // // NOTE: Adapted from go/types/infer.go. If that is exported in a future release remove this copy. type tpWalker struct { + mu sync.Mutex seen map[types.Type]bool } -// isParameterized returns true when typ reaches any type parameter. -func (w *tpWalker) isParameterized(typ types.Type) (res bool) { +// isParameterized reports whether t recursively contains a type parameter. +// Thread-safe. +func (w *tpWalker) isParameterized(t types.Type) bool { + // TODO(adonovan): profile. If this operation is expensive, + // handle the most common but shallow cases such as T, pkg.T, + // *T without consulting the cache under the lock. + + w.mu.Lock() + defer w.mu.Unlock() + return w.isParameterizedLocked(t) +} + +// Requires w.mu. +func (w *tpWalker) isParameterizedLocked(typ types.Type) (res bool) { // NOTE: Adapted from go/types/infer.go. Try to keep in sync. // detect cycles @@ -35,25 +49,25 @@ func (w *tpWalker) isParameterized(typ types.Type) (res bool) { break case *types.Array: - return w.isParameterized(t.Elem()) + return w.isParameterizedLocked(t.Elem()) case *types.Slice: - return w.isParameterized(t.Elem()) + return w.isParameterizedLocked(t.Elem()) case *types.Struct: for i, n := 0, t.NumFields(); i < n; i++ { - if w.isParameterized(t.Field(i).Type()) { + if w.isParameterizedLocked(t.Field(i).Type()) { return true } } case *types.Pointer: - return w.isParameterized(t.Elem()) + return w.isParameterizedLocked(t.Elem()) case *types.Tuple: n := t.Len() for i := 0; i < n; i++ { - if w.isParameterized(t.At(i).Type()) { + if w.isParameterizedLocked(t.At(i).Type()) { return true } } @@ -66,11 +80,11 @@ func (w *tpWalker) isParameterized(typ types.Type) (res bool) { // Similarly, the receiver of a method may declare (rather than // use) type parameters, we don't care about those either. // Thus, we only need to look at the input and result parameters. - return w.isParameterized(t.Params()) || w.isParameterized(t.Results()) + return w.isParameterizedLocked(t.Params()) || w.isParameterizedLocked(t.Results()) case *types.Interface: for i, n := 0, t.NumMethods(); i < n; i++ { - if w.isParameterized(t.Method(i).Type()) { + if w.isParameterizedLocked(t.Method(i).Type()) { return true } } @@ -79,16 +93,16 @@ func (w *tpWalker) isParameterized(typ types.Type) (res bool) { panic(err) } for _, term := range terms { - if w.isParameterized(term.Type()) { + if w.isParameterizedLocked(term.Type()) { return true } } case *types.Map: - return w.isParameterized(t.Key()) || w.isParameterized(t.Elem()) + return w.isParameterizedLocked(t.Key()) || w.isParameterizedLocked(t.Elem()) case *types.Chan: - return w.isParameterized(t.Elem()) + return w.isParameterizedLocked(t.Elem()) case *types.Named: args := typeparams.NamedTypeArgs(t) @@ -97,11 +111,11 @@ func (w *tpWalker) isParameterized(typ types.Type) (res bool) { return true } for i, n := 0, args.Len(); i < n; i++ { - if w.isParameterized(args.At(i)) { + if w.isParameterizedLocked(args.At(i)) { return true } } - return w.isParameterized(t.Underlying()) // recurse for types local to parameterized functions + return w.isParameterizedLocked(t.Underlying()) // recurse for types local to parameterized functions case *typeparams.TypeParam: return true @@ -113,9 +127,13 @@ func (w *tpWalker) isParameterized(typ types.Type) (res bool) { return false } +// anyParameterized reports whether any element of ts is parameterized. +// Thread-safe. func (w *tpWalker) anyParameterized(ts []types.Type) bool { + w.mu.Lock() + defer w.mu.Unlock() for _, t := range ts { - if w.isParameterized(t) { + if w.isParameterizedLocked(t) { return true } } diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 4889a129058..38c47965dc8 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -31,10 +31,11 @@ type Program struct { ctxt *typeparams.Context // cache for type checking instantiations // TODO(adonovan): split this mutex. - methodsMu sync.Mutex // guards the following maps: - methodSets typeutil.Map // maps type to its concrete methodSet - instances map[*Function]*instanceSet // instances of generic functions - parameterized tpWalker // memoization of whether a type reaches a type parameter. + methodsMu sync.Mutex // guards the following maps: + methodSets typeutil.Map // maps type to its concrete methodSet + instances map[*Function]*instanceSet // instances of generic functions + + parameterized tpWalker // memoization of whether a type refers to type parameters runtimeTypesMu sync.Mutex runtimeTypes typeutil.Map // set of runtime types (from MakeInterface) From 970eac5ebf7ce0860c355c2fc5246340cbeebe3b Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 1 Nov 2023 09:48:28 -0400 Subject: [PATCH 070/100] gopls/internal/regtest/marker: port builtin/keyword completion tests Port completion tests related to builtins and keywords, which have special handling in both marker test frameworks. Whereas the old tests used magic file names, the new tests use test flags to control the filtering of builtins and keywords. As these flags had a non-zero default value, they highlighted that we weren't parsing the flag set when the "flags" file was absent; fix this. Also fix the keyword filter to actually filter completion items of kind 'keyword', rather than try to guess. This highlighted that certain statement completion was being mislabeled as 'keyword', where 'snippet' is more appropriate. Also port the 'labels' and 'maps' tests. As described in the CL 538797, porting completion tests is a blocker for zero config gopls. For golang/go#57979 For golang/go#54845 Change-Id: Ibd7e451bed33bdd522c6b4280a43b331811693fb Reviewed-on: https://go-review.googlesource.com/c/tools/+/538798 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/regtest/marker.go | 33 ++++++--- .../lsp/source/completion/statements.go | 5 +- .../lsp/testdata/builtins/builtin_types.go | 11 --- .../lsp/testdata/builtins/builtins.go | 13 ---- .../lsp/testdata/builtins/constants.go | 19 ----- .../keywords/accidental_keywords.go.in | 31 -------- .../lsp/testdata/keywords/empty_select.go | 7 -- .../lsp/testdata/keywords/empty_switch.go | 11 --- .../lsp/testdata/rank/convert_rank.go.in | 3 + .../internal/lsp/testdata/summary.txt.golden | 6 +- .../lsp/testdata/typeparams/type_params.go | 6 ++ .../marker/testdata/completion/builtins.txt} | 58 ++++++++++++++- .../marker/testdata/completion/keywords.txt} | 72 ++++++++++++++++++- .../marker/testdata/completion/labels.txt} | 6 ++ .../marker/testdata/completion/maps.txt} | 13 +++- 15 files changed, 182 insertions(+), 112 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/builtins/builtin_types.go delete mode 100644 gopls/internal/lsp/testdata/builtins/builtins.go delete mode 100644 gopls/internal/lsp/testdata/builtins/constants.go delete mode 100644 gopls/internal/lsp/testdata/keywords/accidental_keywords.go.in delete mode 100644 gopls/internal/lsp/testdata/keywords/empty_select.go delete mode 100644 gopls/internal/lsp/testdata/keywords/empty_switch.go rename gopls/internal/{lsp/testdata/builtins/builtin_args.go => regtest/marker/testdata/completion/builtins.txt} (65%) rename gopls/internal/{lsp/testdata/keywords/keywords.go => regtest/marker/testdata/completion/keywords.txt} (68%) rename gopls/internal/{lsp/testdata/labels/labels.go => regtest/marker/testdata/completion/labels.txt} (90%) rename gopls/internal/{lsp/testdata/maps/maps.go.in => regtest/marker/testdata/completion/maps.txt} (74%) diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index e2b1ffb3c5c..a7dee5e15f1 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -114,6 +114,10 @@ var update = flag.Bool("update", false, "if set, update test data during marker // -ignore_extra_diags suppresses errors for unmatched diagnostics // TODO(rfindley): using build constraint expressions for -skip_goos would // be clearer. +// -filter_builtins=false disables the filtering of builtins from +// completion results. +// -filter_keywords=false disables the filtering of keywords from +// completion results. // TODO(rfindley): support flag values containing whitespace. // - "settings.json": this file is parsed as JSON, and used as the // session configuration (see gopls/doc/settings.md) @@ -755,6 +759,8 @@ type markerTest struct { writeGoSum []string // comma separated dirs to write go sum for skipGOOS []string // comma separated GOOS values to skip ignoreExtraDiags bool + filterBuiltins bool + filterKeywords bool } // flagSet returns the flagset used for parsing the special "flags" file in the @@ -766,6 +772,8 @@ func (t *markerTest) flagSet() *flag.FlagSet { flags.Var((*stringListValue)(&t.writeGoSum), "write_sumfile", "if set, write the sumfile for these directories") flags.Var((*stringListValue)(&t.skipGOOS), "skip_goos", "if set, skip this test on these GOOS values") flags.BoolVar(&t.ignoreExtraDiags, "ignore_extra_diags", false, "if set, suppress errors for unmatched diagnostics") + flags.BoolVar(&t.filterBuiltins, "filter_builtins", true, "if set, filter builtins from completion results") + flags.BoolVar(&t.filterKeywords, "filter_keywords", true, "if set, filter keywords from completion results") return flags } @@ -897,9 +905,6 @@ func loadMarkerTest(name string, content []byte) (*markerTest, error) { case file.Name == "flags": test.flags = strings.Fields(string(file.Data)) - if err := test.flagSet().Parse(test.flags); err != nil { - return nil, fmt.Errorf("parsing flags: %v", err) - } case file.Name == "settings.json": if err := json.Unmarshal(file.Data, &test.settings); err != nil { @@ -964,6 +969,12 @@ func loadMarkerTest(name string, content []byte) (*markerTest, error) { } } + // Parse flags after loading files, as they may have been set by the "flags" + // file. + if err := test.flagSet().Parse(test.flags); err != nil { + return nil, fmt.Errorf("parsing flags: %v", err) + } + return test, nil } @@ -1483,7 +1494,7 @@ func snippetMarker(mark marker, src protocol.Location, item completionItem, want got string all []string // for errors ) - items := filterBuiltinsAndKeywords(list.Items) + items := filterBuiltinsAndKeywords(mark, list.Items) for _, i := range items { all = append(all, i.Label) if i.Label == item.Label { @@ -1508,7 +1519,7 @@ func snippetMarker(mark marker, src protocol.Location, item completionItem, want // results match the expected results. func completeMarker(mark marker, src protocol.Location, want ...completionItem) { list := mark.run.env.Completion(src) - items := filterBuiltinsAndKeywords(list.Items) + items := filterBuiltinsAndKeywords(mark, list.Items) var got []completionItem for i, item := range items { simplified := completionItem{ @@ -1551,13 +1562,17 @@ func completeMarker(mark marker, src protocol.Location, want ...completionItem) // results. // // It over-approximates, and does not detect if builtins are shadowed. -func filterBuiltinsAndKeywords(items []protocol.CompletionItem) []protocol.CompletionItem { +func filterBuiltinsAndKeywords(mark marker, items []protocol.CompletionItem) []protocol.CompletionItem { keep := 0 for _, item := range items { - if types.Universe.Lookup(item.Label) == nil && token.Lookup(item.Label) == token.IDENT { - items[keep] = item - keep++ + if mark.run.test.filterKeywords && item.Kind == protocol.KeywordCompletion { + continue + } + if mark.run.test.filterBuiltins && types.Universe.Lookup(item.Label) != nil { + continue } + items[keep] = item + keep++ } return items[:keep] } diff --git a/gopls/internal/lsp/source/completion/statements.go b/gopls/internal/lsp/source/completion/statements.go index 707375fa19d..a801a09570b 100644 --- a/gopls/internal/lsp/source/completion/statements.go +++ b/gopls/internal/lsp/source/completion/statements.go @@ -307,9 +307,8 @@ func (c *completer) addErrCheck() { } c.items = append(c.items, CompletionItem{ - Label: label, - // There doesn't seem to be a more appropriate kind. - Kind: protocol.KeywordCompletion, + Label: label, + Kind: protocol.SnippetCompletion, Score: highScore, snippet: &snip, }) diff --git a/gopls/internal/lsp/testdata/builtins/builtin_types.go b/gopls/internal/lsp/testdata/builtins/builtin_types.go deleted file mode 100644 index 93a4a709500..00000000000 --- a/gopls/internal/lsp/testdata/builtins/builtin_types.go +++ /dev/null @@ -1,11 +0,0 @@ -package builtins - -func _() { - var _ []bool //@item(builtinBoolSliceType, "[]bool", "[]bool", "type") - - var _ []bool = make() //@rank(")", builtinBoolSliceType, int) - - var _ []bool = make([], 0) //@rank(",", bool, int) - - var _ [][]bool = make([][], 0) //@rank(",", bool, int) -} diff --git a/gopls/internal/lsp/testdata/builtins/builtins.go b/gopls/internal/lsp/testdata/builtins/builtins.go deleted file mode 100644 index bd47477d831..00000000000 --- a/gopls/internal/lsp/testdata/builtins/builtins.go +++ /dev/null @@ -1,13 +0,0 @@ -package builtins - -// Definitions of builtin completion items that are still used in tests. - -/* bool */ //@item(bool, "bool", "", "type") -/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") -/* float32 */ //@item(float32, "float32", "", "type") -/* float64 */ //@item(float64, "float64", "", "type") -/* imag(c complex128) float64 */ //@item(imag, "imag", "func(c complex128) float64", "func") -/* int */ //@item(int, "int", "", "type") -/* iota */ //@item(iota, "iota", "", "const") -/* string */ //@item(string, "string", "", "type") -/* true */ //@item(_true, "true", "", "const") diff --git a/gopls/internal/lsp/testdata/builtins/constants.go b/gopls/internal/lsp/testdata/builtins/constants.go deleted file mode 100644 index 7ad07bd1f3a..00000000000 --- a/gopls/internal/lsp/testdata/builtins/constants.go +++ /dev/null @@ -1,19 +0,0 @@ -package builtins - -func _() { - const ( - foo = iota //@complete(" //", iota) - ) - - iota //@complete(" //") - - var iota int //@item(iotaVar, "iota", "int", "var") - - iota //@complete(" //", iotaVar) -} - -func _() { - var twoRedUpEnd bool //@item(TRUEVar, "twoRedUpEnd", "bool", "var") - - var _ bool = true //@rank(" //", _true, TRUEVar) -} diff --git a/gopls/internal/lsp/testdata/keywords/accidental_keywords.go.in b/gopls/internal/lsp/testdata/keywords/accidental_keywords.go.in deleted file mode 100644 index 3833081c4db..00000000000 --- a/gopls/internal/lsp/testdata/keywords/accidental_keywords.go.in +++ /dev/null @@ -1,31 +0,0 @@ -package keywords - -// non-matching candidate - shouldn't show up as completion -var apple = "apple" - -func _() { - foo.bar() // insert some extra statements to exercise our AST surgery - variance := 123 //@item(kwVariance, "variance", "int", "var") - foo.bar() - println(var) //@complete(")", kwVariance) -} - -func _() { - foo.bar() - var s struct { variance int } //@item(kwVarianceField, "variance", "int", "field") - foo.bar() - s.var //@complete(" //", kwVarianceField) -} - -func _() { - channel := 123 //@item(kwChannel, "channel", "int", "var") - chan //@complete(" //", kwChannel) - foo.bar() -} - -func _() { - foo.bar() - var typeName string //@item(kwTypeName, "typeName", "string", "var") - foo.bar() - type //@complete(" //", kwTypeName) -} diff --git a/gopls/internal/lsp/testdata/keywords/empty_select.go b/gopls/internal/lsp/testdata/keywords/empty_select.go deleted file mode 100644 index 17ca3ec9dd7..00000000000 --- a/gopls/internal/lsp/testdata/keywords/empty_select.go +++ /dev/null @@ -1,7 +0,0 @@ -package keywords - -func _() { - select { - c //@complete(" //", case) - } -} diff --git a/gopls/internal/lsp/testdata/keywords/empty_switch.go b/gopls/internal/lsp/testdata/keywords/empty_switch.go deleted file mode 100644 index 2004d55415d..00000000000 --- a/gopls/internal/lsp/testdata/keywords/empty_switch.go +++ /dev/null @@ -1,11 +0,0 @@ -package keywords - -func _() { - switch { - //@complete("", case, default) - } - - switch test.(type) { - d //@complete(" //", default) - } -} diff --git a/gopls/internal/lsp/testdata/rank/convert_rank.go.in b/gopls/internal/lsp/testdata/rank/convert_rank.go.in index c43004833c2..b9bf285c837 100644 --- a/gopls/internal/lsp/testdata/rank/convert_rank.go.in +++ b/gopls/internal/lsp/testdata/rank/convert_rank.go.in @@ -2,6 +2,9 @@ package rank import "time" +// Copied from the old builtins.go, which has been ported to the new marker tests. +/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") + func _() { type strList []string wantsStrList := func(strList) {} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index ed0c15bf89c..8259afc8f57 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,8 +1,8 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 150 -CompletionSnippetCount = 23 -RankedCompletionsCount = 127 +CompletionsCount = 96 +CompletionSnippetCount = 21 +RankedCompletionsCount = 78 SemanticTokenCount = 3 SuggestedFixCount = 80 MethodExtractionCount = 8 diff --git a/gopls/internal/lsp/testdata/typeparams/type_params.go b/gopls/internal/lsp/testdata/typeparams/type_params.go index 21fc7049f5b..7ece33f58e5 100644 --- a/gopls/internal/lsp/testdata/typeparams/type_params.go +++ b/gopls/internal/lsp/testdata/typeparams/type_params.go @@ -3,6 +3,12 @@ package typeparams +// Copied from the old builtins.go, which has been ported to the new marker tests. +/* string */ //@item(string, "string", "", "type") +/* float32 */ //@item(float32, "float32", "", "type") +/* float64 */ //@item(float64, "float64", "", "type") +/* int */ //@item(int, "int", "", "type") + func one[a int | string]() {} func two[a int | string, b float64 | int]() {} diff --git a/gopls/internal/lsp/testdata/builtins/builtin_args.go b/gopls/internal/regtest/marker/testdata/completion/builtins.txt similarity index 65% rename from gopls/internal/lsp/testdata/builtins/builtin_args.go rename to gopls/internal/regtest/marker/testdata/completion/builtins.txt index 052777fe90e..add694bdb81 100644 --- a/gopls/internal/lsp/testdata/builtins/builtin_args.go +++ b/gopls/internal/regtest/marker/testdata/completion/builtins.txt @@ -1,3 +1,10 @@ +This test checks completion of Go builtins. + +-- flags -- +-ignore_extra_diags +-filter_builtins=false + +-- builtin_args.go -- package builtins func _() { @@ -45,7 +52,7 @@ func _() { type myInt int var mi myInt //@item(builtinMyInt, "mi", "myInt", "var") - make(aSliceType, m) //@snippet(")", builtinMyInt, "mi", "mi") + make(aSliceType, m) //@snippet(")", builtinMyInt, "mi") var _ []int = make() //@rank(")", builtinSliceType, builtinMapType) @@ -60,3 +67,52 @@ func _() { <-a //@rank(" //", builtinChan, builtinInt) } + +-- builtin_types.go -- +package builtins + +func _() { + var _ []bool //@item(builtinBoolSliceType, "[]bool", "[]bool", "type") + + var _ []bool = make() //@rank(")", builtinBoolSliceType, int) + + var _ []bool = make([], 0) //@rank(",", bool, int) + + var _ [][]bool = make([][], 0) //@rank(",", bool, int) +} + +-- builtins.go -- +package builtins + +// Definitions of builtin completion items that are still used in tests. + +/* bool */ //@item(bool, "bool", "", "type") +/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") +/* float32 */ //@item(float32, "float32", "", "type") +/* float64 */ //@item(float64, "float64", "", "type") +/* imag(c complex128) float64 */ //@item(imag, "imag", "func(c complex128) float64", "func") +/* int */ //@item(int, "int", "", "type") +/* iota */ //@item(iota, "iota", "", "const") +/* string */ //@item(string, "string", "", "type") +/* true */ //@item(_true, "true", "", "const") + +-- constants.go -- +package builtins + +func _() { + const ( + foo = iota //@complete(" //", iota) + ) + + iota //@complete(" //") + + var iota int //@item(iotaVar, "iota", "int", "var") + + iota //@complete(" //", iotaVar) +} + +func _() { + var twoRedUpEnd bool //@item(TRUEVar, "twoRedUpEnd", "bool", "var") + + var _ bool = true //@rank(" //", _true, TRUEVar) +} diff --git a/gopls/internal/lsp/testdata/keywords/keywords.go b/gopls/internal/regtest/marker/testdata/completion/keywords.txt similarity index 68% rename from gopls/internal/lsp/testdata/keywords/keywords.go rename to gopls/internal/regtest/marker/testdata/completion/keywords.txt index 0bcaa63bffb..3a43f190553 100644 --- a/gopls/internal/lsp/testdata/keywords/keywords.go +++ b/gopls/internal/regtest/marker/testdata/completion/keywords.txt @@ -1,3 +1,17 @@ +This test checks completion of Go keywords. + +-- flags -- +-ignore_extra_diags +-filter_keywords=false + +-- settings.json -- +{ + "completeUnimported": false, + "matcher": "caseInsensitive", + "experimentalPostfixCompletions": false +} + +-- keywords.go -- package keywords //@rank("", type),rank("", func),rank("", var),rank("", const),rank("", import) @@ -89,12 +103,64 @@ func _() { /* fallthrough */ //@item(fallthrough, "fallthrough", "", "keyword") /* continue */ //@item(continue, "continue", "", "keyword") /* return */ //@item(return, "return", "", "keyword") -/* var */ //@item(var, "var", "", "keyword") -/* const */ //@item(const, "const", "", "keyword") /* goto */ //@item(goto, "goto", "", "keyword") /* struct */ //@item(struct, "struct", "", "keyword") /* interface */ //@item(interface, "interface", "", "keyword") /* map */ //@item(map, "map", "", "keyword") -/* func */ //@item(func, "func", "", "keyword") /* chan */ //@item(chan, "chan", "", "keyword") /* range */ //@item(range, "range", "", "keyword") +/* string */ //@item(string, "string", "", "type") +/* int */ //@item(int, "int", "", "type") + +-- accidental_keywords.go -- +package keywords + +// non-matching candidate - shouldn't show up as completion +var apple = "apple" + +func _() { + foo.bar() // insert some extra statements to exercise our AST surgery + variance := 123 //@item(kwVariance, "variance", "int", "var") + foo.bar() + println(var) //@complete(")", kwVariance) +} + +func _() { + foo.bar() + var s struct { variance int } //@item(kwVarianceField, "variance", "int", "field") + foo.bar() + s.var //@complete(" //", kwVarianceField) +} + +func _() { + channel := 123 //@item(kwChannel, "channel", "int", "var") + chan //@complete(" //", kwChannel) + foo.bar() +} + +func _() { + foo.bar() + var typeName string //@item(kwTypeName, "typeName", "string", "var") + foo.bar() + type //@complete(" //", kwTypeName) +} +-- empty_select.go -- +package keywords + +func _() { + select { + c //@complete(" //", case) + } +} +-- empty_switch.go -- +package keywords + +func _() { + switch { + //@complete("", case, default) + } + + switch test.(type) { + d //@complete(" //", default) + } +} diff --git a/gopls/internal/lsp/testdata/labels/labels.go b/gopls/internal/regtest/marker/testdata/completion/labels.txt similarity index 90% rename from gopls/internal/lsp/testdata/labels/labels.go rename to gopls/internal/regtest/marker/testdata/completion/labels.txt index b9effb6d0e0..3caaa5a211a 100644 --- a/gopls/internal/lsp/testdata/labels/labels.go +++ b/gopls/internal/regtest/marker/testdata/completion/labels.txt @@ -1,3 +1,9 @@ +This test checks completion of labels. + +-- flags -- +-ignore_extra_diags + +-- labels.go -- package labels func _() { diff --git a/gopls/internal/lsp/testdata/maps/maps.go.in b/gopls/internal/regtest/marker/testdata/completion/maps.txt similarity index 74% rename from gopls/internal/lsp/testdata/maps/maps.go.in rename to gopls/internal/regtest/marker/testdata/completion/maps.txt index eeb5576b099..052cc26bd38 100644 --- a/gopls/internal/lsp/testdata/maps/maps.go.in +++ b/gopls/internal/regtest/marker/testdata/completion/maps.txt @@ -1,3 +1,14 @@ +This test checks completion of map keys and values. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false +} + +-- maps.go -- package maps func _() { @@ -11,7 +22,7 @@ func _() { // comparable type aStruct struct{} //@item(mapStructType, "aStruct", "struct{...}", "struct") - map[]a{} //@complete("]", mapSliceType, mapStructType),snippet("]", mapSliceType, "*aSlice", "*aSlice") + map[]a{} //@complete("]", mapSliceType, mapStructType),snippet("]", mapSliceType, "*aSlice") map[a]a{} //@complete("]", mapSliceType, mapStructType) map[a]a{} //@complete("{", mapSliceType, mapStructType) From 5e2efda471682c6186a0a6eddb122c3a2d377036 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 1 Nov 2023 10:23:12 -0400 Subject: [PATCH 071/100] gopls/internal/regtest/marker: port rank and func_rank tests Port a couple tests related to ranking. In both cases, some minor modifications to the test data were required: - In rank.txt, a couple literal completions had to be added, because the new marker tests don't disable literal completions (there is no setting to do so; the old marker tests reach into the internals). - In rank.txt a couple ranking assertions that require deep completion are disabled, because other @complete markers break if deep completion is enabled. An artifact of static options. - In func_rank.txt, an additional item is added where fuzzy matching found more results (old markers use the non-default caseinsensitive matching). As described in CL 538797, porting completion tests is a blocker for zero config gopls. For golang/go#57979 For golang/go#54845 Change-Id: I78f9a2b3b07db570b7c86b45d61c01941b9896ac Reviewed-on: https://go-review.googlesource.com/c/tools/+/538799 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- .../lsp/testdata/rank/assign_rank.go.in | 19 -- .../lsp/testdata/rank/binexpr_rank.go.in | 8 - .../lsp/testdata/rank/boolexpr_rank.go | 11 - .../lsp/testdata/rank/convert_rank.go.in | 57 ----- .../lsp/testdata/rank/struct/struct_rank.go | 11 - .../lsp/testdata/rank/switch_rank.go.in | 29 --- .../lsp/testdata/rank/type_assert_rank.go.in | 8 - .../lsp/testdata/rank/type_switch_rank.go.in | 31 --- .../internal/lsp/testdata/summary.txt.golden | 6 +- .../marker/testdata/completion/func_rank.txt} | 15 +- .../marker/testdata/completion/rank.txt | 212 ++++++++++++++++++ 11 files changed, 229 insertions(+), 178 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/rank/assign_rank.go.in delete mode 100644 gopls/internal/lsp/testdata/rank/binexpr_rank.go.in delete mode 100644 gopls/internal/lsp/testdata/rank/boolexpr_rank.go delete mode 100644 gopls/internal/lsp/testdata/rank/convert_rank.go.in delete mode 100644 gopls/internal/lsp/testdata/rank/struct/struct_rank.go delete mode 100644 gopls/internal/lsp/testdata/rank/switch_rank.go.in delete mode 100644 gopls/internal/lsp/testdata/rank/type_assert_rank.go.in delete mode 100644 gopls/internal/lsp/testdata/rank/type_switch_rank.go.in rename gopls/internal/{lsp/testdata/func_rank/func_rank.go.in => regtest/marker/testdata/completion/func_rank.txt} (89%) create mode 100644 gopls/internal/regtest/marker/testdata/completion/rank.txt diff --git a/gopls/internal/lsp/testdata/rank/assign_rank.go.in b/gopls/internal/lsp/testdata/rank/assign_rank.go.in deleted file mode 100644 index 5c51910d4c3..00000000000 --- a/gopls/internal/lsp/testdata/rank/assign_rank.go.in +++ /dev/null @@ -1,19 +0,0 @@ -package rank - -var ( - apple int = 3 //@item(apple, "apple", "int", "var") - pear string = "hello" //@item(pear, "pear", "string", "var") -) - -func _() { - orange := 1 //@item(orange, "orange", "int", "var") - grape := "hello" //@item(grape, "grape", "string", "var") - orange, grape = 2, "hello" //@complete(" \"", grape, pear, orange, apple) -} - -func _() { - var pineapple int //@item(pineapple, "pineapple", "int", "var") - pineapple = 1 //@complete(" 1", pineapple, apple, pear) - - y := //@complete(" /", pineapple, apple, pear) -} diff --git a/gopls/internal/lsp/testdata/rank/binexpr_rank.go.in b/gopls/internal/lsp/testdata/rank/binexpr_rank.go.in deleted file mode 100644 index 60b2cc1bc44..00000000000 --- a/gopls/internal/lsp/testdata/rank/binexpr_rank.go.in +++ /dev/null @@ -1,8 +0,0 @@ -package rank - -func _() { - _ = 5 + ; //@complete(" ;", apple, pear) - y := + 5; //@complete(" +", apple, pear) - - if 6 == {} //@complete(" {", apple, pear) -} diff --git a/gopls/internal/lsp/testdata/rank/boolexpr_rank.go b/gopls/internal/lsp/testdata/rank/boolexpr_rank.go deleted file mode 100644 index fe512eee161..00000000000 --- a/gopls/internal/lsp/testdata/rank/boolexpr_rank.go +++ /dev/null @@ -1,11 +0,0 @@ -package rank - -func _() { - someRandomBoolFunc := func() bool { //@item(boolExprFunc, "someRandomBoolFunc", "func() bool", "var") - return true - } - - var foo, bar int //@item(boolExprBar, "bar", "int", "var") - if foo == 123 && b { //@rank(" {", boolExprBar, boolExprFunc) - } -} diff --git a/gopls/internal/lsp/testdata/rank/convert_rank.go.in b/gopls/internal/lsp/testdata/rank/convert_rank.go.in deleted file mode 100644 index b9bf285c837..00000000000 --- a/gopls/internal/lsp/testdata/rank/convert_rank.go.in +++ /dev/null @@ -1,57 +0,0 @@ -package rank - -import "time" - -// Copied from the old builtins.go, which has been ported to the new marker tests. -/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") - -func _() { - type strList []string - wantsStrList := func(strList) {} - - var ( - convA string //@item(convertA, "convA", "string", "var") - convB []string //@item(convertB, "convB", "[]string", "var") - ) - wantsStrList(strList(conv)) //@complete("))", convertB, convertA) -} - -func _() { - type myInt int - - const ( - convC = "hi" //@item(convertC, "convC", "string", "const") - convD = 123 //@item(convertD, "convD", "int", "const") - convE int = 123 //@item(convertE, "convE", "int", "const") - convF string = "there" //@item(convertF, "convF", "string", "const") - convG myInt = 123 //@item(convertG, "convG", "myInt", "const") - ) - - var foo int - foo = conv //@rank(" //", convertE, convertD) - - var mi myInt - mi = conv //@rank(" //", convertG, convertD, convertE) - mi + conv //@rank(" //", convertG, convertD, convertE) - - 1 + conv //@rank(" //", convertD, convertC),rank(" //", convertE, convertC),rank(" //", convertG, convertC) - - type myString string - var ms myString - ms = conv //@rank(" //", convertC, convertF) - - type myUint uint32 - var mu myUint - mu = conv //@rank(" //", convertD, convertE) - - // don't downrank constants when assigning to interface{} - var _ interface{} = c //@rank(" //", convertD, complex) - - var _ time.Duration = conv //@rank(" //", convertD, convertE),snippet(" //", convertE, "time.Duration(convE)", "time.Duration(convE)") - - var convP myInt //@item(convertP, "convP", "myInt", "var") - var _ *int = conv //@snippet(" //", convertP, "(*int)(&convP)", "(*int)(&convP)") - - var ff float64 //@item(convertFloat, "ff", "float64", "var") - f == convD //@snippet(" =", convertFloat, "ff", "ff") -} diff --git a/gopls/internal/lsp/testdata/rank/struct/struct_rank.go b/gopls/internal/lsp/testdata/rank/struct/struct_rank.go deleted file mode 100644 index e0bdd38a87d..00000000000 --- a/gopls/internal/lsp/testdata/rank/struct/struct_rank.go +++ /dev/null @@ -1,11 +0,0 @@ -package struct_rank - -type foo struct { - c int //@item(c_rank, "c", "int", "field") - b int //@item(b_rank, "b", "int", "field") - a int //@item(a_rank, "a", "int", "field") -} - -func f() { - foo := foo{} //@rank("}", c_rank, b_rank, a_rank) -} diff --git a/gopls/internal/lsp/testdata/rank/switch_rank.go.in b/gopls/internal/lsp/testdata/rank/switch_rank.go.in deleted file mode 100644 index b828528da80..00000000000 --- a/gopls/internal/lsp/testdata/rank/switch_rank.go.in +++ /dev/null @@ -1,29 +0,0 @@ -package rank - -import "time" - -func _() { - switch pear { - case _: //@rank("_", pear, apple) - } - - time.Monday //@item(timeMonday, "time.Monday", "time.Weekday", "const"),item(monday ,"Monday", "time.Weekday", "const") - time.Friday //@item(timeFriday, "time.Friday", "time.Weekday", "const"),item(friday ,"Friday", "time.Weekday", "const") - - now := time.Now() - now.Weekday //@item(nowWeekday, "now.Weekday", "func() time.Weekday", "method") - - then := time.Now() - then.Weekday //@item(thenWeekday, "then.Weekday", "func() time.Weekday", "method") - - switch time.Weekday(0) { - case time.Monday, time.Tuesday: - case time.Wednesday, time.Thursday: - case time.Saturday, time.Sunday: - case t: //@rank(":", timeFriday, timeMonday) - case time.: //@rank(":", friday, monday) - - case now.Weekday(): - case week: //@rank(":", thenWeekday, nowWeekday) - } -} diff --git a/gopls/internal/lsp/testdata/rank/type_assert_rank.go.in b/gopls/internal/lsp/testdata/rank/type_assert_rank.go.in deleted file mode 100644 index 416541cddee..00000000000 --- a/gopls/internal/lsp/testdata/rank/type_assert_rank.go.in +++ /dev/null @@ -1,8 +0,0 @@ -package rank - -func _() { - type flower int //@item(flower, "flower", "int", "type") - var fig string //@item(fig, "fig", "string", "var") - - _ = interface{}(nil).(f) //@complete(") //", flower) -} diff --git a/gopls/internal/lsp/testdata/rank/type_switch_rank.go.in b/gopls/internal/lsp/testdata/rank/type_switch_rank.go.in deleted file mode 100644 index 1ed12b7c1c7..00000000000 --- a/gopls/internal/lsp/testdata/rank/type_switch_rank.go.in +++ /dev/null @@ -1,31 +0,0 @@ -package rank - -import ( - "fmt" - "go/ast" -) - -func _() { - type basket int //@item(basket, "basket", "int", "type") - var banana string //@item(banana, "banana", "string", "var") - - switch interface{}(pear).(type) { - case b: //@complete(":", basket) - b //@complete(" //", banana, basket) - } - - Ident //@item(astIdent, "Ident", "struct{...}", "struct") - IfStmt //@item(astIfStmt, "IfStmt", "struct{...}", "struct") - - switch ast.Node(nil).(type) { - case *ast.Ident: - case *ast.I: //@rank(":", astIfStmt, astIdent) - } - - Stringer //@item(fmtStringer, "Stringer", "interface{...}", "interface") - GoStringer //@item(fmtGoStringer, "GoStringer", "interface{...}", "interface") - - switch interface{}(nil).(type) { - case fmt.Stringer: //@rank(":", fmtStringer, fmtGoStringer) - } -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 8259afc8f57..18f0e8f574a 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,8 +1,8 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 96 -CompletionSnippetCount = 21 -RankedCompletionsCount = 78 +CompletionsCount = 72 +CompletionSnippetCount = 18 +RankedCompletionsCount = 56 SemanticTokenCount = 3 SuggestedFixCount = 80 MethodExtractionCount = 8 diff --git a/gopls/internal/lsp/testdata/func_rank/func_rank.go.in b/gopls/internal/regtest/marker/testdata/completion/func_rank.txt similarity index 89% rename from gopls/internal/lsp/testdata/func_rank/func_rank.go.in rename to gopls/internal/regtest/marker/testdata/completion/func_rank.txt index 905010b3d47..157361fb62f 100644 --- a/gopls/internal/lsp/testdata/func_rank/func_rank.go.in +++ b/gopls/internal/regtest/marker/testdata/completion/func_rank.txt @@ -1,3 +1,16 @@ +This test checks various ranking of completion results within function call +context. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false, + "deepCompletion": false +} + +-- func_rank.go -- package func_rank import "net/http" @@ -38,7 +51,7 @@ func _() { var aaPtr *string //@item(rankAAPtr, "aaPtr", "*string", "var") var abPtr *int //@item(rankABPtr, "abPtr", "*int", "var") - fnInt(*a) //@complete(")", rankABPtr, rankAAPtr) + fnInt(*a) //@complete(")", rankABPtr, rankAAPtr, stringAVar) _ = func() string { return s.A //@complete(" //", rankAB, rankAA, rankAC) diff --git a/gopls/internal/regtest/marker/testdata/completion/rank.txt b/gopls/internal/regtest/marker/testdata/completion/rank.txt new file mode 100644 index 00000000000..03ea565a400 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/rank.txt @@ -0,0 +1,212 @@ +This test checks various ranking of completion results. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false, + "deepCompletion": false +} + +-- go.mod -- +module golang.org/lsptests/rank + +go 1.18 + +-- struct/struct_rank.go -- +package struct_rank + +type foo struct { + c int //@item(c_rank, "c", "int", "field") + b int //@item(b_rank, "b", "int", "field") + a int //@item(a_rank, "a", "int", "field") +} + +func f() { + foo := foo{} //@rank("}", c_rank, b_rank, a_rank) +} + +-- assign_rank.go -- +package rank + +// Literal completion results. +/* int() */ //@item(int, "int()", "int", "var") +/* string() */ //@item(string, "string()", "string", "var") + +var ( + apple int = 3 //@item(apple, "apple", "int", "var") + pear string = "hello" //@item(pear, "pear", "string", "var") +) + +func _() { + orange := 1 //@item(orange, "orange", "int", "var") + grape := "hello" //@item(grape, "grape", "string", "var") + orange, grape = 2, "hello" //@complete(" \"", grape, pear, string, orange, apple) +} + +func _() { + var pineapple int //@item(pineapple, "pineapple", "int", "var") + pineapple = 1 //@complete(" 1", pineapple, apple, int, pear) + + y := //@complete(" /", pineapple, apple, pear) +} + +-- binexpr_rank.go -- +package rank + +func _() { + _ = 5 + ; //@complete(" ;", apple, pear) + y := + 5; //@complete(" +", apple, pear) + + if 6 == {} //@complete(" {", apple, pear) +} + +-- boolexpr_rank.go -- +package rank + +func _() { + someRandomBoolFunc := func() bool { //@item(boolExprFunc, "someRandomBoolFunc", "func() bool", "var") + return true + } + + var foo, bar int //@item(boolExprBar, "bar", "int", "var") + if foo == 123 && b { //@rank(" {", boolExprBar, boolExprFunc) + } +} + +-- convert_rank.go -- +package rank + +import "time" + +// Copied from the old builtins.go, which has been ported to the new marker tests. +/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") + +func _() { + type strList []string + wantsStrList := func(strList) {} + + var ( + convA string //@item(convertA, "convA", "string", "var") + convB []string //@item(convertB, "convB", "[]string", "var") + ) + wantsStrList(strList(conv)) //@complete("))", convertB, convertA) +} + +func _() { + type myInt int + + const ( + convC = "hi" //@item(convertC, "convC", "string", "const") + convD = 123 //@item(convertD, "convD", "int", "const") + convE int = 123 //@item(convertE, "convE", "int", "const") + convF string = "there" //@item(convertF, "convF", "string", "const") + convG myInt = 123 //@item(convertG, "convG", "myInt", "const") + ) + + var foo int + foo = conv //@rank(" //", convertE, convertD) + + var mi myInt + mi = conv //@rank(" //", convertG, convertD, convertE) + mi + conv //@rank(" //", convertG, convertD, convertE) + + 1 + conv //@rank(" //", convertD, convertC),rank(" //", convertE, convertC),rank(" //", convertG, convertC) + + type myString string + var ms myString + ms = conv //@rank(" //", convertC, convertF) + + type myUint uint32 + var mu myUint + mu = conv //@rank(" //", convertD, convertE) + + // don't downrank constants when assigning to interface{} + var _ interface{} = c //@rank(" //", convertD, complex) + + var _ time.Duration = conv //@rank(" //", convertD, convertE),snippet(" //", convertE, "time.Duration(convE)") + + var convP myInt //@item(convertP, "convP", "myInt", "var") + var _ *int = conv //@snippet(" //", convertP, "(*int)(&convP)") + + var ff float64 //@item(convertFloat, "ff", "float64", "var") + f == convD //@snippet(" =", convertFloat, "ff") +} + +-- switch_rank.go -- +package rank + +import "time" + +func _() { + switch pear { + case _: //@rank("_", pear, apple) + } + + time.Monday //@item(timeMonday, "time.Monday", "time.Weekday", "const"),item(monday ,"Monday", "time.Weekday", "const") + time.Friday //@item(timeFriday, "time.Friday", "time.Weekday", "const"),item(friday ,"Friday", "time.Weekday", "const") + + now := time.Now() + now.Weekday //@item(nowWeekday, "now.Weekday", "func() time.Weekday", "method") + + then := time.Now() + then.Weekday //@item(thenWeekday, "then.Weekday", "func() time.Weekday", "method") + + switch time.Weekday(0) { + case time.Monday, time.Tuesday: + case time.Wednesday, time.Thursday: + case time.Saturday, time.Sunday: + // TODO: these tests were disabled because they require deep completion + // (which would break other tests) + case t: // rank(":", timeFriday, timeMonday) + case time.: //@rank(":", friday, monday) + + case now.Weekday(): + case week: // rank(":", thenWeekday, nowWeekday) + } +} + +-- type_assert_rank.go -- +package rank + +func _() { + type flower int //@item(flower, "flower", "int", "type") + var fig string //@item(fig, "fig", "string", "var") + + _ = interface{}(nil).(f) //@complete(") //", flower) +} + +-- type_switch_rank.go -- +package rank + +import ( + "fmt" + "go/ast" +) + +func _() { + type basket int //@item(basket, "basket", "int", "type") + var banana string //@item(banana, "banana", "string", "var") + + switch interface{}(pear).(type) { + case b: //@complete(":", basket) + b //@complete(" //", banana, basket) + } + + Ident //@item(astIdent, "Ident", "struct{...}", "struct") + IfStmt //@item(astIfStmt, "IfStmt", "struct{...}", "struct") + + switch ast.Node(nil).(type) { + case *ast.Ident: + case *ast.I: //@rank(":", astIfStmt, astIdent) + } + + Stringer //@item(fmtStringer, "Stringer", "interface{...}", "interface") + GoStringer //@item(fmtGoStringer, "GoStringer", "interface{...}", "interface") + + switch interface{}(nil).(type) { + case fmt.Stringer: //@rank(":", fmtStringer, fmtGoStringer) + } +} + From 26349afb9dc73378822c3a145cd0662465e40ded Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 1 Nov 2023 10:36:18 -0400 Subject: [PATCH 072/100] gopls/internal/regtest/marker: port remaining completion tests The following changes were made to test data: - anon.txt, complit.txt: added additional literal completion results. - errors.txt: copied in part of the 'types' package. As described in CL 538797, porting completion tests is a blocker for zero config gopls. For golang/go#57979 For golang/go#54845 Change-Id: I02a0c15a14ea36bea4ecc55664f95a2d635aaa53 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538800 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/completion_test.go | 17 --------- gopls/internal/lsp/testdata/anon/anon.go.in | 23 ------------ gopls/internal/lsp/testdata/errors/errors.go | 10 ----- .../lsp/testdata/issues/issue56505.go | 8 ---- .../internal/lsp/testdata/summary.txt.golden | 5 +-- .../lsp/testdata/unresolved/unresolved.go.in | 6 --- .../lsp/testdata/variadic/variadic_intf.go | 21 ----------- gopls/internal/lsp/tests/tests.go | 17 +-------- .../marker/testdata/completion/anon.txt | 37 +++++++++++++++++++ .../marker/testdata/completion/basic_lit.txt} | 6 +++ .../marker/testdata/completion/cast.txt} | 8 +++- .../marker/testdata/completion/channel.txt} | 11 ++++++ .../marker/testdata/completion/complit.txt} | 32 +++++++++++----- .../marker/testdata/completion/constant.txt} | 6 +++ .../marker/testdata/completion/errors.txt | 33 +++++++++++++++++ .../testdata/completion/field_list.txt} | 11 ++++++ .../marker/testdata/completion/func_sig.txt} | 6 +++ .../testdata/completion/func_value.txt} | 6 +++ .../marker/testdata/completion/index.txt} | 13 ++++++- .../marker/testdata/completion/issue56505.txt | 13 +++++++ .../testdata/completion/type_assert.txt} | 6 +++ .../marker/testdata/completion/unresolved.txt | 16 ++++++++ .../marker/testdata/completion/unsafe.txt} | 37 ++++++++++++------- .../marker/testdata/completion/variadic.txt} | 35 ++++++++++++++++-- 24 files changed, 253 insertions(+), 130 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/anon/anon.go.in delete mode 100644 gopls/internal/lsp/testdata/errors/errors.go delete mode 100644 gopls/internal/lsp/testdata/issues/issue56505.go delete mode 100644 gopls/internal/lsp/testdata/unresolved/unresolved.go.in delete mode 100644 gopls/internal/lsp/testdata/variadic/variadic_intf.go create mode 100644 gopls/internal/regtest/marker/testdata/completion/anon.txt rename gopls/internal/{lsp/testdata/basiclit/basiclit.go => regtest/marker/testdata/completion/basic_lit.txt} (64%) rename gopls/internal/{lsp/testdata/cast/cast.go.in => regtest/marker/testdata/completion/cast.txt} (70%) rename gopls/internal/{lsp/testdata/channel/channel.go => regtest/marker/testdata/completion/channel.txt} (79%) rename gopls/internal/{lsp/testdata/complit/complit.go.in => regtest/marker/testdata/completion/complit.txt} (69%) rename gopls/internal/{lsp/testdata/constant/constant.go => regtest/marker/testdata/completion/constant.txt} (78%) create mode 100644 gopls/internal/regtest/marker/testdata/completion/errors.txt rename gopls/internal/{lsp/testdata/fieldlist/field_list.go => regtest/marker/testdata/completion/field_list.txt} (81%) rename gopls/internal/{lsp/testdata/funcsig/func_sig.go => regtest/marker/testdata/completion/func_sig.txt} (69%) rename gopls/internal/{lsp/testdata/funcvalue/func_value.go => regtest/marker/testdata/completion/func_value.txt} (86%) rename gopls/internal/{lsp/testdata/index/index.go => regtest/marker/testdata/completion/index.txt} (72%) create mode 100644 gopls/internal/regtest/marker/testdata/completion/issue56505.txt rename gopls/internal/{lsp/testdata/typeassert/type_assert.go => regtest/marker/testdata/completion/type_assert.txt} (86%) create mode 100644 gopls/internal/regtest/marker/testdata/completion/unresolved.txt rename gopls/internal/{lsp/testdata/unsafe/unsafe.go => regtest/marker/testdata/completion/unsafe.txt} (60%) rename gopls/internal/{lsp/testdata/variadic/variadic.go.in => regtest/marker/testdata/completion/variadic.txt} (54%) diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go index 0e91c55411f..6e610452e96 100644 --- a/gopls/internal/lsp/completion_test.go +++ b/gopls/internal/lsp/completion_test.go @@ -6,7 +6,6 @@ package lsp import ( "fmt" - "strings" "testing" "golang.org/x/tools/gopls/internal/lsp/protocol" @@ -16,22 +15,6 @@ import ( "golang.org/x/tools/gopls/internal/span" ) -func (r *runner) Completion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - got := r.callCompletion(t, src, func(opts *source.Options) { - opts.DeepCompletion = false - opts.Matcher = source.CaseInsensitive - opts.CompleteUnimported = false - opts.InsertTextFormat = protocol.SnippetTextFormat - opts.LiteralCompletions = strings.Contains(string(src.URI()), "literal") - opts.ExperimentalPostfixCompletions = strings.Contains(string(src.URI()), "postfix") - }) - got = tests.FilterBuiltins(src, got) - want := expected(t, test, items) - if diff := tests.DiffCompletionItems(want, got); diff != "" { - t.Errorf("mismatching completion items (-want +got):\n%s", diff) - } -} - func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) { list := r.callCompletion(t, src, func(opts *source.Options) { opts.UsePlaceholders = placeholders diff --git a/gopls/internal/lsp/testdata/anon/anon.go.in b/gopls/internal/lsp/testdata/anon/anon.go.in deleted file mode 100644 index 36611b2680a..00000000000 --- a/gopls/internal/lsp/testdata/anon/anon.go.in +++ /dev/null @@ -1,23 +0,0 @@ -package anon - -func _() { - for _, _ := range []struct { - i, j int //@item(anonI, "i", "int", "field"),item(anonJ, "j", "int", "field") - }{ - { - i: 1, - //@complete("", anonJ) - }, - { - //@complete("", anonI, anonJ) - }, - } { - continue - } - - s := struct{ f int }{ } //@item(anonF, "f", "int", "field"),item(structS, "s", "struct{...}", "var"),complete(" }", anonF) - - _ = map[struct{ x int }]int{ //@item(anonX, "x", "int", "field") - struct{ x int }{ }: 1, //@complete(" }", anonX, structS) - } -} diff --git a/gopls/internal/lsp/testdata/errors/errors.go b/gopls/internal/lsp/testdata/errors/errors.go deleted file mode 100644 index e14cde69e9e..00000000000 --- a/gopls/internal/lsp/testdata/errors/errors.go +++ /dev/null @@ -1,10 +0,0 @@ -package errors - -import ( - "golang.org/lsptests/types" -) - -func _() { - bob.Bob() //@complete(".") - types.b //@complete(" //", Bob_interface) -} diff --git a/gopls/internal/lsp/testdata/issues/issue56505.go b/gopls/internal/lsp/testdata/issues/issue56505.go deleted file mode 100644 index 8c641bfb852..00000000000 --- a/gopls/internal/lsp/testdata/issues/issue56505.go +++ /dev/null @@ -1,8 +0,0 @@ -package issues - -// Test for golang/go#56505: completion on variables of type *error should not -// panic. -func _() { - var e *error - e.x //@complete(" //") -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 18f0e8f574a..3122dd679da 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,8 +1,7 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 72 -CompletionSnippetCount = 18 -RankedCompletionsCount = 56 +CompletionSnippetCount = 9 +RankedCompletionsCount = 45 SemanticTokenCount = 3 SuggestedFixCount = 80 MethodExtractionCount = 8 diff --git a/gopls/internal/lsp/testdata/unresolved/unresolved.go.in b/gopls/internal/lsp/testdata/unresolved/unresolved.go.in deleted file mode 100644 index e1daecc2e51..00000000000 --- a/gopls/internal/lsp/testdata/unresolved/unresolved.go.in +++ /dev/null @@ -1,6 +0,0 @@ -package unresolved - -func foo(interface{}) { - // don't crash on fake "resolved" type - foo(func(i, j f //@complete(" //") -} diff --git a/gopls/internal/lsp/testdata/variadic/variadic_intf.go b/gopls/internal/lsp/testdata/variadic/variadic_intf.go deleted file mode 100644 index 6e23fc99607..00000000000 --- a/gopls/internal/lsp/testdata/variadic/variadic_intf.go +++ /dev/null @@ -1,21 +0,0 @@ -package variadic - -type baz interface { - baz() -} - -func wantsBaz(...baz) {} - -type bazImpl int - -func (bazImpl) baz() {} - -func _() { - var ( - impls []bazImpl //@item(vImplSlice, "impls", "[]bazImpl", "var") - impl bazImpl //@item(vImpl, "impl", "bazImpl", "var") - bazes []baz //@item(vIntfSlice, "bazes", "[]baz", "var") - ) - - wantsBaz() //@rank(")", vImpl, vImplSlice),rank(")", vIntfSlice, vImplSlice) -} diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 631d873ac01..741688e530e 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -53,7 +53,6 @@ var UpdateGolden = flag.Bool("golden", false, "Update golden files") // type in the field name and the make() expression. type CallHierarchy = map[span.Span]*CallHierarchyResult type CompletionItems = map[token.Pos]*completion.CompletionItem -type Completions = map[span.Span][]Completion type CompletionSnippets = map[span.Span][]CompletionSnippet type RankCompletions = map[span.Span][]Completion type SemanticTokens = []span.Span @@ -69,7 +68,6 @@ type Data struct { Exported *packagestest.Exported CallHierarchy CallHierarchy CompletionItems CompletionItems - Completions Completions CompletionSnippets CompletionSnippets RankCompletions RankCompletions SemanticTokens SemanticTokens @@ -99,7 +97,6 @@ type Data struct { // we can abolish the interface now. type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) - Completion(*testing.T, span.Span, Completion, CompletionItems) CompletionSnippet(*testing.T, span.Span, CompletionSnippet, bool, CompletionItems) RankCompletion(*testing.T, span.Span, Completion, CompletionItems) SemanticTokens(*testing.T, span.Span) @@ -115,7 +112,7 @@ type CompletionTestType int const ( // Default runs the standard completion tests. - CompletionDefault = CompletionTestType(iota) + _ = CompletionTestType(iota) // CompletionRank candidates in test must be valid and in the right relative order. CompletionRank @@ -206,7 +203,6 @@ func load(t testing.TB, mode string, dir string) *Data { datum := &Data{ CallHierarchy: make(CallHierarchy), CompletionItems: make(CompletionItems), - Completions: make(Completions), CompletionSnippets: make(CompletionSnippets), RankCompletions: make(RankCompletions), Renames: make(Renames), @@ -346,7 +342,6 @@ func load(t testing.TB, mode string, dir string) *Data { // Collect any data that needs to be used by subsequent tests. if err := datum.Exported.Expect(map[string]interface{}{ "item": datum.collectCompletionItems, - "complete": datum.collectCompletions(CompletionDefault), "rank": datum.collectCompletions(CompletionRank), "snippet": datum.collectCompletionSnippets, "semantic": datum.collectSemanticTokens, @@ -439,11 +434,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("Completion", func(t *testing.T) { - t.Helper() - eachCompletion(t, data.Completions, tests.Completion) - }) - t.Run("CompletionSnippets", func(t *testing.T) { t.Helper() for _, placeholders := range []bool{true, false} { @@ -575,7 +565,6 @@ func checkData(t *testing.T, data *Data) { } fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy)) - fmt.Fprintf(buf, "CompletionsCount = %v\n", countCompletions(data.Completions)) fmt.Fprintf(buf, "CompletionSnippetCount = %v\n", snippetCount) fmt.Fprintf(buf, "RankedCompletionsCount = %v\n", countCompletions(data.RankCompletions)) fmt.Fprintf(buf, "SemanticTokenCount = %v\n", len(data.SemanticTokens)) @@ -676,9 +665,7 @@ func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []t result(data.RankCompletions, src, expected) } default: - return func(src span.Span, expected []token.Pos) { - result(data.Completions, src, expected) - } + panic("unsupported") } } diff --git a/gopls/internal/regtest/marker/testdata/completion/anon.txt b/gopls/internal/regtest/marker/testdata/completion/anon.txt new file mode 100644 index 00000000000..37d8cf73b65 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/anon.txt @@ -0,0 +1,37 @@ +This test checks completion related to anonymous structs. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "deepCompletion": false +} + +-- anon.go -- +package anon + +// Literal completion results. +/* int() */ //@item(int, "int()", "int", "var") + +func _() { + for _, _ := range []struct { + i, j int //@item(anonI, "i", "int", "field"),item(anonJ, "j", "int", "field") + }{ + { + i: 1, + //@complete("", anonJ) + }, + { + //@complete("", anonI, anonJ, int) + }, + } { + continue + } + + s := struct{ f int }{ } //@item(anonF, "f", "int", "field"),item(structS, "s", "struct{...}", "var"),complete(" }", anonF, int) + + _ = map[struct{ x int }]int{ //@item(anonX, "x", "int", "field") + struct{ x int }{ }: 1, //@complete(" }", anonX, int, structS) + } +} diff --git a/gopls/internal/lsp/testdata/basiclit/basiclit.go b/gopls/internal/regtest/marker/testdata/completion/basic_lit.txt similarity index 64% rename from gopls/internal/lsp/testdata/basiclit/basiclit.go rename to gopls/internal/regtest/marker/testdata/completion/basic_lit.txt index ab895dc011c..aa06326d39b 100644 --- a/gopls/internal/lsp/testdata/basiclit/basiclit.go +++ b/gopls/internal/regtest/marker/testdata/completion/basic_lit.txt @@ -1,3 +1,9 @@ +This test checks completion related to basic literals. + +-- flags -- +-ignore_extra_diags + +-- basiclit.go -- package basiclit func _() { diff --git a/gopls/internal/lsp/testdata/cast/cast.go.in b/gopls/internal/regtest/marker/testdata/completion/cast.txt similarity index 70% rename from gopls/internal/lsp/testdata/cast/cast.go.in rename to gopls/internal/regtest/marker/testdata/completion/cast.txt index 7fe21903c0c..6c52d5063b5 100644 --- a/gopls/internal/lsp/testdata/cast/cast.go.in +++ b/gopls/internal/regtest/marker/testdata/completion/cast.txt @@ -1,3 +1,9 @@ +This test checks completion related to casts. + +-- flags -- +-ignore_extra_diags + +-- cast.go -- package cast func _() { @@ -8,4 +14,4 @@ func _() { func _() { foo := struct{x int}{x: 1} _ = float64(foo. //@complete(" /", x_field) -} \ No newline at end of file +} diff --git a/gopls/internal/lsp/testdata/channel/channel.go b/gopls/internal/regtest/marker/testdata/completion/channel.txt similarity index 79% rename from gopls/internal/lsp/testdata/channel/channel.go rename to gopls/internal/regtest/marker/testdata/completion/channel.txt index d6bd311e332..e07ae8e9be9 100644 --- a/gopls/internal/lsp/testdata/channel/channel.go +++ b/gopls/internal/regtest/marker/testdata/completion/channel.txt @@ -1,3 +1,14 @@ +This test checks completion related to channels. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false +} + +-- channel.go -- package channel func _() { diff --git a/gopls/internal/lsp/testdata/complit/complit.go.in b/gopls/internal/regtest/marker/testdata/completion/complit.txt similarity index 69% rename from gopls/internal/lsp/testdata/complit/complit.go.in rename to gopls/internal/regtest/marker/testdata/completion/complit.txt index e819810d8cd..59384893d79 100644 --- a/gopls/internal/lsp/testdata/complit/complit.go.in +++ b/gopls/internal/regtest/marker/testdata/completion/complit.txt @@ -1,5 +1,19 @@ +This test checks completion related to composite literals. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false +} + +-- complit.go -- package complit +// Literal completion results. +/* int() */ //@item(int, "int()", "int", "var") + // general completions type position struct { //@item(structPosition, "position", "struct{...}", "struct") @@ -8,7 +22,7 @@ type position struct { //@item(structPosition, "position", "struct{...}", "struc func _() { _ = position{ - //@complete("", fieldX, fieldY, structPosition) + //@complete("", fieldX, fieldY, int, structPosition) } _ = position{ X: 1, @@ -20,7 +34,7 @@ func _() { } _ = []*position{ { - //@complete("", fieldX, fieldY, structPosition) + //@complete("", fieldX, fieldY, int, structPosition) }, } } @@ -36,7 +50,7 @@ func _() { } _ = map[int]int{ - //@complete("", abVar, aaVar, structPosition) + //@complete("", abVar, int, aaVar, structPosition) } _ = []string{a: ""} //@complete(":", abVar, aaVar) @@ -44,7 +58,7 @@ func _() { _ = position{X: a} //@complete("}", abVar, aaVar) _ = position{a} //@complete("}", abVar, aaVar) - _ = position{a, } //@complete("}", abVar, aaVar, structPosition) + _ = position{a, } //@complete("}", abVar, int, aaVar, structPosition) _ = []int{a} //@complete("}", abVar, aaVar) _ = [1]int{a} //@complete("}", abVar, aaVar) @@ -73,18 +87,18 @@ func _() { func _() { type foo struct{} //@item(complitFoo, "foo", "struct{...}", "struct") - var _ *foo = &fo{} //@snippet("{", complitFoo, "foo", "foo") - var _ *foo = fo{} //@snippet("{", complitFoo, "&foo", "&foo") + var _ *foo = &fo{} //@snippet("{", complitFoo, "foo") + var _ *foo = fo{} //@snippet("{", complitFoo, "&foo") struct { a, b *foo }{ a: &fo{}, //@rank("{", complitFoo) - b: fo{}, //@snippet("{", complitFoo, "&foo", "&foo") + b: fo{}, //@snippet("{", complitFoo, "&foo") } } func _() { _ := position{ - X: 1, //@complete("X", fieldX),complete(" 1", structPosition) - Y: , //@complete(":", fieldY),complete(" ,", structPosition) + X: 1, //@complete("X", fieldX),complete(" 1", int, structPosition) + Y: , //@complete(":", fieldY),complete(" ,", int, structPosition) } } diff --git a/gopls/internal/lsp/testdata/constant/constant.go b/gopls/internal/regtest/marker/testdata/completion/constant.txt similarity index 78% rename from gopls/internal/lsp/testdata/constant/constant.go rename to gopls/internal/regtest/marker/testdata/completion/constant.txt index c1c88e16edd..9ac2e43316a 100644 --- a/gopls/internal/lsp/testdata/constant/constant.go +++ b/gopls/internal/regtest/marker/testdata/completion/constant.txt @@ -1,3 +1,9 @@ +This test checks completion related to constants. + +-- flags -- +-ignore_extra_diags + +-- constant.go -- package constant const x = 1 //@item(constX, "x", "int", "const") diff --git a/gopls/internal/regtest/marker/testdata/completion/errors.txt b/gopls/internal/regtest/marker/testdata/completion/errors.txt new file mode 100644 index 00000000000..87e86ab05e9 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/errors.txt @@ -0,0 +1,33 @@ +This test checks completion related to errors. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "deepCompletion": false +} + +-- go.mod -- +module golang.org/lsptests + +go 1.18 + +-- errors.go -- +package errors + +import ( + "golang.org/lsptests/types" +) + +func _() { + bob.Bob() //@complete(".") + types.b //@complete(" //", Bob_interface) +} + +-- types/types.go -- +package types + +type Bob interface { //@item(Bob_interface, "Bob", "interface{...}", "interface") + Bobby() +} diff --git a/gopls/internal/lsp/testdata/fieldlist/field_list.go b/gopls/internal/regtest/marker/testdata/completion/field_list.txt similarity index 81% rename from gopls/internal/lsp/testdata/fieldlist/field_list.go rename to gopls/internal/regtest/marker/testdata/completion/field_list.txt index e687defb1d3..40658f04f4d 100644 --- a/gopls/internal/lsp/testdata/fieldlist/field_list.go +++ b/gopls/internal/regtest/marker/testdata/completion/field_list.txt @@ -1,3 +1,14 @@ +This test checks completion related to field lists. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false +} + +-- field_list.go -- package fieldlist var myInt int //@item(flVar, "myInt", "int", "var") diff --git a/gopls/internal/lsp/testdata/funcsig/func_sig.go b/gopls/internal/regtest/marker/testdata/completion/func_sig.txt similarity index 69% rename from gopls/internal/lsp/testdata/funcsig/func_sig.go rename to gopls/internal/regtest/marker/testdata/completion/func_sig.txt index 00f9b575d3c..7b323e23766 100644 --- a/gopls/internal/lsp/testdata/funcsig/func_sig.go +++ b/gopls/internal/regtest/marker/testdata/completion/func_sig.txt @@ -1,3 +1,9 @@ +This test checks completion related to function signatures. + +-- flags -- +-ignore_extra_diags + +-- func_sig.go -- package funcsig type someType int //@item(sigSomeType, "someType", "int", "type") diff --git a/gopls/internal/lsp/testdata/funcvalue/func_value.go b/gopls/internal/regtest/marker/testdata/completion/func_value.txt similarity index 86% rename from gopls/internal/lsp/testdata/funcvalue/func_value.go rename to gopls/internal/regtest/marker/testdata/completion/func_value.txt index 913fcbcfe54..9b1370f129d 100644 --- a/gopls/internal/lsp/testdata/funcvalue/func_value.go +++ b/gopls/internal/regtest/marker/testdata/completion/func_value.txt @@ -1,3 +1,9 @@ +This test checks completion related to function values. + +-- flags -- +-ignore_extra_diags + +-- func_value.go -- package funcvalue func fooFunc() int { //@item(fvFooFunc, "fooFunc", "func() int", "func") diff --git a/gopls/internal/lsp/testdata/index/index.go b/gopls/internal/regtest/marker/testdata/completion/index.txt similarity index 72% rename from gopls/internal/lsp/testdata/index/index.go rename to gopls/internal/regtest/marker/testdata/completion/index.txt index a2656893c91..b2fc840dffc 100644 --- a/gopls/internal/lsp/testdata/index/index.go +++ b/gopls/internal/regtest/marker/testdata/completion/index.txt @@ -1,3 +1,14 @@ +This test checks completion related to index expressions. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false +} + +-- index.go -- package index func _() { @@ -21,5 +32,5 @@ func _() { type myInt int var mi myInt //@item(indexMyInt, "mi", "myInt", "var") - foo[m] //@snippet("]", indexMyInt, "mi", "mi") + foo[m] //@snippet("]", indexMyInt, "mi") } diff --git a/gopls/internal/regtest/marker/testdata/completion/issue56505.txt b/gopls/internal/regtest/marker/testdata/completion/issue56505.txt new file mode 100644 index 00000000000..f79e69f4925 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/issue56505.txt @@ -0,0 +1,13 @@ +Test for golang/go#56505: completion on variables of type *error should not +panic. + +-- flags -- +-ignore_extra_diags + +-- issue.go -- +package issues + +func _() { + var e *error + e.x //@complete(" //") +} diff --git a/gopls/internal/lsp/testdata/typeassert/type_assert.go b/gopls/internal/regtest/marker/testdata/completion/type_assert.txt similarity index 86% rename from gopls/internal/lsp/testdata/typeassert/type_assert.go rename to gopls/internal/regtest/marker/testdata/completion/type_assert.txt index e24b68a070a..9cc81cd441f 100644 --- a/gopls/internal/lsp/testdata/typeassert/type_assert.go +++ b/gopls/internal/regtest/marker/testdata/completion/type_assert.txt @@ -1,3 +1,9 @@ +This test checks completion related to type assertions. + +-- flags -- +-ignore_extra_diags + +-- type_assert.go -- package typeassert type abc interface { //@item(abcIntf, "abc", "interface{...}", "interface") diff --git a/gopls/internal/regtest/marker/testdata/completion/unresolved.txt b/gopls/internal/regtest/marker/testdata/completion/unresolved.txt new file mode 100644 index 00000000000..d509b2670c4 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/unresolved.txt @@ -0,0 +1,16 @@ +This test verifies gopls does not crash on fake "resolved" types. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "completeUnimported": false +} + +-- unresolved.go -- +package unresolved + +func foo(interface{}) { + foo(func(i, j f //@complete(" //") +} diff --git a/gopls/internal/lsp/testdata/unsafe/unsafe.go b/gopls/internal/regtest/marker/testdata/completion/unsafe.txt similarity index 60% rename from gopls/internal/lsp/testdata/unsafe/unsafe.go rename to gopls/internal/regtest/marker/testdata/completion/unsafe.txt index 5d5e4340716..0683e3ae1b8 100644 --- a/gopls/internal/lsp/testdata/unsafe/unsafe.go +++ b/gopls/internal/regtest/marker/testdata/completion/unsafe.txt @@ -1,13 +1,24 @@ -package unsafe - -import ( - "unsafe" -) - -// Pre-set this marker, as we don't have a "source" for it in this package. -/* unsafe.Sizeof */ //@item(Sizeof, "Sizeof", "invalid type", "text") - -func _() { - x := struct{}{} - _ = unsafe.Sizeof(x) //@complete("z", Sizeof) -} +This test checks completion of symbols in the 'unsafe' package. + +-- flags -- +-ignore_extra_diags + +-- settings.json -- +{ + "matcher": "caseinsensitive" +} + +-- unsafe.go -- +package unsafe + +import ( + "unsafe" +) + +// Pre-set this marker, as we don't have a "source" for it in this package. +/* unsafe.Sizeof */ //@item(Sizeof, "Sizeof", "invalid type", "text") + +func _() { + x := struct{}{} + _ = unsafe.Sizeof(x) //@complete("z", Sizeof) +} diff --git a/gopls/internal/lsp/testdata/variadic/variadic.go.in b/gopls/internal/regtest/marker/testdata/completion/variadic.txt similarity index 54% rename from gopls/internal/lsp/testdata/variadic/variadic.go.in rename to gopls/internal/regtest/marker/testdata/completion/variadic.txt index 4787498ce7f..0b2ae8212df 100644 --- a/gopls/internal/lsp/testdata/variadic/variadic.go.in +++ b/gopls/internal/regtest/marker/testdata/completion/variadic.txt @@ -1,3 +1,9 @@ +This test checks completion related to variadic functions. + +-- flags -- +-ignore_extra_diags + +-- variadic.go -- package variadic func foo(i int, strs ...string) {} @@ -20,19 +26,42 @@ func _() { foo(123, s, "") //@rank(", \"", vStr, vStrSlice) // snippet will add the "..." for you - foo(123, ) //@snippet(")", vStrSlice, "ss...", "ss..."),snippet(")", vFunc, "bar()...", "bar()..."),snippet(")", vStr, "s", "s") + foo(123, ) //@snippet(")", vStrSlice, "ss..."),snippet(")", vFunc, "bar()..."),snippet(")", vStr, "s") // don't add "..." for interface{} - foo(123, ) //@snippet(")", vIntf, "v", "v") + foo(123, ) //@snippet(")", vIntf, "v") } func qux(...func()) {} func f() {} //@item(vVarArg, "f", "func()", "func") func _() { - qux(f) //@snippet(")", vVarArg, "f", "f") + qux(f) //@snippet(")", vVarArg, "f") } func _() { foo(0, []string{}...) //@complete(")") } + +-- variadic_intf.go -- +package variadic + +type baz interface { + baz() +} + +func wantsBaz(...baz) {} + +type bazImpl int + +func (bazImpl) baz() {} + +func _() { + var ( + impls []bazImpl //@item(vImplSlice, "impls", "[]bazImpl", "var") + impl bazImpl //@item(vImpl, "impl", "bazImpl", "var") + bazes []baz //@item(vIntfSlice, "bazes", "[]baz", "var") + ) + + wantsBaz() //@rank(")", vImpl, vImplSlice),rank(")", vIntfSlice, vImplSlice) +} From e125dc6bf642903f9f78a4fda4a08bc6d7b9fd71 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 1 Nov 2023 11:26:01 -0400 Subject: [PATCH 073/100] gopls/internal/regtest/marker: port remaining rank and snippet tests Thus ends the old completion tests. For golang/go#57979 For golang/go#54845 Change-Id: Ie4cf71c7581db4be64d7c8243d1c3d76ad8b9a64 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538801 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/completion_test.go | 118 ----------- .../internal/lsp/testdata/summary.txt.golden | 2 - gopls/internal/lsp/testdata/types/types.go | 18 -- gopls/internal/lsp/tests/tests.go | 149 ++------------ gopls/internal/lsp/tests/util.go | 190 ------------------ gopls/internal/lsp/tests/util_go118.go | 13 -- gopls/internal/lsp/tests/util_go121.go | 14 -- gopls/internal/lsp/tests/util_go122.go | 12 -- .../testdata/completion/multi_return.txt} | 7 + .../testdata/completion/nested_complit.txt | 23 +++ .../marker/testdata/completion/printf.txt} | 6 + .../marker/testdata/completion/type_mods.txt} | 10 +- .../testdata/completion/type_params.txt} | 22 +- 13 files changed, 72 insertions(+), 512 deletions(-) delete mode 100644 gopls/internal/lsp/completion_test.go delete mode 100644 gopls/internal/lsp/testdata/types/types.go delete mode 100644 gopls/internal/lsp/tests/util_go118.go delete mode 100644 gopls/internal/lsp/tests/util_go121.go delete mode 100644 gopls/internal/lsp/tests/util_go122.go rename gopls/internal/{lsp/testdata/multireturn/multi_return.go.in => regtest/marker/testdata/completion/multi_return.txt} (91%) create mode 100644 gopls/internal/regtest/marker/testdata/completion/nested_complit.txt rename gopls/internal/{lsp/testdata/printf/printf.go => regtest/marker/testdata/completion/printf.txt} (91%) rename gopls/internal/{lsp/testdata/typemods/type_mods.go => regtest/marker/testdata/completion/type_mods.txt} (65%) rename gopls/internal/{lsp/testdata/typeparams/type_params.go => regtest/marker/testdata/completion/type_params.txt} (86%) diff --git a/gopls/internal/lsp/completion_test.go b/gopls/internal/lsp/completion_test.go deleted file mode 100644 index 6e610452e96..00000000000 --- a/gopls/internal/lsp/completion_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package lsp - -import ( - "fmt" - "testing" - - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/source/completion" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" -) - -func (r *runner) CompletionSnippet(t *testing.T, src span.Span, expected tests.CompletionSnippet, placeholders bool, items tests.CompletionItems) { - list := r.callCompletion(t, src, func(opts *source.Options) { - opts.UsePlaceholders = placeholders - opts.DeepCompletion = true - opts.Matcher = source.Fuzzy - opts.CompleteUnimported = false - }) - got := tests.FindItem(list, *items[expected.CompletionItem]) - want := expected.PlainSnippet - if placeholders { - want = expected.PlaceholderSnippet - } - if diff := tests.DiffSnippets(want, got); diff != "" { - t.Errorf("%s", diff) - } -} - -func (r *runner) RankCompletion(t *testing.T, src span.Span, test tests.Completion, items tests.CompletionItems) { - got := r.callCompletion(t, src, func(opts *source.Options) { - opts.DeepCompletion = true - opts.Matcher = source.Fuzzy - opts.CompleteUnimported = false - opts.LiteralCompletions = true - opts.ExperimentalPostfixCompletions = true - }) - want := expected(t, test, items) - if msg := tests.CheckCompletionOrder(want, got, true); msg != "" { - t.Errorf("%s", msg) - } -} - -func expected(t *testing.T, test tests.Completion, items tests.CompletionItems) []protocol.CompletionItem { - t.Helper() - - toProtocolCompletionItem := func(item *completion.CompletionItem) protocol.CompletionItem { - pItem := protocol.CompletionItem{ - Label: item.Label, - Kind: item.Kind, - Detail: item.Detail, - Tags: []protocol.CompletionItemTag{}, // must be a slice - Documentation: &protocol.Or_CompletionItem_documentation{ - Value: item.Documentation, - }, - InsertText: item.InsertText, - TextEdit: &protocol.TextEdit{ - NewText: item.Snippet(), - }, - // Negate score so best score has lowest sort text like real API. - SortText: fmt.Sprint(-item.Score), - } - if pItem.InsertText == "" { - pItem.InsertText = pItem.Label - } - return pItem - } - - var want []protocol.CompletionItem - for _, pos := range test.CompletionItems { - want = append(want, toProtocolCompletionItem(items[pos])) - } - return want -} - -func (r *runner) callCompletion(t *testing.T, src span.Span, options func(*source.Options)) []protocol.CompletionItem { - t.Helper() - cleanup := r.toggleOptions(t, src.URI(), options) - defer cleanup() - - list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(src.URI()), - }, - Position: protocol.Position{ - Line: uint32(src.Start().Line() - 1), - Character: uint32(src.Start().Column() - 1), - }, - }, - }) - if err != nil { - t.Fatal(err) - } - return list.Items -} - -func (r *runner) toggleOptions(t *testing.T, uri span.URI, options func(*source.Options)) (reset func()) { - view, err := r.server.session.ViewOf(uri) - if err != nil { - t.Fatal(err) - } - folder := view.Folder() - - modified := r.server.Options().Clone() - options(modified) - if err = r.server.session.SetFolderOptions(r.ctx, folder, modified); err != nil { - t.Fatal(err) - } - return func() { - r.server.session.SetFolderOptions(r.ctx, folder, r.server.Options()) - } -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 3122dd679da..5489921f8d6 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,7 +1,5 @@ -- summary -- CallHierarchyCount = 2 -CompletionSnippetCount = 9 -RankedCompletionsCount = 45 SemanticTokenCount = 3 SuggestedFixCount = 80 MethodExtractionCount = 8 diff --git a/gopls/internal/lsp/testdata/types/types.go b/gopls/internal/lsp/testdata/types/types.go deleted file mode 100644 index c60d4b2e427..00000000000 --- a/gopls/internal/lsp/testdata/types/types.go +++ /dev/null @@ -1,18 +0,0 @@ -package types - -type CoolAlias = int //@item(CoolAlias, "CoolAlias", "int", "type") - -type X struct { //@item(X_struct, "X", "struct{...}", "struct") - x int -} - -type Y struct { //@item(Y_struct, "Y", "struct{...}", "struct") - y int -} - -type Bob interface { //@item(Bob_interface, "Bob", "interface{...}", "interface") - Bobby() -} - -func (*X) Bobby() {} -func (*Y) Bobby() {} diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 741688e530e..822a39b205e 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -16,7 +16,6 @@ import ( "os" "path/filepath" "sort" - "strconv" "strings" "sync" "testing" @@ -26,10 +25,8 @@ import ( "golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/source/completion" "golang.org/x/tools/gopls/internal/lsp/tests/compare" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/txtar" ) @@ -52,9 +49,6 @@ var UpdateGolden = flag.Bool("golden", false, "Update golden files") // These type names apparently avoid the need to repeat the // type in the field name and the make() expression. type CallHierarchy = map[span.Span]*CallHierarchyResult -type CompletionItems = map[token.Pos]*completion.CompletionItem -type CompletionSnippets = map[span.Span][]CompletionSnippet -type RankCompletions = map[span.Span][]Completion type SemanticTokens = []span.Span type SuggestedFixes = map[span.Span][]SuggestedFix type MethodExtractions = map[span.Span]span.Span @@ -64,19 +58,16 @@ type AddImport = map[span.URI]string type SelectionRanges = []span.Span type Data struct { - Config packages.Config - Exported *packagestest.Exported - CallHierarchy CallHierarchy - CompletionItems CompletionItems - CompletionSnippets CompletionSnippets - RankCompletions RankCompletions - SemanticTokens SemanticTokens - SuggestedFixes SuggestedFixes - MethodExtractions MethodExtractions - Renames Renames - InlayHints InlayHints - AddImport AddImport - SelectionRanges SelectionRanges + Config packages.Config + Exported *packagestest.Exported + CallHierarchy CallHierarchy + SemanticTokens SemanticTokens + SuggestedFixes SuggestedFixes + MethodExtractions MethodExtractions + Renames Renames + InlayHints InlayHints + AddImport AddImport + SelectionRanges SelectionRanges fragments map[string]string dir string @@ -97,8 +88,6 @@ type Data struct { // we can abolish the interface now. type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) - CompletionSnippet(*testing.T, span.Span, CompletionSnippet, bool, CompletionItems) - RankCompletion(*testing.T, span.Span, Completion, CompletionItems) SemanticTokens(*testing.T, span.Span) SuggestedFix(*testing.T, span.Span, []SuggestedFix, int) MethodExtraction(*testing.T, span.Span, span.Span) @@ -108,16 +97,6 @@ type Tests interface { SelectionRanges(*testing.T, span.Span) } -type CompletionTestType int - -const ( - // Default runs the standard completion tests. - _ = CompletionTestType(iota) - - // CompletionRank candidates in test must be valid and in the right relative order. - CompletionRank -) - type Completion struct { CompletionItems []token.Pos } @@ -201,14 +180,11 @@ func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*tes func load(t testing.TB, mode string, dir string) *Data { datum := &Data{ - CallHierarchy: make(CallHierarchy), - CompletionItems: make(CompletionItems), - CompletionSnippets: make(CompletionSnippets), - RankCompletions: make(RankCompletions), - Renames: make(Renames), - SuggestedFixes: make(SuggestedFixes), - MethodExtractions: make(MethodExtractions), - AddImport: make(AddImport), + CallHierarchy: make(CallHierarchy), + Renames: make(Renames), + SuggestedFixes: make(SuggestedFixes), + MethodExtractions: make(MethodExtractions), + AddImport: make(AddImport), dir: dir, fragments: map[string]string{}, @@ -341,9 +317,6 @@ func load(t testing.TB, mode string, dir string) *Data { // Collect any data that needs to be used by subsequent tests. if err := datum.Exported.Expect(map[string]interface{}{ - "item": datum.collectCompletionItems, - "rank": datum.collectCompletions(CompletionRank), - "snippet": datum.collectCompletionSnippets, "semantic": datum.collectSemanticTokens, "inlayHint": datum.collectInlayHints, "rename": datum.collectRenames, @@ -407,23 +380,6 @@ func Run(t *testing.T, tests Tests, data *Data) { t.Helper() checkData(t, data) - eachCompletion := func(t *testing.T, cases map[span.Span][]Completion, test func(*testing.T, span.Span, Completion, CompletionItems)) { - t.Helper() - - for src, exp := range cases { - for i, e := range exp { - t.Run(SpanName(src)+"_"+strconv.Itoa(i), func(t *testing.T) { - t.Helper() - if strings.Contains(t.Name(), "cgo") { - testenv.NeedsTool(t, "cgo") - } - test(t, src, e, data.CompletionItems) - }) - } - - } - } - t.Run("CallHierarchy", func(t *testing.T) { t.Helper() for spn, callHierarchyResult := range data.CallHierarchy { @@ -434,30 +390,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("CompletionSnippets", func(t *testing.T) { - t.Helper() - for _, placeholders := range []bool{true, false} { - for src, expecteds := range data.CompletionSnippets { - for i, expected := range expecteds { - name := SpanName(src) + "_" + strconv.Itoa(i+1) - if placeholders { - name += "_placeholders" - } - - t.Run(name, func(t *testing.T) { - t.Helper() - tests.CompletionSnippet(t, src, expected, placeholders, data.CompletionItems) - }) - } - } - } - }) - - t.Run("RankCompletions", func(t *testing.T) { - t.Helper() - eachCompletion(t, data.RankCompletions, tests.RankCompletion) - }) - t.Run("SemanticTokens", func(t *testing.T) { t.Helper() for _, spn := range data.SemanticTokens { @@ -552,21 +484,7 @@ func Run(t *testing.T, tests Tests, data *Data) { func checkData(t *testing.T, data *Data) { buf := &bytes.Buffer{} - snippetCount := 0 - for _, want := range data.CompletionSnippets { - snippetCount += len(want) - } - - countCompletions := func(c map[span.Span][]Completion) (count int) { - for _, want := range c { - count += len(want) - } - return count - } - fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy)) - fmt.Fprintf(buf, "CompletionSnippetCount = %v\n", snippetCount) - fmt.Fprintf(buf, "RankedCompletionsCount = %v\n", countCompletions(data.RankCompletions)) fmt.Fprintf(buf, "SemanticTokenCount = %v\n", len(data.SemanticTokens)) fmt.Fprintf(buf, "SuggestedFixCount = %v\n", len(data.SuggestedFixes)) fmt.Fprintf(buf, "MethodExtractionCount = %v\n", len(data.MethodExtractions)) @@ -653,35 +571,6 @@ func (data *Data) Golden(t *testing.T, tag, target string, update func() ([]byte return file.Data[:len(file.Data)-1] // drop the trailing \n } -func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) { - result := func(m map[span.Span][]Completion, src span.Span, expected []token.Pos) { - m[src] = append(m[src], Completion{ - CompletionItems: expected, - }) - } - switch typ { - case CompletionRank: - return func(src span.Span, expected []token.Pos) { - result(data.RankCompletions, src, expected) - } - default: - panic("unsupported") - } -} - -func (data *Data) collectCompletionItems(pos token.Pos, label, detail, kind string, args []string) { - var documentation string - if len(args) > 3 { - documentation = args[3] - } - data.CompletionItems[pos] = &completion.CompletionItem{ - Label: label, - Detail: detail, - Kind: protocol.ParseCompletionItemKind(kind), - Documentation: documentation, - } -} - func (data *Data) collectAddImports(spn span.Span, imp string) { data.AddImport[spn.URI()] = imp } @@ -756,14 +645,6 @@ func (data *Data) mustRange(spn span.Span) protocol.Range { return rng } -func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain, placeholder string) { - data.CompletionSnippets[spn] = append(data.CompletionSnippets[spn], CompletionSnippet{ - CompletionItem: item, - PlainSnippet: plain, - PlaceholderSnippet: placeholder, - }) -} - func uriName(uri span.URI) string { return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go")) } diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go index c7739f42652..ea0920d2e62 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -5,51 +5,11 @@ package tests import ( - "bytes" "fmt" - "go/token" - "sort" - "strconv" - "strings" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source/completion" - "golang.org/x/tools/gopls/internal/span" ) -var builtins = map[string]bool{ - "append": true, - "cap": true, - "close": true, - "complex": true, - "copy": true, - "delete": true, - "error": true, - "false": true, - "imag": true, - "iota": true, - "len": true, - "make": true, - "new": true, - "nil": true, - "panic": true, - "print": true, - "println": true, - "real": true, - "recover": true, - "true": true, -} - -// NormalizeAny replaces occurrences of interface{} in input with any. -// -// In Go 1.18, standard library functions were changed to use the 'any' -// alias in place of interface{}, which affects their type string. -func NormalizeAny(input string) string { - return strings.ReplaceAll(input, "interface{}", "any") -} - // DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string { expected := make(map[protocol.Location]bool) @@ -71,153 +31,3 @@ func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls } return "" } - -func FilterBuiltins(src span.Span, items []protocol.CompletionItem) []protocol.CompletionItem { - var ( - got []protocol.CompletionItem - wantBuiltins = strings.Contains(string(src.URI()), "builtins") - wantKeywords = strings.Contains(string(src.URI()), "keywords") - ) - for _, item := range items { - if !wantBuiltins && isBuiltin(item.Label, item.Detail, item.Kind) { - continue - } - - if !wantKeywords && token.Lookup(item.Label).IsKeyword() { - continue - } - - got = append(got, item) - } - return got -} - -func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool { - if detail == "" && kind == protocol.ClassCompletion { - return true - } - // Remaining builtin constants, variables, interfaces, and functions. - trimmed := label - if i := strings.Index(trimmed, "("); i >= 0 { - trimmed = trimmed[:i] - } - return builtins[trimmed] -} - -func CheckCompletionOrder(want, got []protocol.CompletionItem, strictScores bool) string { - var ( - matchedIdxs []int - lastGotIdx int - lastGotSort float64 - inOrder = true - errorMsg = "completions out of order" - ) - for _, w := range want { - var found bool - for i, g := range got { - if w.Label == g.Label && NormalizeAny(w.Detail) == NormalizeAny(g.Detail) && w.Kind == g.Kind { - matchedIdxs = append(matchedIdxs, i) - found = true - - if i < lastGotIdx { - inOrder = false - } - lastGotIdx = i - - sort, _ := strconv.ParseFloat(g.SortText, 64) - if strictScores && len(matchedIdxs) > 1 && sort <= lastGotSort { - inOrder = false - errorMsg = "candidate scores not strictly decreasing" - } - lastGotSort = sort - - break - } - } - if !found { - return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion") - } - } - - sort.Ints(matchedIdxs) - matched := make([]protocol.CompletionItem, 0, len(matchedIdxs)) - for _, idx := range matchedIdxs { - matched = append(matched, got[idx]) - } - - if !inOrder { - return summarizeCompletionItems(-1, want, matched, errorMsg) - } - - return "" -} - -func DiffSnippets(want string, got *protocol.CompletionItem) string { - if want == "" { - if got != nil { - x := got.TextEdit - return fmt.Sprintf("expected no snippet but got %s", x.NewText) - } - } else { - if got == nil { - return fmt.Sprintf("couldn't find completion matching %q", want) - } - x := got.TextEdit - if want != x.NewText { - return fmt.Sprintf("expected snippet %q, got %q", want, x.NewText) - } - } - return "" -} - -func FindItem(list []protocol.CompletionItem, want completion.CompletionItem) *protocol.CompletionItem { - for _, item := range list { - if item.Label == want.Label { - return &item - } - } - return nil -} - -// DiffCompletionItems prints the diff between expected and actual completion -// test results. -// -// The diff will be formatted using '-' and '+' for want and got, respectively. -func DiffCompletionItems(want, got []protocol.CompletionItem) string { - // Many fields are not set in the "want" slice. - irrelevantFields := []string{ - "AdditionalTextEdits", - "Documentation", - "TextEdit", - "SortText", - "Preselect", - "FilterText", - "InsertText", - "InsertTextFormat", - } - ignore := cmpopts.IgnoreFields(protocol.CompletionItem{}, irrelevantFields...) - normalizeAny := cmpopts.AcyclicTransformer("NormalizeAny", func(item protocol.CompletionItem) protocol.CompletionItem { - item.Detail = NormalizeAny(item.Detail) - return item - }) - return cmp.Diff(want, got, ignore, normalizeAny) -} - -func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string { - msg := &bytes.Buffer{} - fmt.Fprint(msg, "completion failed") - if i >= 0 { - fmt.Fprintf(msg, " at %d", i) - } - fmt.Fprint(msg, " because of ") - fmt.Fprintf(msg, reason, args...) - fmt.Fprint(msg, ":\nexpected:\n") - for _, d := range want { - fmt.Fprintf(msg, " %v\n", d) - } - fmt.Fprintf(msg, "got:\n") - for _, d := range got { - fmt.Fprintf(msg, " %v\n", d) - } - return msg.String() -} diff --git a/gopls/internal/lsp/tests/util_go118.go b/gopls/internal/lsp/tests/util_go118.go deleted file mode 100644 index 6115342df74..00000000000 --- a/gopls/internal/lsp/tests/util_go118.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.18 -// +build go1.18 - -package tests - -func init() { - builtins["any"] = true - builtins["comparable"] = true -} diff --git a/gopls/internal/lsp/tests/util_go121.go b/gopls/internal/lsp/tests/util_go121.go deleted file mode 100644 index c5b2278580b..00000000000 --- a/gopls/internal/lsp/tests/util_go121.go +++ /dev/null @@ -1,14 +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.21 -// +build go1.21 - -package tests - -func init() { - builtins["clear"] = true - builtins["max"] = true - builtins["min"] = true -} diff --git a/gopls/internal/lsp/tests/util_go122.go b/gopls/internal/lsp/tests/util_go122.go deleted file mode 100644 index 90ae029766a..00000000000 --- a/gopls/internal/lsp/tests/util_go122.go +++ /dev/null @@ -1,12 +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.22 -// +build go1.22 - -package tests - -func init() { - builtins["zero"] = true -} diff --git a/gopls/internal/lsp/testdata/multireturn/multi_return.go.in b/gopls/internal/regtest/marker/testdata/completion/multi_return.txt similarity index 91% rename from gopls/internal/lsp/testdata/multireturn/multi_return.go.in rename to gopls/internal/regtest/marker/testdata/completion/multi_return.txt index c302f3815f9..72facfcf6f3 100644 --- a/gopls/internal/lsp/testdata/multireturn/multi_return.go.in +++ b/gopls/internal/regtest/marker/testdata/completion/multi_return.txt @@ -1,3 +1,10 @@ +This test checks various ranking of completion results related to functions +with multiple return values. + +-- flags -- +-ignore_extra_diags + +-- multireturn.go -- package multireturn func f0() {} //@item(multiF0, "f0", "func()", "func") diff --git a/gopls/internal/regtest/marker/testdata/completion/nested_complit.txt b/gopls/internal/regtest/marker/testdata/completion/nested_complit.txt new file mode 100644 index 00000000000..f3a148dedf8 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/completion/nested_complit.txt @@ -0,0 +1,23 @@ +This test checks completion of nested composite literals; + +TODO(rfindley): investigate an un-skip the disabled test below. + +-- flags -- +-ignore_extra_diags + +-- nested_complit.go -- +package nested_complit + +type ncFoo struct {} //@item(structNCFoo, "ncFoo", "struct{...}", "struct") + +type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct") + baz []ncFoo +} + +func _() { + []ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var") + _ := ncBar{ + // disabled - see issue #54822 + baz: [] // complete(" //", structNCFoo, structNCBar) + } +} diff --git a/gopls/internal/lsp/testdata/printf/printf.go b/gopls/internal/regtest/marker/testdata/completion/printf.txt similarity index 91% rename from gopls/internal/lsp/testdata/printf/printf.go rename to gopls/internal/regtest/marker/testdata/completion/printf.txt index 6e56549c141..99b8a1e7e8c 100644 --- a/gopls/internal/lsp/testdata/printf/printf.go +++ b/gopls/internal/regtest/marker/testdata/completion/printf.txt @@ -1,3 +1,9 @@ +This test checks various ranking of completion results related to printf. + +-- flags -- +-ignore_extra_diags + +-- printf.go -- package printf import "fmt" diff --git a/gopls/internal/lsp/testdata/typemods/type_mods.go b/gopls/internal/regtest/marker/testdata/completion/type_mods.txt similarity index 65% rename from gopls/internal/lsp/testdata/typemods/type_mods.go rename to gopls/internal/regtest/marker/testdata/completion/type_mods.txt index f5f0f807674..de295c62e9a 100644 --- a/gopls/internal/lsp/testdata/typemods/type_mods.go +++ b/gopls/internal/regtest/marker/testdata/completion/type_mods.txt @@ -1,3 +1,9 @@ +This test check completion snippets with type modifiers. + +-- flags -- +-ignore_extra_diags + +-- typemods.go -- package typemods func fooFunc() func() int { //@item(modFooFunc, "fooFunc", "func() func() int", "func") @@ -11,11 +17,11 @@ func fooPtr() *int { //@item(modFooPtr, "fooPtr", "func() *int", "func") } func _() { - var _ int = foo //@snippet(" //", modFooFunc, "fooFunc()()", "fooFunc()()"),snippet(" //", modFooPtr, "*fooPtr()", "*fooPtr()") + var _ int = foo //@snippet(" //", modFooFunc, "fooFunc()()"),snippet(" //", modFooPtr, "*fooPtr()") } func _() { var m map[int][]chan int //@item(modMapChanPtr, "m", "map[int]chan *int", "var") - var _ int = m //@snippet(" //", modMapChanPtr, "<-m[${1:}][${2:}]", "<-m[${1:}][${2:}]") + var _ int = m //@snippet(" //", modMapChanPtr, "<-m[${1:}][${2:}]") } diff --git a/gopls/internal/lsp/testdata/typeparams/type_params.go b/gopls/internal/regtest/marker/testdata/completion/type_params.txt similarity index 86% rename from gopls/internal/lsp/testdata/typeparams/type_params.go rename to gopls/internal/regtest/marker/testdata/completion/type_params.txt index 7ece33f58e5..185d77f9911 100644 --- a/gopls/internal/lsp/testdata/typeparams/type_params.go +++ b/gopls/internal/regtest/marker/testdata/completion/type_params.txt @@ -1,6 +1,10 @@ -//go:build go1.18 -// +build go1.18 +This test checks various ranking of completion results related to type +parameters. +-- flags -- +-ignore_extra_diags + +-- type_params.go -- package typeparams // Copied from the old builtins.go, which has been ported to the new marker tests. @@ -32,15 +36,15 @@ func _() { func takesGeneric[a int | string](s[a]) { "s[a]{}" //@item(tpInScopeLit, "s[a]{}", "", "var") - takesGeneric() //@rank(")", tpInScopeLit),snippet(")", tpInScopeLit, "s[a]{\\}", "s[a]{\\}") + takesGeneric() //@rank(")", tpInScopeLit),snippet(")", tpInScopeLit, "s[a]{\\}") } func _() { s[int]{} //@item(tpInstLit, "s[int]{}", "", "var") - takesGeneric[int]() //@rank(")", tpInstLit),snippet(")", tpInstLit, "s[int]{\\}", "s[int]{\\}") + takesGeneric[int]() //@rank(")", tpInstLit),snippet(")", tpInstLit, "s[int]{\\}") "s[...]{}" //@item(tpUninstLit, "s[...]{}", "", "var") - takesGeneric() //@rank(")", tpUninstLit),snippet(")", tpUninstLit, "s[${1:}]{\\}", "s[${1:a}]{\\}") + takesGeneric() //@rank(")", tpUninstLit),snippet(")", tpUninstLit, "s[${1:}]{\\}") } func returnTP[A int | float64](a A) A { //@item(returnTP, "returnTP", "something", "func") @@ -49,7 +53,7 @@ func returnTP[A int | float64](a A) A { //@item(returnTP, "returnTP", "something func _() { // disabled - see issue #54822 - var _ int = returnTP // snippet(" //", returnTP, "returnTP[${1:}](${2:})", "returnTP[${1:A int|float64}](${2:a A})") + var _ int = returnTP // snippet(" //", returnTP, "returnTP[${1:}](${2:})") var aa int //@item(tpInt, "aa", "int", "var") var ab float64 //@item(tpFloat, "ab", "float64", "var") @@ -57,11 +61,11 @@ func _() { } func takesFunc[T any](func(T) T) { - var _ func(t T) T = f //@snippet(" //", tpLitFunc, "func(t T) T {$0\\}", "func(t T) T {$0\\}") + var _ func(t T) T = f //@snippet(" //", tpLitFunc, "func(t T) T {$0\\}") } func _() { _ = "func(...) {}" //@item(tpLitFunc, "func(...) {}", "", "var") - takesFunc() //@snippet(")", tpLitFunc, "func(${1:}) ${2:} {$0\\}", "func(${1:t} ${2:T}) ${3:T} {$0\\}") - takesFunc[int]() //@snippet(")", tpLitFunc, "func(i int) int {$0\\}", "func(${1:i} int) int {$0\\}") + takesFunc() //@snippet(")", tpLitFunc, "func(${1:}) ${2:} {$0\\}") + takesFunc[int]() //@snippet(")", tpLitFunc, "func(i int) int {$0\\}") } From 24c5a47c8795e01485b2f65e9975fb0bde5961ed Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 27 Oct 2023 18:03:32 -0400 Subject: [PATCH 074/100] gopls/internal/lsp/cache: make options immutable on the View In the zero-config gopls world, it will be much simpler if Views are immutable. As a first step toward this, make options an immutable property of the View, and store them in a new Folder abstraction. In the future, folders may be shared across multiple views. This removes the 'minorOptionsChange' logic that avoided recreating Views if the options change was not 'major'. Digging into the history of that functionality, it seems like a premature optimization, or perhaps it was only necessary for tests. Notably, the old completion marker tests relied on being able to toggle options in between assertions (or in some cases, in the middle of assertions!). Those tests ran too slow on this CL, and so have all been ported over to the new marker framework, which uses static per-suite options. On average, most real clients should change options very infrequently if at all, and so reinitializing the view (effectively restarting gopls) is not a big deal. For golang/go#57979 Change-Id: I78d86a9ddb85f29e781705b898a66a5642ff2611 Reviewed-on: https://go-review.googlesource.com/c/tools/+/538796 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/cache/analysis.go | 4 +- gopls/internal/lsp/cache/check.go | 4 +- gopls/internal/lsp/cache/imports.go | 12 +-- gopls/internal/lsp/cache/load.go | 4 +- gopls/internal/lsp/cache/session.go | 28 ++++--- gopls/internal/lsp/cache/snapshot.go | 43 ++++------ gopls/internal/lsp/cache/view.go | 112 +++++++++------------------ gopls/internal/lsp/lsp_test.go | 7 +- gopls/internal/lsp/source/view.go | 13 +++- gopls/internal/lsp/workspace.go | 8 +- 10 files changed, 100 insertions(+), 135 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 0466af33735..542ce5efa2a 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -192,7 +192,7 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, toSrc := make(map[*analysis.Analyzer]*source.Analyzer) var enabled []*analysis.Analyzer // enabled subset + transitive requirements for _, a := range analyzers { - if a.IsEnabled(snapshot.options) { + if a.IsEnabled(snapshot.Options()) { toSrc[a.Analyzer] = a enabled = append(enabled, a.Analyzer) } @@ -309,7 +309,7 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, // Now that we have read all files, // we no longer need the snapshot. // (but options are needed for progress reporting) - options := snapshot.options + options := snapshot.Options() snapshot = nil // Progress reporting. If supported, gopls reports progress on analysis diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index a95c439cfa7..93d6b086fde 100644 --- a/gopls/internal/lsp/cache/check.go +++ b/gopls/internal/lsp/cache/check.go @@ -1342,8 +1342,8 @@ func (s *snapshot) typeCheckInputs(ctx context.Context, m *source.Metadata) (typ depsByImpPath: m.DepsByImpPath, goVersion: goVersion, - relatedInformation: s.options.RelatedInformationSupported, - linkTarget: s.options.LinkTarget, + relatedInformation: s.Options().RelatedInformationSupported, + linkTarget: s.Options().LinkTarget, moduleMode: s.view.moduleMode(), }, nil } diff --git a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go index 028607608cc..f820da4ab46 100644 --- a/gopls/internal/lsp/cache/imports.go +++ b/gopls/internal/lsp/cache/imports.go @@ -54,13 +54,13 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot // view.goEnv is immutable -- changes make a new view. Options can change. // We can't compare build flags directly because we may add -modfile. - localPrefix := snapshot.options.Local - currentBuildFlags := snapshot.options.BuildFlags - currentDirectoryFilters := snapshot.options.DirectoryFilters + localPrefix := snapshot.Options().Local + currentBuildFlags := snapshot.Options().BuildFlags + currentDirectoryFilters := snapshot.Options().DirectoryFilters changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || - snapshot.options.VerboseOutput != (s.processEnv.Logf != nil) || + snapshot.Options().VerboseOutput != (s.processEnv.Logf != nil) || modFileHash != s.cachedModFileHash || - !reflect.DeepEqual(snapshot.options.DirectoryFilters, s.cachedDirectoryFilters) + !reflect.DeepEqual(snapshot.Options().DirectoryFilters, s.cachedDirectoryFilters) // If anything relevant to imports has changed, clear caches and // update the processEnv. Clearing caches blocks on any background @@ -118,7 +118,7 @@ func populateProcessEnvFromSnapshot(ctx context.Context, pe *imports.ProcessEnv, ctx, done := event.Start(ctx, "cache.populateProcessEnvFromSnapshot") defer done() - if snapshot.options.VerboseOutput { + if snapshot.Options().VerboseOutput { pe.Logf = func(format string, args ...interface{}) { event.Log(ctx, fmt.Sprintf(format, args...)) } diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 0ac4e5d9c06..4eae309bafb 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -75,7 +75,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc if err != nil { continue } - if isStandaloneFile(contents, s.options.StandaloneTags) { + if isStandaloneFile(contents, s.Options().StandaloneTags) { standalone = true query = append(query, uri.Filename()) } else { @@ -178,7 +178,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc continue } - if !containsDir || s.options.VerboseOutput { + if !containsDir || s.Options().VerboseOutput { event.Log(ctx, eventName, append( source.SnapshotLabels(s), tag.Package.Of(pkg.ID), diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 7346e24f82a..6a6b7fe2aa6 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -76,15 +76,15 @@ func (s *Session) Cache() *Cache { // of its gopls workspace module in that directory, so that client tooling // can execute in the same main module. On success it also returns a release // function that must be called when the Snapshot is no longer needed. -func (s *Session) NewView(ctx context.Context, name string, folder span.URI, options *source.Options) (*View, source.Snapshot, func(), error) { +func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, source.Snapshot, func(), error) { s.viewMu.Lock() defer s.viewMu.Unlock() for _, view := range s.views { - if span.SameExistingFile(view.folder, folder) { + if span.SameExistingFile(view.folder.Dir, folder.Dir) { return nil, nil, nil, source.ErrViewExists } } - view, snapshot, release, err := s.createView(ctx, name, folder, options, 0) + view, snapshot, release, err := s.createView(ctx, folder, 0) if err != nil { return nil, nil, nil, err } @@ -97,11 +97,11 @@ func (s *Session) NewView(ctx context.Context, name string, folder span.URI, opt // TODO(rfindley): clarify that createView can never be cancelled (with the // possible exception of server shutdown). // On success, the caller becomes responsible for calling the release function once. -func (s *Session) createView(ctx context.Context, name string, folder span.URI, options *source.Options, seqID uint64) (*View, *snapshot, func(), error) { +func (s *Session) createView(ctx context.Context, folder *Folder, seqID uint64) (*View, *snapshot, func(), error) { index := atomic.AddInt64(&viewIndex, 1) // Get immutable workspace information. - info, err := s.getWorkspaceInformation(ctx, folder, options) + info, err := s.getWorkspaceInformation(ctx, folder.Dir, folder.Options) if err != nil { return nil, nil, nil, err } @@ -117,11 +117,10 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, v := &View{ id: strconv.FormatInt(index, 10), gocmdRunner: s.gocmdRunner, - lastOptions: options, + folder: folder, initialWorkspaceLoad: make(chan struct{}), initializationSema: make(chan struct{}, 1), baseCtx: baseCtx, - name: name, moduleUpgrades: map[span.URI]map[string]string{}, vulns: map[span.URI]*vulncheck.Result{}, parseCache: s.parseCache, @@ -133,12 +132,12 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, processEnv: &imports.ProcessEnv{ GocmdRunner: s.gocmdRunner, SkipPathInScan: func(dir string) bool { - prefix := strings.TrimSuffix(string(v.folder), "/") + "/" + prefix := strings.TrimSuffix(string(v.folder.Dir), "/") + "/" uri := strings.TrimSuffix(string(span.URIFromPath(dir)), "/") if !strings.HasPrefix(uri+"/", prefix) { return false } - filterer := source.NewFilterer(options.DirectoryFilters) + filterer := source.NewFilterer(folder.Options.DirectoryFilters) rel := strings.TrimPrefix(uri, prefix) disallow := filterer.Disallow(rel) return disallow @@ -167,7 +166,6 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, workspaceModFiles: wsModFiles, workspaceModFilesErr: wsModFilesErr, pkgIndex: typerefs.NewPackageIndex(), - options: options, } // Save one reference in the view. v.releaseSnapshot = v.snapshot.Acquire() @@ -297,7 +295,7 @@ func (s *Session) RemoveView(view *View) { // // If the resulting error is non-nil, the view may or may not have already been // dropped from the session. -func (s *Session) updateViewLocked(ctx context.Context, view *View, options *source.Options) error { +func (s *Session) updateViewLocked(ctx context.Context, view *View, folder *Folder) error { // Preserve the snapshot ID if we are recreating the view. view.snapshotMu.Lock() if view.snapshot == nil { @@ -312,7 +310,7 @@ func (s *Session) updateViewLocked(ctx context.Context, view *View, options *sou return fmt.Errorf("view %q not found", view.id) } - v, snapshot, release, err := s.createView(ctx, view.name, view.folder, options, seqID) + v, snapshot, release, err := s.createView(ctx, folder, seqID) if err != nil { // we have dropped the old view, but could not create the new one // this should not happen and is very bad, but we still need to clean @@ -427,7 +425,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // synchronously to change processing? Can we assume that the env did not // change, and derive go.work using a combination of the configured // GOWORK value and filesystem? - info, err := s.getWorkspaceInformation(ctx, view.folder, view.lastOptions) + info, err := s.getWorkspaceInformation(ctx, view.folder.Dir, view.folder.Options) if err != nil { // Catastrophic failure, equivalent to a failure of session // initialization and therefore should almost never happen. One @@ -441,7 +439,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif } if info != view.workspaceInformation { - if err := s.updateViewLocked(ctx, view, view.lastOptions); err != nil { + if err := s.updateViewLocked(ctx, view, view.folder); err != nil { // More catastrophic failure. The view may or may not still exist. // The best we can do is log and move on. event.Error(ctx, "recreating view", err) @@ -499,7 +497,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif var releases []func() viewToSnapshot := map[*View]*snapshot{} for view, changed := range views { - snapshot, release := view.invalidateContent(ctx, changed, nil, forceReloadMetadata) + snapshot, release := view.invalidateContent(ctx, changed, forceReloadMetadata) releases = append(releases, release) viewToSnapshot[view] = snapshot } diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index 6fc69bdda3d..d7f51dd9d21 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -175,10 +175,6 @@ type snapshot struct { // detect ignored files. ignoreFilterOnce sync.Once ignoreFilter *ignoreFilter - - // options holds the user configuration at the time this snapshot was - // created. - options *source.Options } var globalSnapshotID uint64 @@ -288,7 +284,7 @@ func (s *snapshot) FileKind(fh source.FileHandle) source.FileKind { case ".work": return source.Work } - exts := s.options.TemplateExtensions + exts := s.Options().TemplateExtensions for _, ext := range exts { if fext == ext || fext == "."+ext { return source.Tmpl @@ -299,7 +295,7 @@ func (s *snapshot) FileKind(fh source.FileHandle) source.FileKind { } func (s *snapshot) Options() *source.Options { - return s.options // temporarily return view options. + return s.view.folder.Options } func (s *snapshot) BackgroundContext() context.Context { @@ -373,7 +369,7 @@ func (s *snapshot) workspaceMode() workspaceMode { return mode } mode |= moduleMode - if s.options.TempModfile { + if s.Options().TempModfile { mode |= tempModfile } return mode @@ -408,7 +404,7 @@ func (s *snapshot) config(ctx context.Context, inv *gocommand.Invocation) *packa panic("go/packages must not be used to parse files") }, Logf: func(format string, args ...interface{}) { - if s.options.VerboseOutput { + if s.Options().VerboseOutput { event.Log(ctx, fmt.Sprintf(format, args...)) } }, @@ -490,16 +486,16 @@ func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd stri // it used only after call to tempModFile. Clarify that it is only // non-nil on success. func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.InvocationFlags, inv *gocommand.Invocation) (tmpURI span.URI, updatedInv *gocommand.Invocation, cleanup func(), err error) { - allowModfileModificationOption := s.options.AllowModfileModifications - allowNetworkOption := s.options.AllowImplicitNetworkAccess + allowModfileModificationOption := s.Options().AllowModfileModifications + allowNetworkOption := s.Options().AllowImplicitNetworkAccess // TODO(rfindley): this is very hard to follow, and may not even be doing the // right thing: should inv.Env really trample view.options? Do we ever invoke // this with a non-empty inv.Env? // // We should refactor to make it clearer that the correct env is being used. - inv.Env = append(append(append(os.Environ(), s.options.EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.GO111MODULE()) - inv.BuildFlags = append([]string{}, s.options.BuildFlags...) + inv.Env = append(append(append(os.Environ(), s.Options().EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.GO111MODULE()) + inv.BuildFlags = append([]string{}, s.Options().BuildFlags...) cleanup = func() {} // fallback // All logic below is for module mode. @@ -659,7 +655,7 @@ func (s *snapshot) PackageDiagnostics(ctx context.Context, ids ...PackageID) (ma perFile[diag.URI] = append(perFile[diag.URI], diag) } } - pre := func(i int, ph *packageHandle) bool { + pre := func(_ int, ph *packageHandle) bool { data, err := filecache.Get(diagnosticsKind, ph.key) if err == nil { // hit collect(ph.m.Diagnostics) @@ -917,7 +913,7 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru // If GOWORK is outside the folder, ensure we are watching it. gowork, _ := s.view.GOWORK() - if gowork != "" && !source.InDir(s.view.folder.Filename(), gowork.Filename()) { + if gowork != "" && !source.InDir(s.view.folder.Dir.Filename(), gowork.Filename()) { patterns[gowork.Filename()] = struct{}{} } @@ -926,7 +922,7 @@ func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]stru for _, dir := range dirs { // If the directory is within the view's folder, we're already watching // it with the first pattern above. - if source.InDir(s.view.folder.Filename(), dir) { + if source.InDir(s.view.folder.Dir.Filename(), dir) { continue } // TODO(rstambler): If microsoft/vscode#3025 is resolved before @@ -983,7 +979,7 @@ func (s *snapshot) workspaceDirs(ctx context.Context) []string { // Dirs should, at the very least, contain the working directory and folder. dirSet[s.view.goCommandDir.Filename()] = unit{} - dirSet[s.view.folder.Filename()] = unit{} + dirSet[s.view.folder.Dir.Filename()] = unit{} // Additionally, if e.g. go.work indicates other workspace modules, we should // include their directories too. @@ -1006,7 +1002,7 @@ func (s *snapshot) workspaceDirs(ctx context.Context) []string { // Code) that do not send notifications for individual files in a directory // when the entire directory is deleted. func (s *snapshot) watchSubdirs() bool { - switch p := s.options.SubdirWatchPatterns; p { + switch p := s.Options().SubdirWatchPatterns; p { case source.SubdirWatchPatternsOn: return true case source.SubdirWatchPatternsOff: @@ -1019,7 +1015,7 @@ func (s *snapshot) watchSubdirs() bool { // requirements that client names do not change. We should update the VS // Code extension to set a default value of "subdirWatchPatterns" to "on", // so that this workaround is only temporary. - if s.options.ClientInfo != nil && s.options.ClientInfo.Name == "Visual Studio Code" { + if s.Options().ClientInfo != nil && s.Options().ClientInfo.Name == "Visual Studio Code" { return true } return false @@ -1684,7 +1680,7 @@ searchOverlays: if goMod, err := nearestModFile(ctx, fh.URI(), s); err == nil && goMod != "" { if _, ok := loadedModFiles[goMod]; !ok { modDir := filepath.Dir(goMod.Filename()) - viewDir := s.view.folder.Filename() + viewDir := s.view.folder.Dir.Filename() // When the module is underneath the view dir, we offer // "use all modules" quick-fixes. @@ -1817,7 +1813,7 @@ func inVendor(uri span.URI) bool { return found && strings.Contains(after, "/") } -func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source.FileHandle, newOptions *source.Options, forceReloadMetadata bool) (*snapshot, func()) { +func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source.FileHandle, forceReloadMetadata bool) (*snapshot, func()) { ctx, done := event.Start(ctx, "cache.snapshot.clone") defer done() @@ -1850,11 +1846,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source workspaceModFilesErr: s.workspaceModFilesErr, importGraph: s.importGraph, pkgIndex: s.pkgIndex, - options: s.options, - } - - if newOptions != nil { - result.options = newOptions } // Create a lease on the new snapshot. @@ -2439,7 +2430,7 @@ func (s *snapshot) BuiltinFile(ctx context.Context) (*source.ParsedGoFile, error s.mu.Unlock() if builtin == "" { - return nil, fmt.Errorf("no builtin package for view %s", s.view.name) + return nil, fmt.Errorf("no builtin package for view %s", s.view.folder.Name) } fh, err := s.ReadFile(ctx, builtin) diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index ed1e2fe56ef..81553d3cf4d 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -14,7 +14,6 @@ import ( "os" "path" "path/filepath" - "reflect" "regexp" "sort" "strings" @@ -34,6 +33,20 @@ import ( "golang.org/x/tools/internal/xcontext" ) +// A Folder represents an LSP workspace folder, together with its per-folder +// options. +// +// Folders (Name and Dir) are specified by the 'initialize' and subsequent +// 'didChangeWorkspaceFolders' requests; their options come from +// didChangeConfiguration. +// +// Folders must not be mutated, as they may be shared across multiple views. +type Folder struct { + Dir span.URI + Name string + Options *source.Options +} + type View struct { id string @@ -43,14 +56,7 @@ type View struct { // background contexts created for this view. baseCtx context.Context - // name is the user-specified name of this view. - name string - - // lastOptions holds the most recent options on this view, used for detecting - // major changes. - // - // Guarded by Session.viewMu. - lastOptions *source.Options + folder *Folder // Workspace information. The fields below are immutable, and together with // options define the build list. Any change to these fields results in a new @@ -407,52 +413,12 @@ func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanu // Name returns the user visible name of this view. func (v *View) Name() string { - return v.name + return v.folder.Name } // Folder returns the folder at the base of this view. func (v *View) Folder() span.URI { - return v.folder -} - -func minorOptionsChange(a, b *source.Options) bool { - // TODO(rfindley): this function detects whether a view should be recreated, - // but this is also checked by the getWorkspaceInformation logic. - // - // We should eliminate this redundancy. - // - // Additionally, this function has existed for a long time, but git history - // suggests that it was added arbitrarily, not due to an actual performance - // problem. - // - // Especially now that we have optimized reinitialization of the session, we - // should consider just always creating a new view on any options change. - - // Check if any of the settings that modify our understanding of files have - // been changed. - if !reflect.DeepEqual(a.Env, b.Env) { - return false - } - if !reflect.DeepEqual(a.DirectoryFilters, b.DirectoryFilters) { - return false - } - if !reflect.DeepEqual(a.StandaloneTags, b.StandaloneTags) { - return false - } - if a.ExpandWorkspaceToModule != b.ExpandWorkspaceToModule { - return false - } - if a.MemoryMode != b.MemoryMode { - return false - } - aBuildFlags := make([]string, len(a.BuildFlags)) - bBuildFlags := make([]string, len(b.BuildFlags)) - copy(aBuildFlags, a.BuildFlags) - copy(bBuildFlags, b.BuildFlags) - sort.Strings(aBuildFlags) - sort.Strings(bBuildFlags) - // the rest of the options are benign - return reflect.DeepEqual(aBuildFlags, bBuildFlags) + return v.folder.Dir } // SetFolderOptions updates the options of each View associated with the folder @@ -465,8 +431,10 @@ func (s *Session) SetFolderOptions(ctx context.Context, uri span.URI, options *s defer s.viewMu.Unlock() for _, v := range s.views { - if v.folder == uri { - if err := s.setViewOptions(ctx, v, options); err != nil { + if v.folder.Dir == uri { + folder2 := *v.folder + folder2.Options = options + if err := s.updateViewLocked(ctx, v, &folder2); err != nil { return err } } @@ -474,23 +442,12 @@ func (s *Session) SetFolderOptions(ctx context.Context, uri span.URI, options *s return nil } -func (s *Session) setViewOptions(ctx context.Context, v *View, options *source.Options) error { - // no need to rebuild the view if the options were not materially changed - if minorOptionsChange(v.lastOptions, options) { - _, release := v.invalidateContent(ctx, nil, options, false) - release() - v.lastOptions = options - return nil - } - return s.updateViewLocked(ctx, v, options) -} - // viewEnv returns a string describing the environment of a newly created view. // // It must not be called concurrently with any other view methods. func viewEnv(v *View) string { - env := v.snapshot.options.EnvSlice() - buildFlags := append([]string{}, v.snapshot.options.BuildFlags...) + env := v.folder.Options.EnvSlice() + buildFlags := append([]string{}, v.folder.Options.BuildFlags...) var buf bytes.Buffer fmt.Fprintf(&buf, `go info for %v @@ -500,7 +457,7 @@ func viewEnv(v *View) string { (build flags: %v) (selected go env: %v) `, - v.folder.Filename(), + v.folder.Dir.Filename(), v.goCommandDir.Filename(), strings.TrimRight(v.workspaceInformation.goversionOutput, "\n"), v.snapshot.validBuildConfiguration(), @@ -539,14 +496,14 @@ func fileHasExtension(path string, suffixes []string) bool { // locateTemplateFiles ensures that the snapshot has mapped template files // within the workspace folder. func (s *snapshot) locateTemplateFiles(ctx context.Context) { - if len(s.options.TemplateExtensions) == 0 { + suffixes := s.Options().TemplateExtensions + if len(suffixes) == 0 { return } - suffixes := s.options.TemplateExtensions searched := 0 filterFunc := s.filterFunc() - err := filepath.WalkDir(s.view.folder.Filename(), func(path string, entry os.DirEntry, err error) error { + err := filepath.WalkDir(s.view.folder.Dir.Filename(), func(path string, entry os.DirEntry, err error) error { if err != nil { return err } @@ -586,10 +543,10 @@ func (s *snapshot) contains(uri span.URI) bool { // user. It would be better to explicitly consider the set of active modules // wherever relevant. inGoDir := false - if source.InDir(s.view.goCommandDir.Filename(), s.view.folder.Filename()) { + if source.InDir(s.view.goCommandDir.Filename(), s.view.folder.Dir.Filename()) { inGoDir = source.InDir(s.view.goCommandDir.Filename(), uri.Filename()) } - inFolder := source.InDir(s.view.folder.Filename(), uri.Filename()) + inFolder := source.InDir(s.view.folder.Dir.Filename(), uri.Filename()) if !inGoDir && !inFolder { return false @@ -601,11 +558,12 @@ func (s *snapshot) contains(uri span.URI) bool { // filterFunc returns a func that reports whether uri is filtered by the currently configured // directoryFilters. func (s *snapshot) filterFunc() func(span.URI) bool { - filterer := buildFilterer(s.view.folder.Filename(), s.view.gomodcache, s.options) + folderDir := s.view.folder.Dir.Filename() + filterer := buildFilterer(folderDir, s.view.gomodcache, s.Options()) return func(uri span.URI) bool { // Only filter relative to the configured root directory. - if source.InDir(s.view.folder.Filename(), uri.Filename()) { - return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), s.view.folder.Filename()), filterer) + if source.InDir(folderDir, uri.Filename()) { + return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), folderDir), filterer) } return false } @@ -917,7 +875,7 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadEr // a callback which the caller must invoke to release that snapshot. // // newOptions may be nil, in which case options remain unchanged. -func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]source.FileHandle, newOptions *source.Options, forceReloadMetadata bool) (*snapshot, func()) { +func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]source.FileHandle, forceReloadMetadata bool) (*snapshot, func()) { // Detach the context so that content invalidation cannot be canceled. ctx = xcontext.Detach(ctx) @@ -939,7 +897,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]sourc prevSnapshot.AwaitInitialized(ctx) // Save one lease of the cloned snapshot in the view. - v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes, newOptions, forceReloadMetadata) + v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) prevReleaseSnapshot() v.destroy(prevSnapshot, "View.invalidateContent") diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 5a5f8562bf9..34f3330520d 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -53,7 +53,12 @@ func testLSP(t *testing.T, datum *tests.Data) { session := cache.NewSession(ctx, cache.New(nil)) options := source.DefaultOptions(tests.DefaultOptions) options.SetEnvSlice(datum.Config.Env) - view, snapshot, release, err := session.NewView(ctx, datum.Config.Dir, span.URIFromPath(datum.Config.Dir), options) + folder := &cache.Folder{ + Dir: span.URIFromPath(datum.Config.Dir), + Name: datum.Config.Dir, + Options: options, + } + view, snapshot, release, err := session.NewView(ctx, folder) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index d501e04ce00..7851d130a6c 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -372,9 +372,16 @@ func (m InvocationFlags) AllowNetwork() bool { return m&AllowNetwork != 0 } -// View represents a single workspace. -// This is the level at which we maintain configuration like working directory -// and build tags. +// View represents a single build context for a workspace. +// +// A unique build is determined by the workspace folder along with a Go +// environment (GOOS, GOARCH, GOWORK, etc). +// +// Additionally, the View holds a pointer to the current state of that build +// (the Snapshot). +// +// TODO(rfindley): move all other state such as module upgrades into the +// Snapshot. type View interface { // ID returns a globally unique identifier for this view. ID() string diff --git a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go index d48e4f473cf..0a7dd4c73a7 100644 --- a/gopls/internal/lsp/workspace.go +++ b/gopls/internal/lsp/workspace.go @@ -9,6 +9,7 @@ import ( "fmt" "sync" + "golang.org/x/tools/gopls/internal/lsp/cache" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/span" @@ -41,7 +42,12 @@ func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source if err != nil { return nil, nil, err } - _, snapshot, release, err := s.session.NewView(ctx, name, uri, options) + folder := &cache.Folder{ + Dir: uri, + Name: name, + Options: options, + } + _, snapshot, release, err := s.session.NewView(ctx, folder) return snapshot, release, err } From 753c5d67214f90e9a65dcc2fc7a066b8787c23c9 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 1 Nov 2023 16:03:31 -0400 Subject: [PATCH 075/100] gopls/internal/lsp/cache: move 'contains' from snapshot to view For zero config gopls, it is philosophically important that we can map files to Views without looking at state (the snapshot). Now that View options are immutable, we can achieve this by migrating the 'contains' method from snapshot to View. This simplifies a couple call sites that acquired a snapshot only for querying containment. For golang/go#57979 Change-Id: Ief400ca8f0ff430c8992bd793df27ad47e7951cb Reviewed-on: https://go-review.googlesource.com/c/tools/+/538766 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/cache/load.go | 10 +++++----- gopls/internal/lsp/cache/session.go | 8 +------- gopls/internal/lsp/cache/view.go | 25 ++++++++++--------------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go index 4eae309bafb..7621f8198f4 100644 --- a/gopls/internal/lsp/cache/load.go +++ b/gopls/internal/lsp/cache/load.go @@ -160,7 +160,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc } moduleErrs := make(map[string][]packages.Error) // module path -> errors - filterFunc := s.filterFunc() + filterFunc := s.view.filterFunc() newMetadata := make(map[PackageID]*source.Metadata) for _, pkg := range pkgs { // The Go command returns synthetic list results for module queries that @@ -620,7 +620,7 @@ func containsPackageLocked(s *snapshot, m *source.Metadata) bool { uris[uri] = struct{}{} } - filterFunc := s.filterFunc() + filterFunc := s.view.filterFunc() for uri := range uris { // Don't use view.contains here. go.work files may include modules // outside of the workspace folder. @@ -631,7 +631,7 @@ func containsPackageLocked(s *snapshot, m *source.Metadata) bool { return false } - return containsFileInWorkspaceLocked(s, m) + return containsFileInWorkspaceLocked(s.view, m) } // containsOpenFileLocked reports whether any file referenced by m is open in @@ -660,7 +660,7 @@ func containsOpenFileLocked(s *snapshot, m *source.Metadata) bool { // workspace of the snapshot s. // // s.mu must be held while calling this function. -func containsFileInWorkspaceLocked(s *snapshot, m *source.Metadata) bool { +func containsFileInWorkspaceLocked(v *View, m *source.Metadata) bool { uris := map[span.URI]struct{}{} for _, uri := range m.CompiledGoFiles { uris[uri] = struct{}{} @@ -675,7 +675,7 @@ func containsFileInWorkspaceLocked(s *snapshot, m *source.Metadata) bool { // The package's files are in this view. It may be a workspace package. // Vendored packages are not likely to be interesting to the user. - if !strings.Contains(string(uri), "/vendor/") && s.contains(uri) { + if !strings.Contains(string(uri), "/vendor/") && v.contains(uri) { return true } } diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 6a6b7fe2aa6..4a03578e412 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -254,15 +254,9 @@ func bestViewForURI(uri span.URI, views []*View) *View { } // TODO(rfindley): this should consider the workspace layout (i.e. // go.work). - snapshot, release, err := view.getSnapshot() - if err != nil { - // view is shutdown - continue - } - if snapshot.contains(uri) { + if view.contains(uri) { longest = view } - release() } if longest != nil { return longest diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 81553d3cf4d..26889d071c9 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -502,7 +502,7 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) { } searched := 0 - filterFunc := s.filterFunc() + filterFunc := s.view.filterFunc() err := filepath.WalkDir(s.view.folder.Dir.Filename(), func(path string, entry os.DirEntry, err error) error { if err != nil { return err @@ -536,30 +536,30 @@ func (s *snapshot) locateTemplateFiles(ctx context.Context) { } } -func (s *snapshot) contains(uri span.URI) bool { +func (v *View) contains(uri span.URI) bool { // If we've expanded the go dir to a parent directory, consider if the // expanded dir contains the uri. // TODO(rfindley): should we ignore the root here? It is not provided by the // user. It would be better to explicitly consider the set of active modules // wherever relevant. inGoDir := false - if source.InDir(s.view.goCommandDir.Filename(), s.view.folder.Dir.Filename()) { - inGoDir = source.InDir(s.view.goCommandDir.Filename(), uri.Filename()) + if source.InDir(v.goCommandDir.Filename(), v.folder.Dir.Filename()) { + inGoDir = source.InDir(v.goCommandDir.Filename(), uri.Filename()) } - inFolder := source.InDir(s.view.folder.Dir.Filename(), uri.Filename()) + inFolder := source.InDir(v.folder.Dir.Filename(), uri.Filename()) if !inGoDir && !inFolder { return false } - return !s.filterFunc()(uri) + return !v.filterFunc()(uri) } // filterFunc returns a func that reports whether uri is filtered by the currently configured // directoryFilters. -func (s *snapshot) filterFunc() func(span.URI) bool { - folderDir := s.view.folder.Dir.Filename() - filterer := buildFilterer(folderDir, s.view.gomodcache, s.Options()) +func (v *View) filterFunc() func(span.URI) bool { + folderDir := v.folder.Dir.Filename() + filterer := buildFilterer(folderDir, v.gomodcache, v.folder.Options) return func(uri span.URI) bool { // Only filter relative to the configured root directory. if source.InDir(folderDir, uri.Filename()) { @@ -590,12 +590,7 @@ func (v *View) relevantChange(c source.FileModification) bool { // had neither test nor associated issue, and cited only emacs behavior, this // logic was deleted. - snapshot, release, err := v.getSnapshot() - if err != nil { - return false // view was shut down - } - defer release() - return snapshot.contains(c.URI) + return v.contains(c.URI) } func (v *View) markKnown(uri span.URI) { From e6864f1e201825ab6d960c4656686183add54d67 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 2 Nov 2023 10:23:26 -0400 Subject: [PATCH 076/100] go/ssa: hang instances off generic Function, not Program Details: - Each generic function has its own instances map (and mutex). We create this map at Function construction (which is factored out of createMemberFromObject in anticipation of the next CL). - Program.needsInstance is now Function.instance. - Remove fluff. - Move Program._Instances to allInstances in the tests. This is another step towards breaking up the big Program lock. Change-Id: I8c659b99fdf25392e3982931d4d791e096ac1ea5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539256 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI --- go/ssa/builder.go | 4 +- go/ssa/create.go | 86 +++++++++++++--------- go/ssa/instantiate.go | 147 +++++++++++-------------------------- go/ssa/instantiate_test.go | 35 +++++++-- go/ssa/methods.go | 2 +- go/ssa/ssa.go | 9 +-- go/ssa/util.go | 10 +++ go/ssa/wrappers.go | 4 +- 8 files changed, 140 insertions(+), 157 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index c86ce4f8535..471d10f4ad2 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -803,7 +803,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { callee := v.(*Function) // (func) if callee.typeparams.Len() > 0 { targs := fn.subst.types(instanceArgs(fn.info, e)) - callee = fn.Prog.needsInstance(callee, targs, b.created) + callee = callee.instance(targs, b.created) } return callee } @@ -999,7 +999,7 @@ func (b *builder) setCallFunc(fn *Function, e *ast.CallExpr, c *CallCommon) { // "Call"-mode call. callee := fn.Prog.originFunc(obj) if callee.typeparams.Len() > 0 { - callee = fn.Prog.needsInstance(callee, receiverTypeArgs(obj), b.created) + callee = callee.instance(receiverTypeArgs(obj), b.created) } c.Value = callee c.Args = append(c.Args, v) diff --git a/go/ssa/create.go b/go/ssa/create.go index 14367bff6c6..c1ae07d0d74 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -29,7 +29,6 @@ func NewProgram(fset *token.FileSet, mode BuilderMode) *Program { mode: mode, canon: newCanonizer(), ctxt: typeparams.NewContext(), - instances: make(map[*Function]*instanceSet), parameterized: tpWalker{seen: make(map[types.Type]bool)}, } } @@ -81,38 +80,8 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion pkg.ninit++ name = fmt.Sprintf("init#%d", pkg.ninit) } - - // Collect type parameters if this is a generic function/method. - var tparams *typeparams.TypeParamList - if rtparams := typeparams.RecvTypeParams(sig); rtparams.Len() > 0 { - tparams = rtparams - } else if sigparams := typeparams.ForSignature(sig); sigparams.Len() > 0 { - tparams = sigparams - } - - /* declared function/method (from syntax or export data) */ - fn := &Function{ - name: name, - object: obj, - Signature: sig, - build: (*builder).buildFromSyntax, - syntax: syntax, - pos: obj.Pos(), - Pkg: pkg, - Prog: pkg.Prog, - typeparams: tparams, - info: pkg.info, - goversion: goversion, - } - pkg.created.Add(fn) - if syntax == nil { - fn.Synthetic = "loaded from gc object file" - fn.build = (*builder).buildParamsOnly - } - if tparams.Len() > 0 { - fn.Prog.createInstanceSet(fn) - } - + fn := createFunction(pkg.Prog, obj, name, syntax, pkg.info, goversion, &pkg.created) + fn.Pkg = pkg pkg.objects[obj] = fn if sig.Recv() == nil { pkg.Members[name] = fn // package-level function @@ -123,6 +92,54 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion } } +// createFunction creates a function or method. +func createFunction(prog *Program, obj *types.Func, name string, syntax ast.Node, info *types.Info, goversion string, cr *creator) *Function { + sig := obj.Type().(*types.Signature) + + // Collect type parameters. + var tparams *typeparams.TypeParamList + if rtparams := typeparams.RecvTypeParams(sig); rtparams.Len() > 0 { + tparams = rtparams // method of generic type + } else if sigparams := typeparams.ForSignature(sig); sigparams.Len() > 0 { + tparams = sigparams // generic function + } + + /* declared function/method (from syntax or export data) */ + fn := &Function{ + name: name, + object: obj, + Signature: sig, + build: (*builder).buildFromSyntax, + syntax: syntax, + pos: obj.Pos(), + Pkg: nil, // may be set by caller + Prog: prog, + typeparams: tparams, + info: info, + goversion: goversion, + } + if fn.syntax == nil { + fn.Synthetic = "from type information" + fn.build = (*builder).buildParamsOnly + } + if tparams.Len() > 0 { + // TODO(adonovan): retain the syntax/info/goversion fields indefinitely + // (i.e. don't clear them after Package.Build). It was a premature + // optimization design to avoid keeping typed syntax live, but the + // typed syntax is always live for some other reason. + // Then 'generic' reduces to a set of instances. + fn.generic = &generic{ + origin: fn, + // Syntax fields may all be empty: + syntax: fn.syntax, + info: fn.info, + goversion: fn.goversion, + } + } + cr.Add(fn) + return fn +} + // membersFromDecl populates package pkg with members for each // typechecker object (var, func, const or type) associated with the // specified decl. @@ -191,6 +208,9 @@ func (c *creator) Len() int { return len(*c) } // The real work of building SSA form for each function is not done // until a subsequent call to Package.Build. func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info *types.Info, importable bool) *Package { + if pkg == nil { + panic("nil pkg") // otherwise pkg.Scope below returns types.Universe! + } p := &Package{ Prog: prog, Members: make(map[string]Member), diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index 7b004ac9494..bfcd001a1e3 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -8,124 +8,58 @@ import ( "fmt" "go/ast" "go/types" + "sync" "golang.org/x/tools/internal/typeparams" ) -// _Instances returns all of the instances generated by runtime types for this function in an unspecified order. -// -// Thread-safe. -// -// This is an experimental interface! It may change without warning. -// -// Acquires prog.methodsMu. -func (prog *Program) _Instances(fn *Function) []*Function { - if fn.typeparams.Len() == 0 || len(fn.typeargs) > 0 { - return nil - } - - prog.methodsMu.Lock() - defer prog.methodsMu.Unlock() - return prog.instances[fn].list() -} - -// A set of instantiations of a generic function fn. -type instanceSet struct { - fn *Function // fn.typeparams.Len() > 0 and len(fn.typeargs) == 0. - instances map[*typeList]*Function // canonical type arguments to an instance. - syntax *ast.FuncDecl // fn.syntax copy for instantiating after fn is done. nil on synthetic packages. - info *types.Info // fn.pkg.info copy for building after fn is done. nil on synthetic packages. - goversion string // goversion to build syntax with. - // TODO(taking): Consider ways to allow for clearing syntax and info when done building. - // May require a public API change as MethodValue can request these be built after prog.Build() is done. -} +// A generic records information about a generic origin function, +// including a cache of existing instantiations. +type generic struct { + origin *Function // generic origin; has typeparams but no typeargs -func (insts *instanceSet) list() []*Function { - if insts == nil { - return nil - } + instancesMu sync.Mutex + instances map[*typeList]*Function // canonical type arguments to an instance. - fns := make([]*Function, 0, len(insts.instances)) - for _, fn := range insts.instances { - fns = append(fns, fn) - } - return fns + // Syntax info saved from origin. Empty for a synthetic package. + syntax ast.Node // saved reference to FuncDecl + info *types.Info // type information + goversion string // goversion of syntax } -// createInstanceSet adds a new instanceSet for a generic function fn if one does not exist. +// instance returns a Function that is the instantiation of generic +// origin function fn with the type arguments targs. // -// Precondition: fn is a package level declaration (function or method). +// Any created instance is added to cr. // -// Acquires prog.methodsMu. -func (prog *Program) createInstanceSet(fn *Function) { - assert(fn.typeparams.Len() > 0 && len(fn.typeargs) == 0, "Can only create instance sets for generic functions") - - prog.methodsMu.Lock() - defer prog.methodsMu.Unlock() - - syntax, _ := fn.syntax.(*ast.FuncDecl) - assert((syntax == nil) == (fn.syntax == nil), "fn.syntax is either nil or a *ast.FuncDecl") - - if _, ok := prog.instances[fn]; !ok { - prog.instances[fn] = &instanceSet{ - fn: fn, - syntax: syntax, - info: fn.info, - goversion: fn.goversion, +// Acquires fn.generic.instancesMu. +func (fn *Function) instance(targs []types.Type, cr *creator) *Function { + key := fn.Prog.canon.List(targs) + + gen := fn.generic + + gen.instancesMu.Lock() + defer gen.instancesMu.Unlock() + inst, ok := gen.instances[key] + if !ok { + inst = createInstance(gen, targs, cr) + if gen.instances == nil { + gen.instances = make(map[*typeList]*Function) } + gen.instances[key] = inst } + return inst } -// needsInstance returns a Function that is the instantiation of fn with the type arguments targs. -// -// Any CREATEd instance is added to cr. -// -// Acquires prog.methodMu. -func (prog *Program) needsInstance(fn *Function, targs []types.Type, cr *creator) *Function { - prog.methodsMu.Lock() - defer prog.methodsMu.Unlock() - - return prog.lookupOrCreateInstance(fn, targs, cr) -} - -// lookupOrCreateInstance returns a Function that is the instantiation of fn with the type arguments targs. -// -// Any CREATEd instance is added to cr. -// -// Requires prog.methodMu. -func (prog *Program) lookupOrCreateInstance(fn *Function, targs []types.Type, cr *creator) *Function { - return prog.instances[fn].lookupOrCreate(targs, &prog.parameterized, cr) -} - -// lookupOrCreate returns the instantiation of insts.fn using targs. +// createInstance returns the instantiation of gen.origin using targs. // If the instantiation is created, this is added to cr. // -// Requires prog.methodMu. -func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWalker, cr *creator) *Function { - if insts.instances == nil { - insts.instances = make(map[*typeList]*Function) - } - - fn := insts.fn +// Requires gen.instancesMu. +func createInstance(gen *generic, targs []types.Type, cr *creator) *Function { + fn := gen.origin prog := fn.Prog - // canonicalize on a tuple of targs. Sig is not unique. - // - // func A[T any]() { - // var x T - // fmt.Println("%T", x) - // } - key := prog.canon.List(targs) - if inst, ok := insts.instances[key]; ok { - return inst - } - - // Create instance or instantiation wrapper. - var syntax ast.Node - if insts.syntax != nil { - syntax = ast.Node(insts.syntax) - } - + // Compute signature. var sig *types.Signature var obj *types.Func if recv := fn.Signature.Recv(); recv != nil { @@ -133,6 +67,7 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa obj = prog.canon.instantiateMethod(fn.object, targs, prog.ctxt) sig = obj.Type().(*types.Signature) } else { + // function instSig, err := typeparams.Instantiate(prog.ctxt, fn.Signature, targs, false) if err != nil { panic(err) @@ -145,14 +80,15 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa sig = prog.canon.Type(instance).(*types.Signature) } + // Choose strategy (instance or wrapper). var ( synthetic string subst *subster build buildFunc ) - if prog.mode&InstantiateGenerics != 0 && !parameterized.anyParameterized(targs) { + if prog.mode&InstantiateGenerics != 0 && !prog.parameterized.anyParameterized(targs) { synthetic = fmt.Sprintf("instance of %s", fn.Name()) - if syntax != nil { + if gen.syntax != nil { scope := typeparams.OriginMethod(obj).Scope() subst = makeSubster(prog.ctxt, scope, fn.typeparams, targs, false) build = (*builder).buildFromSyntax @@ -170,7 +106,7 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa object: obj, Signature: sig, Synthetic: synthetic, - syntax: syntax, + syntax: gen.syntax, build: build, topLevelOrigin: fn, pos: obj.Pos(), @@ -178,11 +114,10 @@ func (insts *instanceSet) lookupOrCreate(targs []types.Type, parameterized *tpWa Prog: fn.Prog, typeparams: fn.typeparams, // share with origin typeargs: targs, - info: insts.info, // on synthetic packages info is nil. + info: gen.info, // on synthetic packages info is nil. subst: subst, - goversion: insts.goversion, + goversion: gen.goversion, } cr.Add(instance) - insts.instances[key] = instance return instance } diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index a333b23ead1..9475a37467e 100644 --- a/go/ssa/instantiate_test.go +++ b/go/ssa/instantiate_test.go @@ -102,7 +102,7 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) var cr creator intSliceTyp := types.NewSlice(types.Typ[types.Int]) - instance := prog.needsInstance(meth, []types.Type{intSliceTyp}, &cr) + instance := meth.instance([]types.Type{intSliceTyp}, &cr) if len(cr) != 1 { t.Errorf("Expected first instance to create a function. got %d created functions", len(cr)) } @@ -112,20 +112,20 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) if len(instance.TypeArgs()) != 1 || !types.Identical(instance.TypeArgs()[0], intSliceTyp) { t.Errorf("Expected TypeArgs of %s to be %v. got %v", instance, []types.Type{intSliceTyp}, instance.typeargs) } - instances := prog._Instances(meth) + instances := allInstances(meth) if want := []*Function{instance}; !reflect.DeepEqual(instances, want) { t.Errorf("Expected instances of %s to be %v. got %v", meth, want, instances) } // A second request with an identical type returns the same Function. - second := prog.needsInstance(meth, []types.Type{types.NewSlice(types.Typ[types.Int])}, &cr) + second := meth.instance([]types.Type{types.NewSlice(types.Typ[types.Int])}, &cr) if second != instance || len(cr) != 1 { t.Error("Expected second identical instantiation to not create a function") } // Add a second instance. - inst2 := prog.needsInstance(meth, []types.Type{types.NewSlice(types.Typ[types.Uint])}, &cr) - instances = prog._Instances(meth) + inst2 := meth.instance([]types.Type{types.NewSlice(types.Typ[types.Uint])}, &cr) + instances = allInstances(meth) // Note: instance.Name() < inst2.Name() sort.Slice(instances, func(i, j int) bool { @@ -258,7 +258,7 @@ func entry(i int, a A) int { } func instanceOf(f *Function, name string, prog *Program) *Function { - for _, i := range prog._Instances(f) { + for _, i := range allInstances(f) { if i.Name() == name { return i } @@ -326,7 +326,6 @@ func Foo[T any, S any](t T, s S) { } p := buildPackage(lprog, "p", SanityCheckFunctions) - prog := p.Prog for _, test := range []struct { orig string @@ -341,7 +340,7 @@ func Foo[T any, S any](t T, s S) { t.Fatalf("origin function not found") } - instances := prog._Instances(f) + instances := allInstances(f) sort.Slice(instances, func(i, j int) bool { return instances[i].Name() < instances[j].Name() }) if got := fmt.Sprintf("%v", instances); !reflect.DeepEqual(got, test.instances) { @@ -350,3 +349,23 @@ func Foo[T any, S any](t T, s S) { }) } } + +// allInstances returns a new unordered array of all instances of the +// specified function, if generic, or nil otherwise. +// +// Thread-safe. +// +// TODO(adonovan): delete this. The tests should be intensional (e.g. +// "what instances of f are reachable?") not representational (e.g. +// "what is the history of calls to Function.instance?"). +// +// Acquires fn.generic.instancesMu. +func allInstances(fn *Function) []*Function { + if fn.generic == nil { + return nil + } + + fn.generic.instancesMu.Lock() + defer fn.generic.instancesMu.Unlock() + return mapValues(fn.generic.instances) +} diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 5c0c3eaa40c..1e0f27a2d7e 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -109,7 +109,7 @@ func (prog *Program) addMethod(mset *methodSet, sel *types.Selection, cr *creato fn = prog.originFunc(obj) if fn.typeparams.Len() > 0 { // instantiate targs := receiverTypeArgs(obj) - fn = prog.lookupOrCreateInstance(fn, targs, cr) + fn = fn.instance(targs, cr) } } if fn.Signature.Recv() == nil { diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 38c47965dc8..fe5967ebbe9 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -23,17 +23,15 @@ import ( type Program struct { Fset *token.FileSet // position information for the files of this Program imported map[string]*Package // all importable Packages, keyed by import path - packages map[*types.Package]*Package // all loaded Packages, keyed by object + packages map[*types.Package]*Package // all created Packages mode BuilderMode // set of mode bits for SSA construction MethodSets typeutil.MethodSetCache // cache of type-checker's method-sets canon *canonizer // type canonicalization map ctxt *typeparams.Context // cache for type checking instantiations - // TODO(adonovan): split this mutex. - methodsMu sync.Mutex // guards the following maps: - methodSets typeutil.Map // maps type to its concrete methodSet - instances map[*Function]*instanceSet // instances of generic functions + methodsMu sync.Mutex + methodSets typeutil.Map // maps type to its concrete *methodSet parameterized tpWalker // memoization of whether a type refers to type parameters @@ -335,6 +333,7 @@ type Function struct { typeparams *typeparams.TypeParamList // type parameters of this function. typeparams.Len() > 0 => generic or instance of generic function typeargs []types.Type // type arguments that instantiated typeparams. len(typeargs) > 0 => instance of generic function topLevelOrigin *Function // the origin function if this is an instance of a source function. nil if Parent()!=nil. + generic *generic // instances of this function, if generic // The following fields are set transiently during building, // then cleared. diff --git a/go/ssa/util.go b/go/ssa/util.go index c78cdbf13b3..63fbbc1282a 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go @@ -369,3 +369,13 @@ func (canon *canonizer) instantiateMethod(m *types.Func, targs []types.Type, ctx // Exposed to ssautil using the linkname hack. func isSyntactic(pkg *Package) bool { return pkg.syntax } + +// mapValues returns a new unordered array of map values. +func mapValues[K comparable, V any](m map[K]V) []V { + vals := make([]V, 0, len(m)) + for _, fn := range m { + vals = append(vals, fn) + } + return vals + +} diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index bd6257aaf99..2a1658abcf0 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -144,7 +144,7 @@ func (b *builder) buildWrapper(fn *Function) { callee := fn.Prog.originFunc(fn.object) if callee.typeparams.Len() > 0 { fn.Prog.methodsMu.Lock() - callee = fn.Prog.lookupOrCreateInstance(callee, receiverTypeArgs(fn.object), b.created) + callee = callee.instance(receiverTypeArgs(fn.object), b.created) fn.Prog.methodsMu.Unlock() } c.Call.Value = callee @@ -229,7 +229,7 @@ func (b *builder) buildBound(fn *Function) { callee := fn.Prog.originFunc(fn.object) if callee.typeparams.Len() > 0 { fn.Prog.methodsMu.Lock() - callee = fn.Prog.lookupOrCreateInstance(callee, receiverTypeArgs(fn.object), b.created) + callee = callee.instance(receiverTypeArgs(fn.object), b.created) fn.Prog.methodsMu.Unlock() } c.Call.Value = callee From 08edf75c4df409a46133447c4ad0a4cde62d6a73 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 2 Nov 2023 20:20:20 -0400 Subject: [PATCH 077/100] gopls/internal/regtest/marker: port half of the suggestedfix markers Port the fillstruct and self_assignment suggestedfix marker tests. The fillstruct marker test in particular had incredibly verbose golden content, which made the tests very difficult to read. To mitigate this, introduce a new 'codeactionedit' marker which only stores edits in the golden directory, rather than complete file content. Additionally, narrow the unified diff to have no edges, for brevity. Since none of the fillstruct tests require multi-line ranges, use a single location for the range. Furthermore, standardize on putting locations before action kind in code action markers. This is more consistent with other markers. For golang/go#54845 Change-Id: Id5d713b77fa751bfe8be473b19304376bc3bb139 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539655 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/analysis/internal/checker/checker.go | 4 +- .../lsp/analysis/fillstruct/fillstruct.go | 2 +- gopls/internal/lsp/cmd/cmd.go | 2 +- gopls/internal/lsp/regtest/marker.go | 96 ++- gopls/internal/lsp/regtest/wrappers.go | 12 + gopls/internal/lsp/testdata/fillstruct/a.go | 27 - .../lsp/testdata/fillstruct/a.go.golden | 126 ---- gopls/internal/lsp/testdata/fillstruct/a2.go | 29 - .../lsp/testdata/fillstruct/a2.go.golden | 139 ---- gopls/internal/lsp/testdata/fillstruct/a3.go | 42 -- .../lsp/testdata/fillstruct/a3.go.golden | 243 ------- gopls/internal/lsp/testdata/fillstruct/a4.go | 39 -- .../lsp/testdata/fillstruct/a4.go.golden | 174 ----- .../lsp/testdata/fillstruct/data/a.go | 6 - .../lsp/testdata/fillstruct/fill_struct.go | 26 - .../testdata/fillstruct/fill_struct.go.golden | 124 ---- .../testdata/fillstruct/fill_struct_anon.go | 14 - .../fillstruct/fill_struct_anon.go.golden | 20 - .../testdata/fillstruct/fill_struct_nested.go | 15 - .../fillstruct/fill_struct_nested.go.golden | 19 - .../fillstruct/fill_struct_package.go | 12 - .../fillstruct/fill_struct_package.go.golden | 36 - .../fillstruct/fill_struct_partial.go | 24 - .../fillstruct/fill_struct_partial.go.golden | 52 -- .../testdata/fillstruct/fill_struct_spaces.go | 9 - .../fillstruct/fill_struct_spaces.go.golden | 13 - .../testdata/fillstruct/fill_struct_unsafe.go | 12 - .../fillstruct/fill_struct_unsafe.go.golden | 17 - .../lsp/testdata/fillstruct/typeparams.go | 37 - .../testdata/fillstruct/typeparams.go.golden | 206 ------ .../suggestedfix/has_suggested_fix.go | 11 - .../suggestedfix/has_suggested_fix.go.golden | 13 - .../internal/lsp/testdata/summary.txt.golden | 2 +- .../testdata/codeaction/fill_struct.txt | 630 ++++++++++++++++++ .../codeaction/functionextraction.txt | 100 +-- .../functionextraction_issue44813.txt | 4 +- .../marker/testdata/codeaction/imports.txt | 28 +- .../testdata/codeaction/infertypeargs.txt | 4 +- .../marker/testdata/codeaction/inline.txt | 4 +- .../testdata/codeaction/removeparam.txt | 26 +- .../codeaction/removeparam_formatting.txt | 4 +- .../codeaction/removeparam_funcvalue.txt | 2 +- .../codeaction/removeparam_imports.txt | 8 +- .../testdata/suggestedfix/self_assignment.txt | 28 + .../{quickfix => suggestedfix}/undeclared.txt | 0 .../unusedrequire.txt | 0 .../unusedrequire_gowork.txt | 0 internal/analysisinternal/analysis.go | 2 +- internal/diff/diff_test.go | 2 +- internal/diff/difftest/difftest.go | 2 +- internal/diff/unified.go | 27 +- 51 files changed, 863 insertions(+), 1611 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/fillstruct/a.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/a.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/a2.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/a2.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/a3.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/a3.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/a4.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/a4.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/data/a.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden delete mode 100644 gopls/internal/lsp/testdata/fillstruct/typeparams.go delete mode 100644 gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden delete mode 100644 gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go delete mode 100644 gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden create mode 100644 gopls/internal/regtest/marker/testdata/codeaction/fill_struct.txt create mode 100644 gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt rename gopls/internal/regtest/marker/testdata/{quickfix => suggestedfix}/undeclared.txt (100%) rename gopls/internal/regtest/marker/testdata/{quickfix => suggestedfix}/unusedrequire.txt (100%) rename gopls/internal/regtest/marker/testdata/{quickfix => suggestedfix}/unusedrequire_gowork.txt (100%) diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index 33ca77a06c9..8efb89f6bd5 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -485,11 +485,11 @@ func diff3Conflict(path string, xlabel, ylabel string, xedits, yedits []diff.Edi } oldlabel, old := "base", string(contents) - xdiff, err := diff.ToUnified(oldlabel, xlabel, old, xedits) + xdiff, err := diff.ToUnified(oldlabel, xlabel, old, xedits, diff.DefaultContextLines) if err != nil { return err } - ydiff, err := diff.ToUnified(oldlabel, ylabel, old, yedits) + ydiff, err := diff.ToUnified(oldlabel, ylabel, old, yedits, diff.DefaultContextLines) if err != nil { return err } diff --git a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go index 6d145cf3304..3b87ce5b0f9 100644 --- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go @@ -357,7 +357,7 @@ func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { case u.Kind() == types.UnsafePointer: return ast.NewIdent("nil") default: - panic("unknown basic type") + panic(fmt.Sprintf("unknown basic type %v", u)) } case *types.Map: diff --git a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go index d3d4069a412..714a5bf4baa 100644 --- a/gopls/internal/lsp/cmd/cmd.go +++ b/gopls/internal/lsp/cmd/cmd.go @@ -610,7 +610,7 @@ func applyTextEdits(mapper *protocol.Mapper, edits []protocol.TextEdit, flags *E } if flags.Diff { - unified, err := diff.ToUnified(filename+".orig", filename, string(mapper.Content), renameEdits) + unified, err := diff.ToUnified(filename+".orig", filename, string(mapper.Content), renameEdits, diff.DefaultContextLines) if err != nil { return err } diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index a7dee5e15f1..d07c6d070e3 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -36,6 +36,7 @@ import ( "golang.org/x/tools/gopls/internal/lsp/source" "golang.org/x/tools/gopls/internal/lsp/tests" "golang.org/x/tools/gopls/internal/lsp/tests/compare" + "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/testenv" @@ -152,12 +153,17 @@ var update = flag.Bool("update", false, "if set, update test data during marker // completion candidate produced at the given location with provided label // results in the given golden state. // -// - codeaction(kind, start, end, golden): specifies a codeaction to request +// - codeaction(start, end, kind, golden): specifies a code action to request // for the given range. To support multi-line ranges, the range is defined // to be between start.Start and end.End. The golden directory contains // changed file content after the code action is applied. // -// - codeactionerr(kind, start, end, wantError): specifies a codeaction that +// - codeactionedit(range, kind, golden): a shorter form of codeaction. +// Invokes a code action of the given kind for the given in-line range, and +// compares the resulting formatted unified *edits* (notably, not the full +// file content) with the golden directory. +// +// - codeactionerr(start, end, kind, wantError): specifies a codeaction that // fails with an error that matches the expectation. // // - codelens(location, title): specifies that a codelens is expected at the @@ -243,6 +249,9 @@ var update = flag.Bool("update", false, "if set, update test data during marker // to have exactly one associated code action of the specified kind. // This action is executed for its editing effects on the source files. // Like rename, the golden directory contains the expected transformed files. +// TODO(rfindley): we probably only need 'suggestedfix' for quick-fixes. All +// other actions should use codeaction markers. In that case, we can remove +// the 'kind' parameter. // // - rank(location, ...completionItem): executes a textDocument/completion // request at the given location, and verifies that each expected @@ -708,6 +717,7 @@ var valueMarkerFuncs = map[string]func(marker){ var actionMarkerFuncs = map[string]func(marker){ "acceptcompletion": actionMarkerFunc(acceptCompletionMarker), "codeaction": actionMarkerFunc(codeActionMarker), + "codeactionedit": actionMarkerFunc(codeActionEditMarker), "codeactionerr": actionMarkerFunc(codeActionErrMarker), "codelenses": actionMarkerFunc(codeLensesMarker), "complete": actionMarkerFunc(completeMarker), @@ -1420,6 +1430,41 @@ func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) { } } +// checkDiffs computes unified diffs for each changed file, and compares with +// the diff content stored in the given golden directory. +func checkDiffs(mark marker, changed map[string][]byte, golden *Golden) { + diffs := make(map[string]string) + for name, after := range changed { + before := mark.run.env.FileContent(name) + edits := diff.Strings(before, string(after)) + d, err := diff.ToUnified("before", "after", before, edits, 0) + if err != nil { + // Can't happen: edits are consistent. + log.Fatalf("internal error in diff.ToUnified: %v", err) + } + diffs[name] = d + } + // Check changed files match expectations. + for filename, got := range diffs { + if want, ok := golden.Get(mark.run.env.T, filename, []byte(got)); !ok { + mark.errorf("%s: unexpected change to file %s; got diff:\n%s", + mark.note.Name, filename, got) + + } else if got != string(want) { + mark.errorf("%s: wrong diff for %s:\n\ngot:\n%s\n\nwant:\n%s\n", + mark.note.Name, filename, got, want) + } + } + // Report unmet expectations. + for filename := range golden.data { + if _, ok := changed[filename]; !ok { + want, _ := golden.Get(mark.run.env.T, filename, nil) + mark.errorf("%s: missing change to file %s; want:\n%s", + mark.note.Name, filename, want) + } + } +} + // ---- marker functions ---- // TODO(rfindley): consolidate documentation of these markers. They are already @@ -1887,7 +1932,7 @@ func applyDocumentChanges(env *Env, changes []protocol.DocumentChanges, fileChan return nil } -func codeActionMarker(mark marker, actionKind string, start, end protocol.Location, golden *Golden) { +func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden) { // Request the range from start.Start to end.End. loc := start loc.Range.End = end.Range.End @@ -1900,10 +1945,20 @@ func codeActionMarker(mark marker, actionKind string, start, end protocol.Locati } // Check the file state. - checkChangedFiles(mark, changed, golden) + checkChangedFiles(mark, changed, g) } -func codeActionErrMarker(mark marker, actionKind string, start, end protocol.Location, wantErr wantError) { +func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden) { + changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil) + if err != nil { + mark.errorf("codeAction failed: %v", err) + return + } + + checkDiffs(mark, changed, g) +} + +func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr wantError) { loc := start loc.Range.End = end.Range.End _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil) @@ -2008,6 +2063,21 @@ func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, a // applied. Currently, this function does not support code actions that return // edits directly; it only supports code action commands. func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic) (map[string][]byte, error) { + changes, err := codeActionChanges(env, uri, rng, actionKind, diag) + if err != nil { + return nil, err + } + fileChanges := make(map[string][]byte) + if err := applyDocumentChanges(env, changes, fileChanges); err != nil { + return nil, fmt.Errorf("applying document changes: %v", err) + } + return fileChanges, nil +} + +// codeActionChanges executes a textDocument/codeAction request for the +// specified location and kind, and captures the resulting document changes. +// If diag is non-nil, it is used as the code action context. +func codeActionChanges(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic) ([]protocol.DocumentChanges, error) { // Request all code actions that apply to the diagnostic. // (The protocol supports filtering using Context.Only={actionKind} // but we can give a better error if we don't filter.) @@ -2047,20 +2117,19 @@ func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKi // Spec: // "If a code action provides an edit and a command, first the edit is // executed and then the command." - fileChanges := make(map[string][]byte) // An action may specify an edit and/or a command, to be // applied in that order. But since applyDocumentChanges(env, // action.Edit.DocumentChanges) doesn't compose, for now we - // assert that all commands used in the @suggestedfix tests - // return only a command. + // assert that actions return one or the other. if action.Edit != nil { if action.Edit.Changes != nil { env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title) } if action.Edit.DocumentChanges != nil { - if err := applyDocumentChanges(env, action.Edit.DocumentChanges, fileChanges); err != nil { - return nil, fmt.Errorf("applying document changes: %v", err) + if action.Command != nil { + env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Command", action.Kind, action.Title) } + return action.Edit.DocumentChanges, nil } } @@ -2085,13 +2154,10 @@ func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKi }); err != nil { return nil, err } - - if err := applyDocumentChanges(env, env.Awaiter.takeDocumentChanges(), fileChanges); err != nil { - return nil, fmt.Errorf("applying document changes from command: %v", err) - } + return env.Awaiter.takeDocumentChanges(), nil } - return fileChanges, nil + return nil, nil } // TODO(adonovan): suggestedfixerr diff --git a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go index 0220d30d390..a2c6a1eea7e 100644 --- a/gopls/internal/lsp/regtest/wrappers.go +++ b/gopls/internal/lsp/regtest/wrappers.go @@ -114,6 +114,18 @@ func (e *Env) SetBufferContent(name string, content string) { } } +// ReadFile returns the file content for name that applies to the current +// editing session: if the file is open, it returns its buffer content, +// otherwise it returns on disk content. +func (e *Env) FileContent(name string) string { + e.T.Helper() + text, ok := e.Editor.BufferText(name) + if ok { + return text + } + return e.ReadWorkspaceFile(name) +} + // RegexpSearch returns the starting position of the first match for re in the // buffer specified by name, calling t.Fatal on any error. It first searches // for the position in open buffers, then in workspace files. diff --git a/gopls/internal/lsp/testdata/fillstruct/a.go b/gopls/internal/lsp/testdata/fillstruct/a.go deleted file mode 100644 index e1add2d4713..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/a.go +++ /dev/null @@ -1,27 +0,0 @@ -package fillstruct - -import ( - "golang.org/lsptests/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/gopls/internal/lsp/testdata/fillstruct/a.go.golden b/gopls/internal/lsp/testdata/fillstruct/a.go.golden deleted file mode 100644 index ca1db04ead8..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/a.go.golden +++ /dev/null @@ -1,126 +0,0 @@ --- suggestedfix_a_11_21 -- -package fillstruct - -import ( - "golang.org/lsptests/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{ - foo: 0, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a_18_22 -- -package fillstruct - -import ( - "golang.org/lsptests/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{ - foo: 0, - bar: "", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a_25_22 -- -package fillstruct - -import ( - "golang.org/lsptests/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{ - bar: "", - basic: basicStruct{}, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a_27_16 -- -package fillstruct - -import ( - "golang.org/lsptests/fillstruct/data" -) - -type basicStruct struct { - foo int -} - -var _ = basicStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStruct struct { - foo int - bar string -} - -var _ = twoArgStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStruct struct { - bar string - basic basicStruct -} - -var _ = nestedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = data.B{ - ExportedInt: 0, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - diff --git a/gopls/internal/lsp/testdata/fillstruct/a2.go b/gopls/internal/lsp/testdata/fillstruct/a2.go deleted file mode 100644 index b5e30a84f1e..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/a2.go +++ /dev/null @@ -1,29 +0,0 @@ -package fillstruct - -type typedStruct struct { - m map[string]int - s []int - c chan int - c1 <-chan int - a [2]string -} - -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStruct struct { - fn func(i int) int -} - -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructCompex struct { - fn func(i int, s string) (string, int) -} - -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructEmpty struct { - fn func() -} - -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/gopls/internal/lsp/testdata/fillstruct/a2.go.golden b/gopls/internal/lsp/testdata/fillstruct/a2.go.golden deleted file mode 100644 index 2eca3e349a1..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/a2.go.golden +++ /dev/null @@ -1,139 +0,0 @@ --- suggestedfix_a2_11_21 -- -package fillstruct - -type typedStruct struct { - m map[string]int - s []int - c chan int - c1 <-chan int - a [2]string -} - -var _ = typedStruct{ - m: map[string]int{}, - s: []int{}, - c: make(chan int), - c1: make(<-chan int), - a: [2]string{}, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStruct struct { - fn func(i int) int -} - -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructCompex struct { - fn func(i int, s string) (string, int) -} - -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructEmpty struct { - fn func() -} - -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a2_17_19 -- -package fillstruct - -type typedStruct struct { - m map[string]int - s []int - c chan int - c1 <-chan int - a [2]string -} - -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStruct struct { - fn func(i int) int -} - -var _ = funStruct{ - fn: func(i int) int { - }, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructCompex struct { - fn func(i int, s string) (string, int) -} - -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructEmpty struct { - fn func() -} - -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a2_23_25 -- -package fillstruct - -type typedStruct struct { - m map[string]int - s []int - c chan int - c1 <-chan int - a [2]string -} - -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStruct struct { - fn func(i int) int -} - -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructCompex struct { - fn func(i int, s string) (string, int) -} - -var _ = funStructCompex{ - fn: func(i int, s string) (string, int) { - }, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructEmpty struct { - fn func() -} - -var _ = funStructEmpty{} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a2_29_24 -- -package fillstruct - -type typedStruct struct { - m map[string]int - s []int - c chan int - c1 <-chan int - a [2]string -} - -var _ = typedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStruct struct { - fn func(i int) int -} - -var _ = funStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructCompex struct { - fn func(i int, s string) (string, int) -} - -var _ = funStructCompex{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type funStructEmpty struct { - fn func() -} - -var _ = funStructEmpty{ - fn: func() { - }, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - diff --git a/gopls/internal/lsp/testdata/fillstruct/a3.go b/gopls/internal/lsp/testdata/fillstruct/a3.go deleted file mode 100644 index 59cd9fa28b5..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/a3.go +++ /dev/null @@ -1,42 +0,0 @@ -package fillstruct - -import ( - "go/ast" - "go/token" -) - -type Foo struct { - A int -} - -type Bar struct { - X *Foo - Y *Foo -} - -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type importedStruct struct { - m map[*ast.CompositeLit]ast.Field - s []ast.BadExpr - a [3]token.Token - c chan ast.EmptyStmt - fn func(ast_decl ast.DeclStmt) ast.Ellipsis - st ast.CompositeLit -} - -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type pointerBuiltinStruct struct { - b *bool - s *string - i *int -} - -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite", "Fill") -} - -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") diff --git a/gopls/internal/lsp/testdata/fillstruct/a3.go.golden b/gopls/internal/lsp/testdata/fillstruct/a3.go.golden deleted file mode 100644 index a7c7baa8d27..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/a3.go.golden +++ /dev/null @@ -1,243 +0,0 @@ --- suggestedfix_a3_17_13 -- -package fillstruct - -import ( - "go/ast" - "go/token" -) - -type Foo struct { - A int -} - -type Bar struct { - X *Foo - Y *Foo -} - -var _ = Bar{ - X: &Foo{}, - Y: &Foo{}, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type importedStruct struct { - m map[*ast.CompositeLit]ast.Field - s []ast.BadExpr - a [3]token.Token - c chan ast.EmptyStmt - fn func(ast_decl ast.DeclStmt) ast.Ellipsis - st ast.CompositeLit -} - -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type pointerBuiltinStruct struct { - b *bool - s *string - i *int -} - -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite", "Fill") -} - -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a3_28_24 -- -package fillstruct - -import ( - "go/ast" - "go/token" -) - -type Foo struct { - A int -} - -type Bar struct { - X *Foo - Y *Foo -} - -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type importedStruct struct { - m map[*ast.CompositeLit]ast.Field - s []ast.BadExpr - a [3]token.Token - c chan ast.EmptyStmt - fn func(ast_decl ast.DeclStmt) ast.Ellipsis - st ast.CompositeLit -} - -var _ = importedStruct{ - m: map[*ast.CompositeLit]ast.Field{}, - s: []ast.BadExpr{}, - a: [3]token.Token{}, - c: make(chan ast.EmptyStmt), - fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { - }, - st: ast.CompositeLit{}, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type pointerBuiltinStruct struct { - b *bool - s *string - i *int -} - -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite", "Fill") -} - -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a3_36_30 -- -package fillstruct - -import ( - "go/ast" - "go/token" -) - -type Foo struct { - A int -} - -type Bar struct { - X *Foo - Y *Foo -} - -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type importedStruct struct { - m map[*ast.CompositeLit]ast.Field - s []ast.BadExpr - a [3]token.Token - c chan ast.EmptyStmt - fn func(ast_decl ast.DeclStmt) ast.Ellipsis - st ast.CompositeLit -} - -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type pointerBuiltinStruct struct { - b *bool - s *string - i *int -} - -var _ = pointerBuiltinStruct{ - b: new(bool), - s: new(string), - i: new(int), -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite", "Fill") -} - -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a3_39_3 -- -package fillstruct - -import ( - "go/ast" - "go/token" -) - -type Foo struct { - A int -} - -type Bar struct { - X *Foo - Y *Foo -} - -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type importedStruct struct { - m map[*ast.CompositeLit]ast.Field - s []ast.BadExpr - a [3]token.Token - c chan ast.EmptyStmt - fn func(ast_decl ast.DeclStmt) ast.Ellipsis - st ast.CompositeLit -} - -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type pointerBuiltinStruct struct { - b *bool - s *string - i *int -} - -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = []ast.BasicLit{ - { - ValuePos: 0, - Kind: 0, - Value: "", - }, //@suggestedfix("}", "refactor.rewrite", "Fill") -} - -var _ = []ast.BasicLit{{}} //@suggestedfix("}", "refactor.rewrite", "Fill") - --- suggestedfix_a3_42_25 -- -package fillstruct - -import ( - "go/ast" - "go/token" -) - -type Foo struct { - A int -} - -type Bar struct { - X *Foo - Y *Foo -} - -var _ = Bar{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type importedStruct struct { - m map[*ast.CompositeLit]ast.Field - s []ast.BadExpr - a [3]token.Token - c chan ast.EmptyStmt - fn func(ast_decl ast.DeclStmt) ast.Ellipsis - st ast.CompositeLit -} - -var _ = importedStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type pointerBuiltinStruct struct { - b *bool - s *string - i *int -} - -var _ = pointerBuiltinStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = []ast.BasicLit{ - {}, //@suggestedfix("}", "refactor.rewrite", "Fill") -} - -var _ = []ast.BasicLit{{ - ValuePos: 0, - Kind: 0, - Value: "", -}} //@suggestedfix("}", "refactor.rewrite", "Fill") - diff --git a/gopls/internal/lsp/testdata/fillstruct/a4.go b/gopls/internal/lsp/testdata/fillstruct/a4.go deleted file mode 100644 index 5f52a55fa72..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/a4.go +++ /dev/null @@ -1,39 +0,0 @@ -package fillstruct - -import "go/ast" - -type iStruct struct { - X int -} - -type sStruct struct { - str string -} - -type multiFill struct { - num int - strin string - arr []int -} - -type assignStruct struct { - n ast.Node -} - -func fill() { - var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var n int - _ = []int{} - if true { - arr := []int{1, 2} - } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} diff --git a/gopls/internal/lsp/testdata/fillstruct/a4.go.golden b/gopls/internal/lsp/testdata/fillstruct/a4.go.golden deleted file mode 100644 index b1e376f05f1..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/a4.go.golden +++ /dev/null @@ -1,174 +0,0 @@ --- suggestedfix_a4_25_18 -- -package fillstruct - -import "go/ast" - -type iStruct struct { - X int -} - -type sStruct struct { - str string -} - -type multiFill struct { - num int - strin string - arr []int -} - -type assignStruct struct { - n ast.Node -} - -func fill() { - var x int - var _ = iStruct{ - X: x, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - - var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var n int - _ = []int{} - if true { - arr := []int{1, 2} - } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_a4_28_18 -- -package fillstruct - -import "go/ast" - -type iStruct struct { - X int -} - -type sStruct struct { - str string -} - -type multiFill struct { - num int - strin string - arr []int -} - -type assignStruct struct { - n ast.Node -} - -func fill() { - var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var s string - var _ = sStruct{ - str: s, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - - var n int - _ = []int{} - if true { - arr := []int{1, 2} - } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_a4_35_20 -- -package fillstruct - -import "go/ast" - -type iStruct struct { - X int -} - -type sStruct struct { - str string -} - -type multiFill struct { - num int - strin string - arr []int -} - -type assignStruct struct { - n ast.Node -} - -func fill() { - var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var n int - _ = []int{} - if true { - arr := []int{1, 2} - } - var _ = multiFill{ - num: n, - strin: s, - arr: []int{}, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - - var node *ast.CompositeLit - var _ = assignStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_a4_38_23 -- -package fillstruct - -import "go/ast" - -type iStruct struct { - X int -} - -type sStruct struct { - str string -} - -type multiFill struct { - num int - strin string - arr []int -} - -type assignStruct struct { - n ast.Node -} - -func fill() { - var x int - var _ = iStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var s string - var _ = sStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var n int - _ = []int{} - if true { - arr := []int{1, 2} - } - var _ = multiFill{} //@suggestedfix("}", "refactor.rewrite", "Fill") - - var node *ast.CompositeLit - var _ = assignStruct{ - n: node, - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} - diff --git a/gopls/internal/lsp/testdata/fillstruct/data/a.go b/gopls/internal/lsp/testdata/fillstruct/data/a.go deleted file mode 100644 index 7ca37736bd1..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/data/a.go +++ /dev/null @@ -1,6 +0,0 @@ -package data - -type B struct { - ExportedInt int - unexportedInt int -} diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct.go deleted file mode 100644 index 3da904741d0..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct.go +++ /dev/null @@ -1,26 +0,0 @@ -package fillstruct - -type StructA struct { - unexportedIntField int - ExportedIntField int - MapA map[int]string - Array []int - StructB -} - -type StructA2 struct { - B *StructB -} - -type StructA3 struct { - B StructB -} - -func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") - if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") - } -} diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden deleted file mode 100644 index de01a40f052..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct.go.golden +++ /dev/null @@ -1,124 +0,0 @@ --- suggestedfix_fill_struct_20_15 -- -package fillstruct - -type StructA struct { - unexportedIntField int - ExportedIntField int - MapA map[int]string - Array []int - StructB -} - -type StructA2 struct { - B *StructB -} - -type StructA3 struct { - B StructB -} - -func fill() { - a := StructA{ - unexportedIntField: 0, - ExportedIntField: 0, - MapA: map[int]string{}, - Array: []int{}, - StructB: StructB{}, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") - if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") - } -} - --- suggestedfix_fill_struct_21_16 -- -package fillstruct - -type StructA struct { - unexportedIntField int - ExportedIntField int - MapA map[int]string - Array []int - StructB -} - -type StructA2 struct { - B *StructB -} - -type StructA3 struct { - B StructB -} - -func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") - b := StructA2{ - B: &StructB{}, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") - if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") - } -} - --- suggestedfix_fill_struct_22_16 -- -package fillstruct - -type StructA struct { - unexportedIntField int - ExportedIntField int - MapA map[int]string - Array []int - StructB -} - -type StructA2 struct { - B *StructB -} - -type StructA3 struct { - B StructB -} - -func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") - c := StructA3{ - B: StructB{}, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - if true { - _ = StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") - } -} - --- suggestedfix_fill_struct_24_16 -- -package fillstruct - -type StructA struct { - unexportedIntField int - ExportedIntField int - MapA map[int]string - Array []int - StructB -} - -type StructA2 struct { - B *StructB -} - -type StructA3 struct { - B StructB -} - -func fill() { - a := StructA{} //@suggestedfix("}", "refactor.rewrite", "Fill") - b := StructA2{} //@suggestedfix("}", "refactor.rewrite", "Fill") - c := StructA3{} //@suggestedfix("}", "refactor.rewrite", "Fill") - if true { - _ = StructA3{ - B: StructB{}, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - } -} - diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go deleted file mode 100644 index 2c099a80ea7..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go +++ /dev/null @@ -1,14 +0,0 @@ -package fillstruct - -type StructAnon struct { - a struct{} - b map[string]interface{} - c map[string]struct { - d int - e bool - } -} - -func fill() { - _ := StructAnon{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden deleted file mode 100644 index 7cc9ac23d02..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_anon.go.golden +++ /dev/null @@ -1,20 +0,0 @@ --- suggestedfix_fill_struct_anon_13_18 -- -package fillstruct - -type StructAnon struct { - a struct{} - b map[string]interface{} - c map[string]struct { - d int - e bool - } -} - -func fill() { - _ := StructAnon{ - a: struct{}{}, - b: map[string]interface{}{}, - c: map[string]struct{d int; e bool}{}, - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} - diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go deleted file mode 100644 index ab7be5a7b58..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go +++ /dev/null @@ -1,15 +0,0 @@ -package fillstruct - -type StructB struct { - StructC -} - -type StructC struct { - unexportedInt int -} - -func nested() { - c := StructB{ - StructC: StructC{}, //@suggestedfix("}", "refactor.rewrite", "Fill") - } -} diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden deleted file mode 100644 index c902ee7f12b..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_nested.go.golden +++ /dev/null @@ -1,19 +0,0 @@ --- suggestedfix_fill_struct_nested_13_20 -- -package fillstruct - -type StructB struct { - StructC -} - -type StructC struct { - unexportedInt int -} - -func nested() { - c := StructB{ - StructC: StructC{ - unexportedInt: 0, - }, //@suggestedfix("}", "refactor.rewrite", "Fill") - } -} - diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go deleted file mode 100644 index ef35627c8ea..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go +++ /dev/null @@ -1,12 +0,0 @@ -package fillstruct - -import ( - h2 "net/http" - - "golang.org/lsptests/fillstruct/data" -) - -func unexported() { - a := data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") - _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden deleted file mode 100644 index 0cdbfc820ba..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_package.go.golden +++ /dev/null @@ -1,36 +0,0 @@ --- suggestedfix_fill_struct_package_10_14 -- -package fillstruct - -import ( - h2 "net/http" - - "golang.org/lsptests/fillstruct/data" -) - -func unexported() { - a := data.B{ - ExportedInt: 0, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - _ = h2.Client{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_fill_struct_package_11_16 -- -package fillstruct - -import ( - h2 "net/http" - - "golang.org/lsptests/fillstruct/data" -) - -func unexported() { - a := data.B{} //@suggestedfix("}", "refactor.rewrite", "Fill") - _ = h2.Client{ - Transport: nil, - CheckRedirect: func(req *h2.Request, via []*h2.Request) error { - }, - Jar: nil, - Timeout: 0, - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} - diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go deleted file mode 100644 index 5de1722c783..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go +++ /dev/null @@ -1,24 +0,0 @@ -package fillstruct - -type StructPartialA struct { - PrefilledInt int - UnfilledInt int - StructPartialB -} - -type StructPartialB struct { - PrefilledInt int - UnfilledInt int -} - -func fill() { - a := StructPartialA{ - PrefilledInt: 5, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - b := StructPartialB{ - /* this comment should disappear */ - PrefilledInt: 7, // This comment should be blown away. - /* As should - this one */ - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden deleted file mode 100644 index 3aa437a0334..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_partial.go.golden +++ /dev/null @@ -1,52 +0,0 @@ --- suggestedfix_fill_struct_partial_17_2 -- -package fillstruct - -type StructPartialA struct { - PrefilledInt int - UnfilledInt int - StructPartialB -} - -type StructPartialB struct { - PrefilledInt int - UnfilledInt int -} - -func fill() { - a := StructPartialA{ - PrefilledInt: 5, - UnfilledInt: 0, - StructPartialB: StructPartialB{}, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - b := StructPartialB{ - /* this comment should disappear */ - PrefilledInt: 7, // This comment should be blown away. - /* As should - this one */ - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_fill_struct_partial_23_2 -- -package fillstruct - -type StructPartialA struct { - PrefilledInt int - UnfilledInt int - StructPartialB -} - -type StructPartialB struct { - PrefilledInt int - UnfilledInt int -} - -func fill() { - a := StructPartialA{ - PrefilledInt: 5, - } //@suggestedfix("}", "refactor.rewrite", "Fill") - b := StructPartialB{ - PrefilledInt: 7, - UnfilledInt: 0, - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} - diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go deleted file mode 100644 index 6a468cd544c..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go +++ /dev/null @@ -1,9 +0,0 @@ -package fillstruct - -type StructD struct { - ExportedIntField int -} - -func spaces() { - d := StructD{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden deleted file mode 100644 index 590c91611d0..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_spaces.go.golden +++ /dev/null @@ -1,13 +0,0 @@ --- suggestedfix_fill_struct_spaces_8_15 -- -package fillstruct - -type StructD struct { - ExportedIntField int -} - -func spaces() { - d := StructD{ - ExportedIntField: 0, - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} - diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go b/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go deleted file mode 100644 index f5e42a4f2fe..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go +++ /dev/null @@ -1,12 +0,0 @@ -package fillstruct - -import "unsafe" - -type unsafeStruct struct { - x int - p unsafe.Pointer -} - -func fill() { - _ := unsafeStruct{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} diff --git a/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden b/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden deleted file mode 100644 index 7e8e1952f86..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/fill_struct_unsafe.go.golden +++ /dev/null @@ -1,17 +0,0 @@ --- suggestedfix_fill_struct_unsafe_11_20 -- -package fillstruct - -import "unsafe" - -type unsafeStruct struct { - x int - p unsafe.Pointer -} - -func fill() { - _ := unsafeStruct{ - x: 0, - p: nil, - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} - diff --git a/gopls/internal/lsp/testdata/fillstruct/typeparams.go b/gopls/internal/lsp/testdata/fillstruct/typeparams.go deleted file mode 100644 index c0b702f57c7..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/typeparams.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build go1.18 -// +build go1.18 - -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} // no suggested fix - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -func _[T any]() { - type S struct{ t T } - _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} diff --git a/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden b/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden deleted file mode 100644 index 625df7577b7..00000000000 --- a/gopls/internal/lsp/testdata/fillstruct/typeparams.go.golden +++ /dev/null @@ -1,206 +0,0 @@ --- suggestedfix_typeparams_14_40 -- -//go:build go1.18 -// +build go1.18 - -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} // no suggested fix - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{ - foo: 0, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -func _[T any]() { - type S struct{ t T } - _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_typeparams_21_49 -- -//go:build go1.18 -// +build go1.18 - -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} // no suggested fix - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{ - foo: "", - bar: 0, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -func _[T any]() { - type S struct{ t T } - _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_typeparams_25_1 -- -//go:build go1.18 -// +build go1.18 - -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} // no suggested fix - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - foo: 0, - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -func _[T any]() { - type S struct{ t T } - _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_typeparams_32_36 -- -//go:build go1.18 -// +build go1.18 - -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} // no suggested fix - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{ - bar: "", - basic: basicStructWithTypeParams{}, -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -func _[T any]() { - type S struct{ t T } - _ = S{} //@suggestedfix("}", "refactor.rewrite", "Fill") -} - --- suggestedfix_typeparams_36_8 -- -//go:build go1.18 -// +build go1.18 - -package fillstruct - -type emptyStructWithTypeParams[A any] struct{} - -var _ = emptyStructWithTypeParams[int]{} // no suggested fix - -type basicStructWithTypeParams[T any] struct { - foo T -} - -var _ = basicStructWithTypeParams[int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type twoArgStructWithTypeParams[F, B any] struct { - foo F - bar B -} - -var _ = twoArgStructWithTypeParams[string, int]{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -var _ = twoArgStructWithTypeParams[int, string]{ - bar: "bar", -} //@suggestedfix("}", "refactor.rewrite", "Fill") - -type nestedStructWithTypeParams struct { - bar string - basic basicStructWithTypeParams[int] -} - -var _ = nestedStructWithTypeParams{} //@suggestedfix("}", "refactor.rewrite", "Fill") - -func _[T any]() { - type S struct{ t T } - _ = S{ - t: *new(T), - } //@suggestedfix("}", "refactor.rewrite", "Fill") -} - diff --git a/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go b/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go deleted file mode 100644 index 7ff524479b4..00000000000 --- a/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go +++ /dev/null @@ -1,11 +0,0 @@ -package suggestedfix - -import ( - "log" -) - -func goodbye() { - s := "hiiiiiii" - s = s //@suggestedfix("s = s", "quickfix", "") - log.Print(s) -} diff --git a/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden b/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden deleted file mode 100644 index e7e84fc227d..00000000000 --- a/gopls/internal/lsp/testdata/suggestedfix/has_suggested_fix.go.golden +++ /dev/null @@ -1,13 +0,0 @@ --- suggestedfix_has_suggested_fix_9_2 -- -package suggestedfix - -import ( - "log" -) - -func goodbye() { - s := "hiiiiiii" - //@suggestedfix("s = s", "quickfix", "") - log.Print(s) -} - diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 5489921f8d6..2f610b5f2ba 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,7 +1,7 @@ -- summary -- CallHierarchyCount = 2 SemanticTokenCount = 3 -SuggestedFixCount = 80 +SuggestedFixCount = 45 MethodExtractionCount = 8 InlayHintsCount = 5 RenamesCount = 45 diff --git a/gopls/internal/regtest/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/regtest/marker/testdata/codeaction/fill_struct.txt new file mode 100644 index 00000000000..c5398ead279 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/fill_struct.txt @@ -0,0 +1,630 @@ +This test checks the behavior of the 'fill struct' code action. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/fillstruct + +go 1.18 + +-- data/data.go -- +package data + +type B struct { + ExportedInt int + unexportedInt int +} + +-- a.go -- +package fillstruct + +import ( + "golang.org/lsptests/fillstruct/data" +) + +type basicStruct struct { + foo int +} + +var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) + +type twoArgStruct struct { + foo int + bar string +} + +var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) + +type nestedStruct struct { + bar string + basic basicStruct +} + +var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) + +var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +-- @a1/a.go -- +--- before ++++ after +@@ -11 +11,3 @@ +-var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) ++var _ = basicStruct{ ++ foo: 0, ++} //@codeactionedit("}", "refactor.rewrite", a1) +-- @a2/a.go -- +--- before ++++ after +@@ -18 +18,4 @@ +-var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) ++var _ = twoArgStruct{ ++ foo: 0, ++ bar: "", ++} //@codeactionedit("}", "refactor.rewrite", a2) +-- @a3/a.go -- +--- before ++++ after +@@ -25 +25,4 @@ +-var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) ++var _ = nestedStruct{ ++ bar: "", ++ basic: basicStruct{}, ++} //@codeactionedit("}", "refactor.rewrite", a3) +-- @a4/a.go -- +--- before ++++ after +@@ -27 +27,3 @@ +-var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) ++var _ = data.B{ ++ ExportedInt: 0, ++} //@codeactionedit("}", "refactor.rewrite", a4) +-- a2.go -- +package fillstruct + +type typedStruct struct { + m map[string]int + s []int + c chan int + c1 <-chan int + a [2]string +} + +var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) + +type funStruct struct { + fn func(i int) int +} + +var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) + +type funStructCompex struct { + fn func(i int, s string) (string, int) +} + +var _ = funStructCompex{} //@codeactionedit("}", "refactor.rewrite", a23) + +type funStructEmpty struct { + fn func() +} + +var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) + +-- @a21/a2.go -- +--- before ++++ after +@@ -11 +11,7 @@ +-var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) ++var _ = typedStruct{ ++ m: map[string]int{}, ++ s: []int{}, ++ c: make(chan int), ++ c1: make(<-chan int), ++ a: [2]string{}, ++} //@codeactionedit("}", "refactor.rewrite", a21) +-- @a22/a2.go -- +--- before ++++ after +@@ -17 +17,4 @@ +-var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) ++var _ = funStruct{ ++ fn: func(i int) int { ++ }, ++} //@codeactionedit("}", "refactor.rewrite", a22) +-- @a23/a2.go -- +--- before ++++ after +@@ -23 +23,4 @@ +-var _ = funStructCompex{} //@codeactionedit("}", "refactor.rewrite", a23) ++var _ = funStructCompex{ ++ fn: func(i int, s string) (string, int) { ++ }, ++} //@codeactionedit("}", "refactor.rewrite", a23) +-- @a24/a2.go -- +--- before ++++ after +@@ -29 +29,4 @@ +-var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) ++var _ = funStructEmpty{ ++ fn: func() { ++ }, ++} //@codeactionedit("}", "refactor.rewrite", a24) +-- a3.go -- +package fillstruct + +import ( + "go/ast" + "go/token" +) + +type Foo struct { + A int +} + +type Bar struct { + X *Foo + Y *Foo +} + +var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) + +type importedStruct struct { + m map[*ast.CompositeLit]ast.Field + s []ast.BadExpr + a [3]token.Token + c chan ast.EmptyStmt + fn func(ast_decl ast.DeclStmt) ast.Ellipsis + st ast.CompositeLit +} + +var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) + +type pointerBuiltinStruct struct { + b *bool + s *string + i *int +} + +var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) + +var _ = []ast.BasicLit{ + {}, //@codeactionedit("}", "refactor.rewrite", a34) +} + +var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +-- @a31/a3.go -- +--- before ++++ after +@@ -17 +17,4 @@ +-var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) ++var _ = Bar{ ++ X: &Foo{}, ++ Y: &Foo{}, ++} //@codeactionedit("}", "refactor.rewrite", a31) +-- @a32/a3.go -- +--- before ++++ after +@@ -28 +28,9 @@ +-var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) ++var _ = importedStruct{ ++ m: map[*ast.CompositeLit]ast.Field{}, ++ s: []ast.BadExpr{}, ++ a: [3]token.Token{}, ++ c: make(chan ast.EmptyStmt), ++ fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { ++ }, ++ st: ast.CompositeLit{}, ++} //@codeactionedit("}", "refactor.rewrite", a32) +-- @a33/a3.go -- +--- before ++++ after +@@ -36 +36,5 @@ +-var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) ++var _ = pointerBuiltinStruct{ ++ b: new(bool), ++ s: new(string), ++ i: new(int), ++} //@codeactionedit("}", "refactor.rewrite", a33) +-- @a34/a3.go -- +--- before ++++ after +@@ -39 +39,5 @@ +- {}, //@codeactionedit("}", "refactor.rewrite", a34) ++ { ++ ValuePos: 0, ++ Kind: 0, ++ Value: "", ++ }, //@codeactionedit("}", "refactor.rewrite", a34) +-- @a35/a3.go -- +--- before ++++ after +@@ -42 +42,5 @@ +-var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) ++var _ = []ast.BasicLit{{ ++ ValuePos: 0, ++ Kind: 0, ++ Value: "", ++}} //@codeactionedit("}", "refactor.rewrite", a35) +-- a4.go -- +package fillstruct + +import "go/ast" + +type iStruct struct { + X int +} + +type sStruct struct { + str string +} + +type multiFill struct { + num int + strin string + arr []int +} + +type assignStruct struct { + n ast.Node +} + +func fill() { + var x int + var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) + + var s string + var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) + + var n int + _ = []int{} + if true { + arr := []int{1, 2} + } + var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) + + var node *ast.CompositeLit + var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) +} + +-- @a41/a4.go -- +--- before ++++ after +@@ -25 +25,3 @@ +- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) ++ var _ = iStruct{ ++ X: x, ++ } //@codeactionedit("}", "refactor.rewrite", a41) +-- @a42/a4.go -- +--- before ++++ after +@@ -28 +28,3 @@ +- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) ++ var _ = sStruct{ ++ str: s, ++ } //@codeactionedit("}", "refactor.rewrite", a42) +-- @a43/a4.go -- +--- before ++++ after +@@ -35 +35,5 @@ +- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) ++ var _ = multiFill{ ++ num: n, ++ strin: s, ++ arr: []int{}, ++ } //@codeactionedit("}", "refactor.rewrite", a43) +-- @a45/a4.go -- +--- before ++++ after +@@ -38 +38,3 @@ +- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) ++ var _ = assignStruct{ ++ n: node, ++ } //@codeactionedit("}", "refactor.rewrite", a45) +-- fill_struct.go -- +package fillstruct + +type StructA struct { + unexportedIntField int + ExportedIntField int + MapA map[int]string + Array []int + StructB +} + +type StructA2 struct { + B *StructB +} + +type StructA3 struct { + B StructB +} + +func fill() { + a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) + b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) + c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) + if true { + _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) + } +} + +-- @fill_struct1/fill_struct.go -- +--- before ++++ after +@@ -20 +20,7 @@ +- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) ++ a := StructA{ ++ unexportedIntField: 0, ++ ExportedIntField: 0, ++ MapA: map[int]string{}, ++ Array: []int{}, ++ StructB: StructB{}, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct1) +-- @fill_struct2/fill_struct.go -- +--- before ++++ after +@@ -21 +21,3 @@ +- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) ++ b := StructA2{ ++ B: &StructB{}, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct2) +-- @fill_struct3/fill_struct.go -- +--- before ++++ after +@@ -22 +22,3 @@ +- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) ++ c := StructA3{ ++ B: StructB{}, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct3) +-- @fill_struct4/fill_struct.go -- +--- before ++++ after +@@ -24 +24,3 @@ +- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) ++ _ = StructA3{ ++ B: StructB{}, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct4) +-- fill_struct_anon.go -- +package fillstruct + +type StructAnon struct { + a struct{} + b map[string]interface{} + c map[string]struct { + d int + e bool + } +} + +func fill() { + _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +} +-- @fill_struct_anon/fill_struct_anon.go -- +--- before ++++ after +@@ -13 +13,5 @@ +- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) ++ _ := StructAnon{ ++ a: struct{}{}, ++ b: map[string]interface{}{}, ++ c: map[string]struct{d int; e bool}{}, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +-- fill_struct_nested.go -- +package fillstruct + +type StructB struct { + StructC +} + +type StructC struct { + unexportedInt int +} + +func nested() { + c := StructB{ + StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) + } +} + +-- @fill_nested/fill_struct_nested.go -- +--- before ++++ after +@@ -13 +13,3 @@ +- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) ++ StructC: StructC{ ++ unexportedInt: 0, ++ }, //@codeactionedit("}", "refactor.rewrite", fill_nested) +-- fill_struct_package.go -- +package fillstruct + +import ( + h2 "net/http" + + "golang.org/lsptests/fillstruct/data" +) + +func unexported() { + a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) + _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +} +-- @fill_struct_package1/fill_struct_package.go -- +--- before ++++ after +@@ -10 +10,3 @@ +- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) ++ a := data.B{ ++ ExportedInt: 0, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +-- @fill_struct_package2/fill_struct_package.go -- +--- before ++++ after +@@ -11 +11,7 @@ +- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) ++ _ = h2.Client{ ++ Transport: nil, ++ CheckRedirect: func(req *h2.Request, via []*h2.Request) error { ++ }, ++ Jar: nil, ++ Timeout: 0, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +-- fill_struct_partial.go -- +package fillstruct + +type StructPartialA struct { + PrefilledInt int + UnfilledInt int + StructPartialB +} + +type StructPartialB struct { + PrefilledInt int + UnfilledInt int +} + +func fill() { + a := StructPartialA{ + PrefilledInt: 5, + } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1) + b := StructPartialB{ + /* this comment should disappear */ + PrefilledInt: 7, // This comment should be blown away. + /* As should + this one */ + } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2) +} + +-- @fill_struct_partial1/fill_struct_partial.go -- +--- before ++++ after +@@ -16 +16,3 @@ +- PrefilledInt: 5, ++ PrefilledInt: 5, ++ UnfilledInt: 0, ++ StructPartialB: StructPartialB{}, +-- @fill_struct_partial2/fill_struct_partial.go -- +--- before ++++ after +@@ -19,4 +19,2 @@ +- /* this comment should disappear */ ++ PrefilledInt: 7, +- PrefilledInt: 7, // This comment should be blown away. +- /* As should +- this one */ ++ UnfilledInt: 0, +-- fill_struct_spaces.go -- +package fillstruct + +type StructD struct { + ExportedIntField int +} + +func spaces() { + d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +} + +-- @fill_struct_spaces/fill_struct_spaces.go -- +--- before ++++ after +@@ -8 +8,3 @@ +- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) ++ d := StructD{ ++ ExportedIntField: 0, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +-- fill_struct_unsafe.go -- +package fillstruct + +import "unsafe" + +type unsafeStruct struct { + x int + p unsafe.Pointer +} + +func fill() { + _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +} + +-- @fill_struct_unsafe/fill_struct_unsafe.go -- +--- before ++++ after +@@ -11 +11,4 @@ +- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) ++ _ := unsafeStruct{ ++ x: 0, ++ p: nil, ++ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +-- typeparams.go -- +package fillstruct + +type emptyStructWithTypeParams[A any] struct{} + +var _ = emptyStructWithTypeParams[int]{} // no suggested fix + +type basicStructWithTypeParams[T any] struct { + foo T +} + +var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) + +type twoArgStructWithTypeParams[F, B any] struct { + foo F + bar B +} + +var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) + +var _ = twoArgStructWithTypeParams[int, string]{ + bar: "bar", +} //@codeactionedit("}", "refactor.rewrite", typeparams3) + +type nestedStructWithTypeParams struct { + bar string + basic basicStructWithTypeParams[int] +} + +var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) + +func _[T any]() { + type S struct{ t T } + _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +} +-- @typeparams1/typeparams.go -- +--- before ++++ after +@@ -11 +11,3 @@ +-var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) ++var _ = basicStructWithTypeParams[int]{ ++ foo: 0, ++} //@codeactionedit("}", "refactor.rewrite", typeparams1) +-- @typeparams2/typeparams.go -- +--- before ++++ after +@@ -18 +18,4 @@ +-var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) ++var _ = twoArgStructWithTypeParams[string, int]{ ++ foo: "", ++ bar: 0, ++} //@codeactionedit("}", "refactor.rewrite", typeparams2) +-- @typeparams3/typeparams.go -- +--- before ++++ after +@@ -20 +20,2 @@ +-var _ = twoArgStructWithTypeParams[int, string]{ ++var _ = twoArgStructWithTypeParams[int, string]{ ++ foo: 0, +-- @typeparams4/typeparams.go -- +--- before ++++ after +@@ -29 +29,4 @@ +-var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) ++var _ = nestedStructWithTypeParams{ ++ bar: "", ++ basic: basicStructWithTypeParams{}, ++} //@codeactionedit("}", "refactor.rewrite", typeparams4) +-- @typeparams5/typeparams.go -- +--- before ++++ after +@@ -33 +33,3 @@ +- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) ++ _ = S{ ++ t: *new(T), ++ } //@codeactionedit("}", "refactor.rewrite", typeparams5) diff --git a/gopls/internal/regtest/marker/testdata/codeaction/functionextraction.txt b/gopls/internal/regtest/marker/testdata/codeaction/functionextraction.txt index d5bd9869d16..b37009c78d9 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/functionextraction.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/functionextraction.txt @@ -8,16 +8,16 @@ go 1.18 -- basic.go -- package extract -func _() { //@codeaction("refactor.extract", "{", closeBracket, outer) - a := 1 //@codeaction("refactor.extract", "a", end, inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) + a := 1 //@codeaction("a", end, "refactor.extract", inner) _ = a + 4 //@loc(end, "4") } //@loc(closeBracket, "}") -- @inner/basic.go -- package extract -func _() { //@codeaction("refactor.extract", "{", closeBracket, outer) - //@codeaction("refactor.extract", "a", end, inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) + //@codeaction("a", end, "refactor.extract", inner) newFunction() //@loc(end, "4") } @@ -29,8 +29,8 @@ func newFunction() { -- @outer/basic.go -- package extract -func _() { //@codeaction("refactor.extract", "{", closeBracket, outer) - //@codeaction("refactor.extract", "a", end, inner) +func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) + //@codeaction("a", end, "refactor.extract", inner) newFunction() //@loc(end, "4") } @@ -44,7 +44,7 @@ package extract func _() bool { x := 1 - if x == 0 { //@codeaction("refactor.extract", "if", ifend, return) + if x == 0 { //@codeaction("if", ifend, "refactor.extract", return) return true } //@loc(ifend, "}") return false @@ -55,7 +55,7 @@ package extract func _() bool { x := 1 - //@codeaction("refactor.extract", "if", ifend, return) + //@codeaction("if", ifend, "refactor.extract", return) shouldReturn, returnValue := newFunction(x) if shouldReturn { return returnValue @@ -74,7 +74,7 @@ func newFunction(x int) (bool, bool) { package extract func _() bool { - x := 1 //@codeaction("refactor.extract", "x", rnnEnd, rnn) + x := 1 //@codeaction("x", rnnEnd, "refactor.extract", rnn) if x == 0 { return true } @@ -85,7 +85,7 @@ func _() bool { package extract func _() bool { - //@codeaction("refactor.extract", "x", rnnEnd, rnn) + //@codeaction("x", rnnEnd, "refactor.extract", rnn) return newFunction() //@loc(rnnEnd, "false") } @@ -105,7 +105,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - z := "bye" //@codeaction("refactor.extract", "z", rcEnd, rc) + z := "bye" //@codeaction("z", rcEnd, "refactor.extract", rc) if y == z { return x, y, fmt.Errorf("same") } else if false { @@ -123,7 +123,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - //@codeaction("refactor.extract", "z", rcEnd, rc) + //@codeaction("z", rcEnd, "refactor.extract", rc) z, shouldReturn, returnValue, returnValue1, returnValue2 := newFunction(y, x) if shouldReturn { return returnValue, returnValue1, returnValue2 @@ -150,7 +150,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - z := "bye" //@codeaction("refactor.extract", "z", rcnnEnd, rcnn) + z := "bye" //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) if y == z { return x, y, fmt.Errorf("same") } else if false { @@ -168,7 +168,7 @@ import "fmt" func _() (int, string, error) { x := 1 y := "hello" - //@codeaction("refactor.extract", "z", rcnnEnd, rcnn) + //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) return newFunction(y, x) //@loc(rcnnEnd, "nil") } @@ -190,7 +190,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - if n == nil { //@codeaction("refactor.extract", "if", rflEnd, rfl) + if n == nil { //@codeaction("if", rflEnd, "refactor.extract", rfl) return true } //@loc(rflEnd, "}") return false @@ -204,7 +204,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - //@codeaction("refactor.extract", "if", rflEnd, rfl) + //@codeaction("if", rflEnd, "refactor.extract", rfl) shouldReturn, returnValue := newFunction(n) if shouldReturn { return returnValue @@ -227,7 +227,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - if n == nil { //@codeaction("refactor.extract", "if", rflnnEnd, rflnn) + if n == nil { //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) return true } return false //@loc(rflnnEnd, "false") @@ -241,7 +241,7 @@ import "go/ast" func _() { ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { - //@codeaction("refactor.extract", "if", rflnnEnd, rflnn) + //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) return newFunction(n) //@loc(rflnnEnd, "false") }) } @@ -258,7 +258,7 @@ package extract func _() string { x := 1 - if x == 0 { //@codeaction("refactor.extract", "if", riEnd, ri) + if x == 0 { //@codeaction("if", riEnd, "refactor.extract", ri) x = 3 return "a" } //@loc(riEnd, "}") @@ -271,7 +271,7 @@ package extract func _() string { x := 1 - //@codeaction("refactor.extract", "if", riEnd, ri) + //@codeaction("if", riEnd, "refactor.extract", ri) shouldReturn, returnValue := newFunction(x) if shouldReturn { return returnValue @@ -293,7 +293,7 @@ package extract func _() string { x := 1 - if x == 0 { //@codeaction("refactor.extract", "if", rinnEnd, rinn) + if x == 0 { //@codeaction("if", rinnEnd, "refactor.extract", rinn) x = 3 return "a" } @@ -306,7 +306,7 @@ package extract func _() string { x := 1 - //@codeaction("refactor.extract", "if", rinnEnd, rinn) + //@codeaction("if", rinnEnd, "refactor.extract", rinn) return newFunction(x) //@loc(rinnEnd, "\"b\"") } @@ -324,10 +324,10 @@ package extract func _() { a := 1 - a = 5 //@codeaction("refactor.extract", "a", araend, ara) + a = 5 //@codeaction("a", araend, "refactor.extract", ara) a = a + 2 //@loc(araend, "2") - b := a * 2 //@codeaction("refactor.extract", "b", arbend, arb) + b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) _ = b + 4 //@loc(arbend, "4") } @@ -336,10 +336,10 @@ package extract func _() { a := 1 - //@codeaction("refactor.extract", "a", araend, ara) + //@codeaction("a", araend, "refactor.extract", ara) a = newFunction(a) //@loc(araend, "2") - b := a * 2 //@codeaction("refactor.extract", "b", arbend, arb) + b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) _ = b + 4 //@loc(arbend, "4") } @@ -354,10 +354,10 @@ package extract func _() { a := 1 - a = 5 //@codeaction("refactor.extract", "a", araend, ara) + a = 5 //@codeaction("a", araend, "refactor.extract", ara) a = a + 2 //@loc(araend, "2") - //@codeaction("refactor.extract", "b", arbend, arb) + //@codeaction("b", arbend, "refactor.extract", arb) newFunction(a) //@loc(arbend, "4") } @@ -371,7 +371,7 @@ package extract func _() { newFunction := 1 - a := newFunction //@codeaction("refactor.extract", "a", "newFunction", scope) + a := newFunction //@codeaction("a", "newFunction", "refactor.extract", scope) _ = a // avoid diagnostic } @@ -384,7 +384,7 @@ package extract func _() { newFunction := 1 - a := newFunction2(newFunction) //@codeaction("refactor.extract", "a", "newFunction", scope) + a := newFunction2(newFunction) //@codeaction("a", "newFunction", "refactor.extract", scope) _ = a // avoid diagnostic } @@ -402,7 +402,7 @@ package extract func _() { var a []int - a = append(a, 2) //@codeaction("refactor.extract", "a", siEnd, si) + a = append(a, 2) //@codeaction("a", siEnd, "refactor.extract", si) b := 4 //@loc(siEnd, "4") a = append(a, b) } @@ -412,7 +412,7 @@ package extract func _() { var a []int - //@codeaction("refactor.extract", "a", siEnd, si) + //@codeaction("a", siEnd, "refactor.extract", si) a, b := newFunction(a) //@loc(siEnd, "4") a = append(a, b) } @@ -429,7 +429,7 @@ package extract func _() { var b []int var a int - a = 2 //@codeaction("refactor.extract", "a", srEnd, sr) + a = 2 //@codeaction("a", srEnd, "refactor.extract", sr) b = []int{} b = append(b, a) //@loc(srEnd, ")") b[0] = 1 @@ -441,7 +441,7 @@ package extract func _() { var b []int var a int - //@codeaction("refactor.extract", "a", srEnd, sr) + //@codeaction("a", srEnd, "refactor.extract", sr) b = newFunction(a, b) //@loc(srEnd, ")") b[0] = 1 } @@ -458,7 +458,7 @@ package extract func _() { var b []int - a := 2 //@codeaction("refactor.extract", "a", upEnd, up) + a := 2 //@codeaction("a", upEnd, "refactor.extract", up) b = []int{} b = append(b, a) //@loc(upEnd, ")") b[0] = 1 @@ -472,7 +472,7 @@ package extract func _() { var b []int - //@codeaction("refactor.extract", "a", upEnd, up) + //@codeaction("a", upEnd, "refactor.extract", up) a, b := newFunction(b) //@loc(upEnd, ")") b[0] = 1 if a == 2 { @@ -491,9 +491,9 @@ func newFunction(b []int) (int, []int) { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("refactor.extract", "a", commentEnd, comment1) - // Comment on its own line //@codeaction("refactor.extract", "Comment", commentEnd, comment2) - _ = a + 4 //@loc(commentEnd, "4"),codeaction("refactor.extract", "_", lastComment, comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) + _ = a + 4 //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -504,9 +504,9 @@ package extract func _() { /* comment in the middle of a line */ - //@codeaction("refactor.extract", "a", commentEnd, comment1) - // Comment on its own line //@codeaction("refactor.extract", "Comment", commentEnd, comment2) - newFunction() //@loc(commentEnd, "4"),codeaction("refactor.extract", "_", lastComment, comment3) + //@codeaction("a", commentEnd, "refactor.extract", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) + newFunction() //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -522,9 +522,9 @@ func newFunction() { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("refactor.extract", "a", commentEnd, comment1) - // Comment on its own line //@codeaction("refactor.extract", "Comment", commentEnd, comment2) - newFunction(a) //@loc(commentEnd, "4"),codeaction("refactor.extract", "_", lastComment, comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) + newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -538,9 +538,9 @@ func newFunction(a int) { package extract func _() { - a := /* comment in the middle of a line */ 1 //@codeaction("refactor.extract", "a", commentEnd, comment1) - // Comment on its own line //@codeaction("refactor.extract", "Comment", commentEnd, comment2) - newFunction(a) //@loc(commentEnd, "4"),codeaction("refactor.extract", "_", lastComment, comment3) + a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) + // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) + newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) // Comment right after 3 + 4 // Comment after with space //@loc(lastComment, "Comment") @@ -557,7 +557,7 @@ import "strconv" func _() { i, err := strconv.Atoi("1") - u, err := strconv.Atoi("2") //@codeaction("refactor.extract", "u", ")", redefine) + u, err := strconv.Atoi("2") //@codeaction("u", ")", "refactor.extract", redefine) if i == u || err == nil { return } @@ -570,7 +570,7 @@ import "strconv" func _() { i, err := strconv.Atoi("1") - u, err := newFunction() //@codeaction("refactor.extract", "u", ")", redefine) + u, err := newFunction() //@codeaction("u", ")", "refactor.extract", redefine) if i == u || err == nil { return } diff --git a/gopls/internal/regtest/marker/testdata/codeaction/functionextraction_issue44813.txt b/gopls/internal/regtest/marker/testdata/codeaction/functionextraction_issue44813.txt index 46369d0a30c..cadc8e94263 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/functionextraction_issue44813.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/functionextraction_issue44813.txt @@ -12,7 +12,7 @@ package extract import "fmt" func main() { - x := []rune{} //@codeaction("refactor.extract", "x", end, ext) + x := []rune{} //@codeaction("x", end, "refactor.extract", ext) s := "HELLO" for _, c := range s { x = append(x, c) @@ -26,7 +26,7 @@ package extract import "fmt" func main() { - //@codeaction("refactor.extract", "x", end, ext) + //@codeaction("x", end, "refactor.extract", ext) x := newFunction() //@loc(end, "}") fmt.Printf("%x\n", x) } diff --git a/gopls/internal/regtest/marker/testdata/codeaction/imports.txt b/gopls/internal/regtest/marker/testdata/codeaction/imports.txt index 325733ec86d..3d058fb36a1 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/imports.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/imports.txt @@ -6,7 +6,7 @@ module mod.test/imports go 1.18 -- add.go -- -package imports //@codeaction("source.organizeImports", "imports", "", add) +package imports //@codeaction("imports", "", "source.organizeImports", add) import ( "fmt" @@ -18,7 +18,7 @@ func _() { } -- @add/add.go -- -package imports //@codeaction("source.organizeImports", "imports", "", add) +package imports //@codeaction("imports", "", "source.organizeImports", add) import ( "bytes" @@ -31,7 +31,7 @@ func _() { } -- good.go -- -package imports //@codeactionerr("source.organizeImports", "imports", "", re"found 0 CodeActions") +package imports //@codeactionerr("imports", "", "source.organizeImports", re"found 0 CodeActions") import "fmt" @@ -46,7 +46,7 @@ fmt.Println("") // package doc -package imports //@codeaction("source.organizeImports", "imports", "", issue35458) +package imports //@codeaction("imports", "", "source.organizeImports", issue35458) @@ -66,7 +66,7 @@ func _() { -- @issue35458/issue35458.go -- // package doc -package imports //@codeaction("source.organizeImports", "imports", "", issue35458) +package imports //@codeaction("imports", "", "source.organizeImports", issue35458) @@ -85,7 +85,7 @@ func _() { -- multi.go -- -package imports //@codeaction("source.organizeImports", "imports", "", multi) +package imports //@codeaction("imports", "", "source.organizeImports", multi) import "fmt" @@ -96,7 +96,7 @@ func _() { } -- @multi/multi.go -- -package imports //@codeaction("source.organizeImports", "imports", "", multi) +package imports //@codeaction("imports", "", "source.organizeImports", multi) import "fmt" @@ -107,7 +107,7 @@ func _() { } -- needs.go -- -package imports //@codeaction("source.organizeImports", "package", "", needs) +package imports //@codeaction("package", "", "source.organizeImports", needs) func goodbye() { fmt.Printf("HI") //@diag("fmt", re"(undeclared|undefined)") @@ -115,7 +115,7 @@ func goodbye() { } -- @needs/needs.go -- -package imports //@codeaction("source.organizeImports", "package", "", needs) +package imports //@codeaction("package", "", "source.organizeImports", needs) import ( "fmt" @@ -128,7 +128,7 @@ func goodbye() { } -- remove.go -- -package imports //@codeaction("source.organizeImports", "package", "", remove) +package imports //@codeaction("package", "", "source.organizeImports", remove) import ( "bytes" //@diag("\"bytes\"", re"not used") @@ -140,7 +140,7 @@ func _() { } -- @remove/remove.go -- -package imports //@codeaction("source.organizeImports", "package", "", remove) +package imports //@codeaction("package", "", "source.organizeImports", remove) import ( "fmt" @@ -151,7 +151,7 @@ func _() { } -- removeall.go -- -package imports //@codeaction("source.organizeImports", "package", "", removeall) +package imports //@codeaction("package", "", "source.organizeImports", removeall) import ( "bytes" //@diag("\"bytes\"", re"not used") @@ -163,7 +163,7 @@ func _() { } -- @removeall/removeall.go -- -package imports //@codeaction("source.organizeImports", "package", "", removeall) +package imports //@codeaction("package", "", "source.organizeImports", removeall) //@diag("\"fmt\"", re"not used") @@ -172,4 +172,4 @@ func _() { -- twolines.go -- package imports -func main() {} //@codeactionerr("source.organizeImports", "main", "", re"found 0") +func main() {} //@codeactionerr("main", "", "source.organizeImports", re"found 0") diff --git a/gopls/internal/regtest/marker/testdata/codeaction/infertypeargs.txt b/gopls/internal/regtest/marker/testdata/codeaction/infertypeargs.txt index 8ee1b67ff56..6f7b5fbe8c0 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/infertypeargs.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/infertypeargs.txt @@ -18,7 +18,7 @@ func app[S interface{ ~[]E }, E interface{}](s S, e E) S { func _() { _ = app[[]int] _ = app[[]int, int] - _ = app[[]int]([]int{}, 0) //@codeaction("refactor.rewrite", "app", ")", infer) + _ = app[[]int]([]int{}, 0) //@codeaction("app", ")", "refactor.rewrite", infer) _ = app([]int{}, 0) } @@ -32,7 +32,7 @@ func app[S interface{ ~[]E }, E interface{}](s S, e E) S { func _() { _ = app[[]int] _ = app[[]int, int] - _ = app([]int{}, 0) //@codeaction("refactor.rewrite", "app", ")", infer) + _ = app([]int{}, 0) //@codeaction("app", ")", "refactor.rewrite", infer) _ = app([]int{}, 0) } diff --git a/gopls/internal/regtest/marker/testdata/codeaction/inline.txt b/gopls/internal/regtest/marker/testdata/codeaction/inline.txt index 15d3cabfcc8..813a69ce09c 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/inline.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/inline.txt @@ -8,7 +8,7 @@ go 1.18 package a func _() { - println(add(1, 2)) //@codeaction("refactor.inline", "add", ")", inline) + println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline) } func add(x, y int) int { return x + y } @@ -17,7 +17,7 @@ func add(x, y int) int { return x + y } package a func _() { - println(1 + 2) //@codeaction("refactor.inline", "add", ")", inline) + println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline) } func add(x, y int) int { return x + y } diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt index 7caa660babe..ad2289284d8 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt @@ -8,14 +8,14 @@ go 1.18 -- a/a.go -- package a -func A(x, unused int) int { //@codeaction("refactor.rewrite", "unused", "unused", a) +func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) return x } -- @a/a/a.go -- package a -func A(x int) int { //@codeaction("refactor.rewrite", "unused", "unused", a) +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) return x } @@ -98,7 +98,7 @@ func _() { -- field/field.go -- package field -func Field(x int, field int) { //@codeaction("refactor.rewrite", "int", "int", field) +func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field) } func _() { @@ -107,7 +107,7 @@ func _() { -- @field/field/field.go -- package field -func Field(field int) { //@codeaction("refactor.rewrite", "int", "int", field) +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field) } func _() { @@ -116,7 +116,7 @@ func _() { -- ellipsis/ellipsis.go -- package ellipsis -func Ellipsis(...any) { //@codeaction("refactor.rewrite", "any", "any", ellipsis) +func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis) } func _() { @@ -137,7 +137,7 @@ func i() []any -- @ellipsis/ellipsis/ellipsis.go -- package ellipsis -func Ellipsis() { //@codeaction("refactor.rewrite", "any", "any", ellipsis) +func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis) } func _() { @@ -161,7 +161,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) } func _() { @@ -175,7 +175,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) } func _() { @@ -190,7 +190,7 @@ func h() (int, int) -- overlapping/overlapping.go -- package overlapping -func Overlapping(i int) int { //@codeactionerr("refactor.rewrite", re"(i) int", re"(i) int", re"overlapping") +func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") return 0 } @@ -202,7 +202,7 @@ func _() { -- effects/effects.go -- package effects -func effects(x, y int) int { //@codeaction("refactor.rewrite", "y", "y", effects) +func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite", effects) return x } @@ -216,7 +216,7 @@ func _() { -- @effects/effects/effects.go -- package effects -func effects(x int) int { //@codeaction("refactor.rewrite", "y", "y", effects) +func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite", effects) return x } @@ -234,13 +234,13 @@ func _() { -- recursive/recursive.go -- package recursive -func Recursive(x int) int { //@codeaction("refactor.rewrite", "x", "x", recursive) +func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive) return Recursive(1) } -- @recursive/recursive/recursive.go -- package recursive -func Recursive() int { //@codeaction("refactor.rewrite", "x", "x", recursive) +func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive) return Recursive() } diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt index 39f3ddbf121..17abb98d5c9 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt @@ -14,7 +14,7 @@ go 1.18 package a // A doc comment. -func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("refactor.rewrite", "unused", "unused", a) +func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("unused", "unused", "refactor.rewrite", a) // about to return return x // returning // just returned @@ -36,7 +36,7 @@ func one() int { package a // A doc comment. -func A(x int) int { //@codeaction("refactor.rewrite", "unused", "unused", a) +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) // about to return return x // returning // just returned diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt index 417318497d7..e67e378fde3 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt @@ -10,7 +10,7 @@ go 1.18 -- a/a.go -- package a -func A(x, unused int) int { //@codeactionerr("refactor.rewrite", "unused", "unused", re"non-call function reference") +func A(x, unused int) int { //@codeactionerr("unused", "unused", "refactor.rewrite", re"non-call function reference") return x } diff --git a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt index 1a483d2525c..d183cc44135 100644 --- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt @@ -59,7 +59,7 @@ import "mod.test/c" var Chan chan c.C -func B(x, y c.C) { //@codeaction("refactor.rewrite", "x", "x", b) +func B(x, y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) } -- c/c.go -- @@ -73,7 +73,7 @@ package d // Removing the parameter should remove this import. import "mod.test/c" -func D(x c.C) { //@codeaction("refactor.rewrite", "x", "x", d) +func D(x c.C) { //@codeaction("x", "x", "refactor.rewrite", d) } func _() { @@ -145,14 +145,14 @@ import "mod.test/c" var Chan chan c.C -func B(y c.C) { //@codeaction("refactor.rewrite", "x", "x", b) +func B(y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) } -- @d/d/d.go -- package d // Removing the parameter should remove this import. -func D() { //@codeaction("refactor.rewrite", "x", "x", d) +func D() { //@codeaction("x", "x", "refactor.rewrite", d) } func _() { diff --git a/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt new file mode 100644 index 00000000000..241a80a99c2 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt @@ -0,0 +1,28 @@ +Test of the suggested fix to remove unnecessary assignments. + +-- a.go -- +package suggestedfix + +import ( + "log" +) + +func goodbye() { + s := "hiiiiiii" + s = s //@suggestedfix("s = s", re"self-assignment", "quickfix", fix) + log.Print(s) +} + +-- @fix/a.go -- +package suggestedfix + +import ( + "log" +) + +func goodbye() { + s := "hiiiiiii" + //@suggestedfix("s = s", re"self-assignment", "quickfix", fix) + log.Print(s) +} + diff --git a/gopls/internal/regtest/marker/testdata/quickfix/undeclared.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt similarity index 100% rename from gopls/internal/regtest/marker/testdata/quickfix/undeclared.txt rename to gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt diff --git a/gopls/internal/regtest/marker/testdata/quickfix/unusedrequire.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt similarity index 100% rename from gopls/internal/regtest/marker/testdata/quickfix/unusedrequire.txt rename to gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt diff --git a/gopls/internal/regtest/marker/testdata/quickfix/unusedrequire_gowork.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt similarity index 100% rename from gopls/internal/regtest/marker/testdata/quickfix/unusedrequire_gowork.txt rename to gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt diff --git a/internal/analysisinternal/analysis.go b/internal/analysisinternal/analysis.go index 8948ecce551..2b291680479 100644 --- a/internal/analysisinternal/analysis.go +++ b/internal/analysisinternal/analysis.go @@ -42,7 +42,7 @@ func ZeroValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { case u.Info()&types.IsString != 0: return &ast.BasicLit{Kind: token.STRING, Value: `""`} default: - panic("unknown basic type") + panic(fmt.Sprintf("unknown basic type %v", u)) } case *types.Chan, *types.Interface, *types.Map, *types.Pointer, *types.Signature, *types.Slice, *types.Array: return ast.NewIdent("nil") diff --git a/internal/diff/diff_test.go b/internal/diff/diff_test.go index 7b25c3af5c3..77a20baf272 100644 --- a/internal/diff/diff_test.go +++ b/internal/diff/diff_test.go @@ -123,7 +123,7 @@ func TestToUnified(t *testing.T) { testenv.NeedsTool(t, "patch") for _, tc := range difftest.TestCases { t.Run(tc.Name, func(t *testing.T) { - unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits) + unified, err := diff.ToUnified(difftest.FileA, difftest.FileB, tc.In, tc.Edits, diff.DefaultContextLines) if err != nil { t.Fatal(err) } diff --git a/internal/diff/difftest/difftest.go b/internal/diff/difftest/difftest.go index fb691edc386..a5507675f17 100644 --- a/internal/diff/difftest/difftest.go +++ b/internal/diff/difftest/difftest.go @@ -307,7 +307,7 @@ func DiffTest(t *testing.T, compute func(before, after string) []diff.Edit) { if err != nil { t.Fatalf("Apply failed: %v", err) } - unified, err := diff.ToUnified(FileA, FileB, test.In, edits) + unified, err := diff.ToUnified(FileA, FileB, test.In, edits, diff.DefaultContextLines) if err != nil { t.Fatalf("ToUnified: %v", err) } diff --git a/internal/diff/unified.go b/internal/diff/unified.go index 1308503f70c..cfbda61020a 100644 --- a/internal/diff/unified.go +++ b/internal/diff/unified.go @@ -10,12 +10,16 @@ import ( "strings" ) +// DefaultContextLines is the number of unchanged lines of surrounding +// context displayed by Unified. Use ToUnified to specify a different value. +const DefaultContextLines = 3 + // Unified returns a unified diff of the old and new strings. // The old and new labels are the names of the old and new files. // If the strings are equal, it returns the empty string. func Unified(oldLabel, newLabel, old, new string) string { edits := Strings(old, new) - unified, err := ToUnified(oldLabel, newLabel, old, edits) + unified, err := ToUnified(oldLabel, newLabel, old, edits, DefaultContextLines) if err != nil { // Can't happen: edits are consistent. log.Fatalf("internal error in diff.Unified: %v", err) @@ -23,11 +27,12 @@ func Unified(oldLabel, newLabel, old, new string) string { return unified } -// ToUnified applies the edits to content and returns a unified diff. +// ToUnified applies the edits to content and returns a unified diff, +// with contextLines lines of (unchanged) context around each diff hunk. // The old and new labels are the names of the content and result files. // It returns an error if the edits are inconsistent; see ApplyEdits. -func ToUnified(oldLabel, newLabel, content string, edits []Edit) (string, error) { - u, err := toUnified(oldLabel, newLabel, content, edits) +func ToUnified(oldLabel, newLabel, content string, edits []Edit, contextLines int) (string, error) { + u, err := toUnified(oldLabel, newLabel, content, edits, contextLines) if err != nil { return "", err } @@ -93,14 +98,10 @@ func (k opKind) String() string { } } -const ( - edge = 3 - gap = edge * 2 -) - // toUnified takes a file contents and a sequence of edits, and calculates // a unified diff that represents those edits. -func toUnified(fromName, toName string, content string, edits []Edit) (unified, error) { +func toUnified(fromName, toName string, content string, edits []Edit, contextLines int) (unified, error) { + gap := contextLines * 2 u := unified{ from: fromName, to: toName, @@ -136,7 +137,7 @@ func toUnified(fromName, toName string, content string, edits []Edit) (unified, //need to start a new hunk if h != nil { // add the edge to the previous hunk - addEqualLines(h, lines, last, last+edge) + addEqualLines(h, lines, last, last+contextLines) u.hunks = append(u.hunks, h) } toLine += start - last @@ -145,7 +146,7 @@ func toUnified(fromName, toName string, content string, edits []Edit) (unified, toLine: toLine + 1, } // add the edge to the new hunk - delta := addEqualLines(h, lines, start-edge, start) + delta := addEqualLines(h, lines, start-contextLines, start) h.fromLine -= delta h.toLine -= delta } @@ -163,7 +164,7 @@ func toUnified(fromName, toName string, content string, edits []Edit) (unified, } if h != nil { // add the edge to the final hunk - addEqualLines(h, lines, last, last+edge) + addEqualLines(h, lines, last, last+contextLines) u.hunks = append(u.hunks, h) } return u, nil From 04cf2690dfcb1e49061a5921b045eb2508424e18 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 2 Nov 2023 15:27:29 -0400 Subject: [PATCH 078/100] gopls/internal/lsp/cache: allow duplicate analyzer names The Actions map is now keyed by an analyzer's stable name, which has a suffix consisting of the code address of its Run function. This allows us to use two analyzers called nilness in the same action graph, so long as their implementations are different. (Two analyzers with the same Name and Run code--e.g. closures with different values--is a theoretical but remote possibility.) Change-Id: Iecc21a61f666a1f6d1a71c8691c31ec3fd039875 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539495 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/internal/lsp/cache/analysis.go | 96 +++++++++++++++++++--------- 1 file changed, 67 insertions(+), 29 deletions(-) diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 542ce5efa2a..600444094ae 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -19,6 +19,7 @@ import ( "go/types" "log" urlpkg "net/url" + "path/filepath" "reflect" "runtime" "runtime/debug" @@ -167,9 +168,6 @@ const AnalysisProgressTitle = "Analyzing Dependencies" // The analyzers list must be duplicate free; order does not matter. // // Notifications of progress may be sent to the optional reporter. -// -// Precondition: all analyzers within the process have distinct names. -// (The names are relied on by the serialization logic.) func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, analyzers []*source.Analyzer, reporter *progress.Tracker) ([]*source.Diagnostic, error) { start := time.Now() // for progress reporting @@ -202,10 +200,22 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, }) analyzers = nil // prevent accidental use - // Register fact types of required analyzers. enabled = requiredAnalyzers(enabled) + + // Perform basic sanity checks. + // (Ideally we would do this only once.) + if err := analysis.Validate(enabled); err != nil { + return nil, fmt.Errorf("invalid analyzer configuration: %v", err) + } + + stableNames := make(map[*analysis.Analyzer]string) + var facty []*analysis.Analyzer // facty subset of enabled + transitive requirements for _, a := range enabled { + // TODO(adonovan): reject duplicate stable names (very unlikely). + stableNames[a] = stableName(a) + + // Register fact types of all required analyzers. if len(a.FactTypes) > 0 { facty = append(facty, a) for _, f := range a.FactTypes { @@ -239,11 +249,12 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, // -- preorder -- an = &analysisNode{ - fset: fset, - m: m, - analyzers: facty, // all nodes run at least the facty analyzers - allDeps: make(map[PackagePath]*analysisNode), - exportDeps: make(map[PackagePath]*analysisNode), + fset: fset, + m: m, + analyzers: facty, // all nodes run at least the facty analyzers + allDeps: make(map[PackagePath]*analysisNode), + exportDeps: make(map[PackagePath]*analysisNode), + stableNames: stableNames, } nodes[id] = an @@ -434,7 +445,7 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, // cause #60909 since none of the analyzers added for // requirements (e.g. ctrlflow, inspect, buildssa) // is capable of reporting diagnostics. - if summary := root.summary.Actions[a.Name]; summary != nil { + if summary := root.summary.Actions[stableNames[a]]; summary != nil { if n := len(summary.Diagnostics); n > 0 { bug.Reportf("Internal error: got %d unexpected diagnostics from analyzer %s. This analyzer was added only to fulfil the requirements of the requested set of analyzers, and it is not expected that such analyzers report diagnostics. Please report this in issue #60909.", n, a) } @@ -443,10 +454,10 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, } // Inv: root.summary is the successful result of run (via runCached). - summary, ok := root.summary.Actions[a.Name] + summary, ok := root.summary.Actions[stableNames[a]] if summary == nil { panic(fmt.Sprintf("analyzeSummary.Actions[%q] = (nil, %t); got %v (#60551)", - a.Name, ok, root.summary.Actions)) + stableNames[a], ok, root.summary.Actions)) } if summary.Err != "" { continue // action failed @@ -500,6 +511,7 @@ type analysisNode struct { allDeps map[PackagePath]*analysisNode // all dependencies including self exportDeps map[PackagePath]*analysisNode // subset of allDeps ref'd by export data (+self) summary *analyzeSummary // serializable result of analyzing this package + stableNames map[*analysis.Analyzer]string // cross-process stable names for Analyzers typesOnce sync.Once // guards lazy population of types and typesErr fields types *types.Package // type information lazily imported from summary @@ -568,16 +580,16 @@ type analyzeSummary struct { Export []byte // encoded types of package DeepExportHash source.Hash // hash of reflexive transitive closure of export data Compiles bool // transitively free of list/parse/type errors - Actions actionsMap // map from analyzer name to analysis results (*actionSummary) + Actions actionMap // maps analyzer stablename to analysis results (*actionSummary) } -// actionsMap defines a stable Gob encoding for a map. +// actionMap defines a stable Gob encoding for a map. // TODO(adonovan): generalize and move to a library when we can use generics. -type actionsMap map[string]*actionSummary +type actionMap map[string]*actionSummary var ( - _ gob.GobEncoder = (actionsMap)(nil) - _ gob.GobDecoder = (*actionsMap)(nil) + _ gob.GobEncoder = (actionMap)(nil) + _ gob.GobDecoder = (*actionMap)(nil) ) type actionsMapEntry struct { @@ -585,7 +597,7 @@ type actionsMapEntry struct { V *actionSummary } -func (m actionsMap) GobEncode() ([]byte, error) { +func (m actionMap) GobEncode() ([]byte, error) { entries := make([]actionsMapEntry, 0, len(m)) for k, v := range m { entries = append(entries, actionsMapEntry{k, v}) @@ -598,12 +610,12 @@ func (m actionsMap) GobEncode() ([]byte, error) { return buf.Bytes(), err } -func (m *actionsMap) GobDecode(data []byte) error { +func (m *actionMap) GobDecode(data []byte) error { var entries []actionsMapEntry if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&entries); err != nil { return err } - *m = make(actionsMap, len(entries)) + *m = make(actionMap, len(entries)) for _, e := range entries { (*m)[e.K] = e.V } @@ -849,7 +861,13 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { for _, req := range a.Requires { hdeps = append(hdeps, mkAction(req)) } - act = &action{a: a, pkg: pkg, vdeps: an.succs, hdeps: hdeps} + act = &action{ + a: a, + stableName: an.stableNames[a], + pkg: pkg, + vdeps: an.succs, + hdeps: hdeps, + } actions[a] = act } return act @@ -876,7 +894,7 @@ func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { if root.summary == nil { panic("root has nil action.summary (#60551)") } - summaries[root.a.Name] = root.summary + summaries[root.stableName] = root.summary } return &analyzeSummary{ @@ -1095,11 +1113,12 @@ type analysisPackage struct { // package (as different analyzers are applied, either in sequence or // parallel), and across packages (as dependencies are analyzed). type action struct { - once sync.Once - a *analysis.Analyzer - pkg *analysisPackage - hdeps []*action // horizontal dependencies - vdeps map[PackageID]*analysisNode // vertical dependencies + once sync.Once + a *analysis.Analyzer + stableName string // cross-process stable name of analyzer + pkg *analysisPackage + hdeps []*action // horizontal dependencies + vdeps map[PackageID]*analysisNode // vertical dependencies // results of action.exec(): result interface{} // result of Run function, of type a.ResultType @@ -1158,7 +1177,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) { if hasFacts { // TODO(adonovan): use deterministic order. for _, vdep := range act.vdeps { - if summ := vdep.summary.Actions[analyzer.Name]; summ.Err != "" { + if summ := vdep.summary.Actions[act.stableName]; summ.Err != "" { return nil, nil, errors.New(summ.Err) } } @@ -1232,7 +1251,7 @@ func (act *action) exec() (interface{}, *actionSummary, error) { return nil, bug.Errorf("internal error in %s: missing vdep for id=%s", pkg.types.Path(), id) } - return vdep.summary.Actions[analyzer.Name].Facts, nil + return vdep.summary.Actions[act.stableName].Facts, nil }) if err != nil { return nil, nil, fmt.Errorf("internal error decoding analysis facts: %w", err) @@ -1513,3 +1532,22 @@ func effectiveURL(a *analysis.Analyzer, diag analysis.Diagnostic) string { } return u } + +// stableName returns a name for the analyzer that is unique and +// stable across address spaces. +// +// Analyzer names are not unique. For example, gopls includes +// both x/tools/passes/nilness and staticcheck/nilness. +// For serialization, we must assign each analyzer a unique identifier +// that two gopls processes accessing the cache can agree on. +func stableName(a *analysis.Analyzer) string { + // Incorporate the file and line of the analyzer's Run function. + addr := reflect.ValueOf(a.Run).Pointer() + fn := runtime.FuncForPC(addr) + file, line := fn.FileLine(addr) + + // It is tempting to use just a.Name as the stable name when + // it is unique, but making them always differ helps avoid + // name/stablename confusion. + return fmt.Sprintf("%s(%s:%d)", a.Name, filepath.Base(file), line) +} From 0ee4d8760d6c6a1c615a47048132acd7e6c676b3 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 31 Oct 2023 10:45:17 -0400 Subject: [PATCH 079/100] go/ssa: create methods on demand This change enables the SSA builder to create all methods on demand as the need (e.g. a call) is encountered. This allows package-at-a-time analyses to call CreatePackage only for the direct imports of a syntax package, but not for all the indirect dependencies as well, which improves the asmyptote for separate analysis (e.g. in unitchecker or gopls). Details: - Prog.objectMethod creates a method from a types.Func, even if the package was not created. It combines the work of originFunc (RIP) + receiverTypeArgs + needsInstance; this triad of calls used to appear three times. - Delete originFunc declaredFunc. - Doc tweaks. - Unit test Also: - go/analysis/passes/buildssa: don't call CreatePackage for indirect dependencies. Change-Id: I905ea6e52473a4fe6a73ed574ea0f0ce0e8fe7ed Reviewed-on: https://go-review.googlesource.com/c/tools/+/538763 LUCI-TryBot-Result: Go LUCI Reviewed-by: Tim King Auto-Submit: Alan Donovan --- go/analysis/passes/buildssa/buildssa.go | 18 +--- go/ssa/builder.go | 25 ++---- go/ssa/builder_test.go | 112 ++++++++++++++++++++++++ go/ssa/create.go | 23 ++++- go/ssa/example_test.go | 5 ++ go/ssa/func.go | 3 +- go/ssa/methods.go | 62 +++++++++---- go/ssa/sanity.go | 3 +- go/ssa/source.go | 39 ++++----- go/ssa/ssa.go | 5 ++ go/ssa/ssautil/load.go | 37 ++++++++ go/ssa/wrappers.go | 18 +--- 12 files changed, 259 insertions(+), 91 deletions(-) diff --git a/go/analysis/passes/buildssa/buildssa.go b/go/analysis/passes/buildssa/buildssa.go index 881b8fd67d7..5a348ac245d 100644 --- a/go/analysis/passes/buildssa/buildssa.go +++ b/go/analysis/passes/buildssa/buildssa.go @@ -33,8 +33,6 @@ type SSA struct { } func run(pass *analysis.Pass) (interface{}, error) { - // Plundered from ssautil.BuildPackage. - // We must create a new Program for each Package because the // analysis API provides no place to hang a Program shared by // all Packages. Consequently, SSA Packages and Functions do not @@ -51,20 +49,10 @@ func run(pass *analysis.Pass) (interface{}, error) { prog := ssa.NewProgram(pass.Fset, mode) - // Create SSA packages for all imports. - // Order is not significant. - created := make(map[*types.Package]bool) - var createAll func(pkgs []*types.Package) - createAll = func(pkgs []*types.Package) { - for _, p := range pkgs { - if !created[p] { - created[p] = true - prog.CreatePackage(p, nil, nil, true) - createAll(p.Imports()) - } - } + // Create SSA packages for direct imports. + for _, p := range pass.Pkg.Imports() { + prog.CreatePackage(p, nil, nil, true) } - createAll(pass.Pkg.Imports()) // Create and build the primary package. ssapkg := prog.CreatePackage(pass.Pkg, pass.Files, pass.TypesInfo, false) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 471d10f4ad2..a8bed151f29 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -34,13 +34,6 @@ package ssa // Generic functions declared in a package P can be instantiated from functions // outside of P. This happens independently of the CREATE and BUILD phase of P. // -// Locks: -// -// Mutexes are currently acquired according to the following order: -// Prog.methodsMu ⊃ canonizer.mu ⊃ printMu -// where x ⊃ y denotes that y can be acquired while x is held -// and x cannot be acquired while y is held. -// // Synthetics: // // During the BUILD phase new functions can be created and built. These include: @@ -795,7 +788,9 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { case *types.Nil: return zeroConst(fn.instanceType(e)) } + // Package-level func or var? + // (obj must belong to same package or a direct import.) if v := fn.Prog.packageLevelMember(obj); v != nil { if g, ok := v.(*Global); ok { return emitLoad(fn, g) // var (address) @@ -997,11 +992,7 @@ func (b *builder) setCallFunc(fn *Function, e *ast.CallExpr, c *CallCommon) { c.Method = obj } else { // "Call"-mode call. - callee := fn.Prog.originFunc(obj) - if callee.typeparams.Len() > 0 { - callee = callee.instance(receiverTypeArgs(obj), b.created) - } - c.Value = callee + c.Value = fn.Prog.objectMethod(obj, b.created) c.Args = append(c.Args, v) } return @@ -2592,11 +2583,11 @@ func (b *builder) buildFromSyntax(fn *Function) { // Acquires prog.runtimeTypesMu. func addRuntimeType(prog *Program, t types.Type) { prog.runtimeTypesMu.Lock() + defer prog.runtimeTypesMu.Unlock() forEachReachable(&prog.MethodSets, t, func(t types.Type) bool { prev, _ := prog.runtimeTypes.Set(t, true).(bool) return !prev // already seen? }) - prog.runtimeTypesMu.Unlock() } // Build calls Package.Build for each package in prog. @@ -2624,9 +2615,11 @@ func (prog *Program) Build() { // Build builds SSA code for all functions and vars in package p. // -// Precondition: CreatePackage must have been called for all of p's -// direct imports (and hence its direct imports must have been -// error-free). +// CreatePackage must have been called for all of p's direct imports +// (and hence its direct imports must have been error-free). It is not +// necessary to call CreatePackage for indirect dependencies. +// Functions will be created for all necessary methods in those +// packages on demand. // // Build is idempotent and thread-safe. func (p *Package) Build() { p.buildOnce.Do(p.build) } diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index df8763aaec0..e561533a867 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -22,10 +22,12 @@ import ( "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/loader" + "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/txtar" ) func isEmpty(f *ssa.Function) bool { return f.Blocks == nil } @@ -165,6 +167,116 @@ func main() { } } +// Tests that methods from indirect dependencies not subject to +// CreatePackage are created as needed. +func TestNoIndirectCreatePackage(t *testing.T) { + testenv.NeedsGoBuild(t) // for go/packages + + src := ` +-- go.mod -- +module testdata +go 1.18 + +-- a/a.go -- +package a + +import "testdata/b" + +func A() { + var x b.B + x.F() +} + +-- b/b.go -- +package b + +import "testdata/c" + +type B struct { c.C } + +-- c/c.go -- +package c + +type C int +func (C) F() {} +` + dir := t.TempDir() + if err := extractArchive(dir, src); err != nil { + t.Fatal(err) + } + pkgs, err := loadPackages(dir, "testdata/a") + if err != nil { + t.Fatal(err) + } + a := pkgs[0] + + // Create a from syntax, its direct deps b from types, but not indirect deps c. + prog := ssa.NewProgram(a.Fset, ssa.SanityCheckFunctions|ssa.PrintFunctions) + aSSA := prog.CreatePackage(a.Types, a.Syntax, a.TypesInfo, false) + for _, p := range a.Types.Imports() { + prog.CreatePackage(p, nil, nil, true) + } + + // Build SSA for package a. + aSSA.Build() + + // Find the function in the sole call in the sole block of function a.A. + var got string + for _, instr := range aSSA.Members["A"].(*ssa.Function).Blocks[0].Instrs { + if call, ok := instr.(*ssa.Call); ok { + f := call.Call.Value.(*ssa.Function) + got = fmt.Sprintf("%v # %s", f, f.Synthetic) + break + } + } + want := "(testdata/c.C).F # from type information (on demand)" + if got != want { + t.Errorf("for sole call in a.A, got: <<%s>>, want <<%s>>", got, want) + } +} + +// extractArchive extracts the txtar archive into the specified directory. +func extractArchive(dir, arch string) error { + // TODO(adonovan): publish this a helper (#61386). + extractTxtar := func(ar *txtar.Archive, dir string) error { + for _, file := range ar.Files { + name := filepath.Join(dir, file.Name) + if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil { + return err + } + if err := os.WriteFile(name, file.Data, 0666); err != nil { + return err + } + } + return nil + } + + // Extract archive to temporary tree. + ar := txtar.Parse([]byte(arch)) + return extractTxtar(ar, dir) +} + +// loadPackages loads packages from the specified directory, using LoadSyntax. +func loadPackages(dir string, patterns ...string) ([]*packages.Package, error) { + cfg := &packages.Config{ + Dir: dir, + Mode: packages.LoadSyntax, + Env: append(os.Environ(), + "GO111MODULES=on", + "GOPATH=", + "GOWORK=off", + "GOPROXY=off"), + } + pkgs, err := packages.Load(cfg, patterns...) + if err != nil { + return nil, err + } + if packages.PrintErrors(pkgs) > 0 { + return nil, fmt.Errorf("there were errors") + } + return pkgs, nil +} + // TestRuntimeTypes tests that (*Program).RuntimeTypes() includes all necessary types. func TestRuntimeTypes(t *testing.T) { testenv.NeedsGoBuild(t) // for importer.Default() diff --git a/go/ssa/create.go b/go/ssa/create.go index c1ae07d0d74..df9a28314b0 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -21,6 +21,17 @@ import ( // NewProgram returns a new SSA Program. // // mode controls diagnostics and checking during SSA construction. +// +// To construct an SSA program: +// +// - Call NewProgram to create an empty Program. +// - Call CreatePackage providing typed syntax for each package +// you want to build, and call it with types but not +// syntax for each of those package's direct dependencies. +// - Call [Package.Build] on each syntax package you wish to build, +// or [Program.Build] to build all of them. +// +// See the Example tests for simple examples. func NewProgram(fset *token.FileSet, mode BuilderMode) *Program { return &Program{ Fset: fset, @@ -92,7 +103,9 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion } } -// createFunction creates a function or method. +// createFunction creates a function or method. It supports both +// CreatePackage (with or without syntax) and the on-demand creation +// of methods in non-created packages based on their types.Func. func createFunction(prog *Program, obj *types.Func, name string, syntax ast.Node, info *types.Info, goversion string, cr *creator) *Function { sig := obj.Type().(*types.Signature) @@ -207,7 +220,11 @@ func (c *creator) Len() int { return len(*c) } // // The real work of building SSA form for each function is not done // until a subsequent call to Package.Build. +// +// CreatePackage should not be called after building any package in +// the program. func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info *types.Info, importable bool) *Package { + // TODO(adonovan): assert that no package has yet been built. if pkg == nil { panic("nil pkg") // otherwise pkg.Scope below returns types.Universe! } @@ -296,8 +313,8 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * // printMu serializes printing of Packages/Functions to stdout. var printMu sync.Mutex -// AllPackages returns a new slice containing all packages in the -// program prog in unspecified order. +// AllPackages returns a new slice containing all packages created by +// prog.CreatePackage in in unspecified order. func (prog *Program) AllPackages() []*Package { pkgs := make([]*Package, 0, len(prog.packages)) for _, pkg := range prog.packages { diff --git a/go/ssa/example_test.go b/go/ssa/example_test.go index eb34d60c6c6..99e513cb822 100644 --- a/go/ssa/example_test.go +++ b/go/ssa/example_test.go @@ -53,6 +53,11 @@ func main() { // Build and run the ssadump.go program if you want a standalone tool // with similar functionality. It is located at // golang.org/x/tools/cmd/ssadump. +// +// Use ssautil.BuildPackage only if you have parsed--but not +// type-checked--syntax trees. Typically, clients already have typed +// syntax, perhaps obtained from from golang.org/x/tools/go/packages. +// In that case, see the other examples for simpler approaches. func Example_buildPackage() { // Replace interface{} with any for this test. ssa.SetNormalizeAnyForTesting(true) diff --git a/go/ssa/func.go b/go/ssa/func.go index 04476474ec8..77fff71b00f 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -337,7 +337,6 @@ func (f *Function) removeNilBlocks() { // size of the instruction stream, and causes Functions to depend upon // the ASTs, potentially keeping them live in memory for longer. func (pkg *Package) SetDebugMode(debug bool) { - // TODO(adonovan): do we want ast.File granularity? pkg.debug = debug } @@ -482,7 +481,7 @@ func writeSignature(buf *bytes.Buffer, from *types.Package, name string, sig *ty func (fn *Function) declaredPackage() *Package { switch { case fn.Pkg != nil: - return fn.Pkg // non-generic function + return fn.Pkg // non-generic function (does that follow??) case fn.topLevelOrigin != nil: return fn.topLevelOrigin.Pkg // instance of a named generic function case fn.parent != nil: diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 1e0f27a2d7e..2d8c260cf34 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -52,6 +52,49 @@ func (prog *Program) MethodValue(sel *types.Selection) *Function { return m } +// objectMethod returns the Function for a given method symbol. +// The symbol may be an instance of a generic function. It need not +// belong to an existing SSA package created by a call to +// prog.CreatePackage. +// +// objectMethod panics if the function is not a method. +// +// Acquires prog.objectMethodsMu. +func (prog *Program) objectMethod(obj *types.Func, cr *creator) *Function { + sig := obj.Type().(*types.Signature) + if sig.Recv() == nil { + panic("not a method: " + obj.String()) + } + + // Belongs to a created package? + if fn := prog.FuncValue(obj); fn != nil { + return fn + } + + // Instantiation of generic? + if originObj := typeparams.OriginMethod(obj); originObj != obj { + origin := prog.objectMethod(originObj, cr) + assert(origin.typeparams.Len() > 0, "origin is not generic") + targs := receiverTypeArgs(obj) + return origin.instance(targs, cr) + } + + // Consult/update cache of methods created from types.Func. + prog.objectMethodsMu.Lock() + defer prog.objectMethodsMu.Unlock() + fn, ok := prog.objectMethods[obj] + if !ok { + fn = createFunction(prog, obj, obj.Name(), nil, nil, "", cr) + fn.Synthetic = "from type information (on demand)" + + if prog.objectMethods == nil { + prog.objectMethods = make(map[*types.Func]*Function) + } + prog.objectMethods[obj] = fn + } + return fn +} + // LookupMethod returns the implementation of the method of type T // identified by (pkg, name). It returns nil if the method exists but // is abstract, and panics if T has no such method. @@ -65,8 +108,7 @@ func (prog *Program) LookupMethod(T types.Type, pkg *types.Package, name string) // methodSet contains the (concrete) methods of a concrete type (non-interface, non-parameterized). type methodSet struct { - mapping map[string]*Function // populated lazily - complete bool // mapping contains all methods + mapping map[string]*Function // populated lazily } // Precondition: T is a concrete type, e.g. !isInterface(T) and not parameterized. @@ -77,6 +119,7 @@ func (prog *Program) createMethodSet(T types.Type) *methodSet { panic("type is interface or parameterized") } } + mset, ok := prog.methodSets.At(T).(*methodSet) if !ok { mset = &methodSet{mapping: make(map[string]*Function)} @@ -106,11 +149,7 @@ func (prog *Program) addMethod(mset *methodSet, sel *types.Selection, cr *creato if needsPromotion || needsIndirection { fn = createWrapper(prog, sel, cr) } else { - fn = prog.originFunc(obj) - if fn.typeparams.Len() > 0 { // instantiate - targs := receiverTypeArgs(obj) - fn = fn.instance(targs, cr) - } + fn = prog.objectMethod(obj, cr) } if fn.Signature.Recv() == nil { panic(fn) // missing receiver @@ -139,15 +178,6 @@ func (prog *Program) RuntimeTypes() []types.Type { return prog.runtimeTypes.Keys() } -// declaredFunc returns the concrete function/method denoted by obj. -// Panic ensues if there is none. -func (prog *Program) declaredFunc(obj *types.Func) *Function { - if v := prog.packageLevelMember(obj); v != nil { - return v.(*Function) - } - panic("no concrete method: " + obj.String()) -} - // forEachReachable calls f for type T and each type reachable from // its type through reflection. // diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 886be053251..28ec131f8c4 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -422,7 +422,8 @@ func (s *sanity) checkFunction(fn *Function) bool { // shared across packages, or duplicated as weak symbols in a // separate-compilation model), and error.Error. if fn.Pkg == nil { - if strings.HasPrefix(fn.Synthetic, "wrapper ") || + if strings.HasPrefix(fn.Synthetic, "from type information (on demand)") || + strings.HasPrefix(fn.Synthetic, "wrapper ") || strings.HasPrefix(fn.Synthetic, "bound ") || strings.HasPrefix(fn.Synthetic, "thunk ") || strings.HasSuffix(fn.name, "Error") || diff --git a/go/ssa/source.go b/go/ssa/source.go index 487abcf814d..7b1eb8527f9 100644 --- a/go/ssa/source.go +++ b/go/ssa/source.go @@ -198,16 +198,19 @@ func (f *Function) ValueForExpr(e ast.Expr) (value Value, isAddr bool) { // --- Lookup functions for source-level named entities (types.Objects) --- // Package returns the SSA Package corresponding to the specified -// type-checker package object. -// It returns nil if no such SSA package has been created. -func (prog *Program) Package(obj *types.Package) *Package { - return prog.packages[obj] +// type-checker package. It returns nil if no such Package was +// created by a prior call to prog.CreatePackage. +func (prog *Program) Package(pkg *types.Package) *Package { + return prog.packages[pkg] } -// packageLevelMember returns the package-level member corresponding to -// the specified named object, which may be a package-level const -// (*NamedConst), var (*Global) or func (*Function) of some package in -// prog. It returns nil if the object is not found. +// packageLevelMember returns the package-level member corresponding +// to the specified symbol, which may be a package-level const +// (*NamedConst), var (*Global) or func/method (*Function) of some +// package in prog. +// +// It returns nil if the object belongs to a package that has not been +// created by prog.CreatePackage. func (prog *Program) packageLevelMember(obj types.Object) Member { if pkg, ok := prog.packages[obj.Pkg()]; ok { return pkg.objects[obj] @@ -215,24 +218,16 @@ func (prog *Program) packageLevelMember(obj types.Object) Member { return nil } -// originFunc returns the package-level generic function that is the -// origin of obj. If returns nil if the generic function is not found. -func (prog *Program) originFunc(obj *types.Func) *Function { - return prog.declaredFunc(typeparams.OriginMethod(obj)) -} - -// FuncValue returns the concrete Function denoted by the source-level -// named function obj, or nil if obj denotes an interface method. -// -// TODO(adonovan): check the invariant that obj.Type() matches the -// result's Signature, both in the params/results and in the receiver. +// FuncValue returns the SSA function or (non-interface) method +// denoted by the specified func symbol. It returns nil id the symbol +// denotes an interface method, or belongs to a package that was not +// created by prog.CreatePackage. func (prog *Program) FuncValue(obj *types.Func) *Function { fn, _ := prog.packageLevelMember(obj).(*Function) return fn } -// ConstValue returns the SSA Value denoted by the source-level named -// constant obj. +// ConstValue returns the SSA constant denoted by the specified const symbol. func (prog *Program) ConstValue(obj *types.Const) *Const { // TODO(adonovan): opt: share (don't reallocate) // Consts for const objects and constant ast.Exprs. @@ -249,7 +244,7 @@ func (prog *Program) ConstValue(obj *types.Const) *Const { } // VarValue returns the SSA Value that corresponds to a specific -// identifier denoting the source-level named variable obj. +// identifier denoting the specified var symbol. // // VarValue returns nil if a local variable was not found, perhaps // because its package was not built, the debug information was not diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index fe5967ebbe9..d9ac23f073d 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -37,6 +37,11 @@ type Program struct { runtimeTypesMu sync.Mutex runtimeTypes typeutil.Map // set of runtime types (from MakeInterface) + + // objectMethods is a memoization of objectMethod + // to avoid creation of duplicate methods from type information. + objectMethodsMu sync.Mutex + objectMethods map[*types.Func]*Function } // A Package is a single analyzed Go package containing Members for diff --git a/go/ssa/ssautil/load.go b/go/ssa/ssautil/load.go index 281ad10f52d..67e75cb261a 100644 --- a/go/ssa/ssautil/load.go +++ b/go/ssa/ssautil/load.go @@ -35,6 +35,24 @@ import ( // // The mode parameter controls diagnostics and checking during SSA construction. func Packages(initial []*packages.Package, mode ssa.BuilderMode) (*ssa.Program, []*ssa.Package) { + // TODO(adonovan): opt: this calls CreatePackage far more than + // necessary: for all dependencies, not just the (non-initial) + // direct dependencies of the initial packages. + // + // But can it reasonably be changed without breaking the + // spirit and/or letter of the law above? Clients may notice + // if we call CreatePackage less, as methods like + // Program.FuncValue will return nil. Or must we provide a new + // function (and perhaps deprecate this one)? Is it worth it? + // + // Tim King makes the interesting point that it would be + // possible to entirely alleviate the client from the burden + // of calling CreatePackage for non-syntax packages, if we + // were to treat vars and funcs lazily in the same way we now + // treat methods. (In essence, try to move away from the + // notion of ssa.Packages, and make the Program answer + // all reasonable questions about any types.Object.) + return doPackages(initial, mode, false) } @@ -169,6 +187,25 @@ func BuildPackage(tc *types.Config, fset *token.FileSet, pkg *types.Package, fil } createAll(pkg.Imports()) + // TODO(adonovan): we could replace createAll with just: + // + // // Create SSA packages for all imports. + // for _, p := range pkg.Imports() { + // prog.CreatePackage(p, nil, nil, true) + // } + // + // (with minor changes to changes to ../builder_test.go as + // shown in CL 511715 PS 10.) But this would strictly violate + // the letter of the doc comment above, which says "all + // dependencies created". + // + // Tim makes the good point with some extra work we could + // remove the need for any CreatePackage calls except the + // ones with syntax (i.e. primary packages). Of course + // You wouldn't have ssa.Packages and Members for as + // many things but no-one really uses that anyway. + // I wish I had done this from the outset. + // Create and build the primary package. ssapkg := prog.CreatePackage(pkg, files, info, false) ssapkg.Build() diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index 2a1658abcf0..c366abe77c6 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -79,7 +79,6 @@ func createWrapper(prog *Program, sel *selection, cr *creator) *Function { } // buildWrapper builds fn.Body for a method wrapper. -// Acquires fn.Prog.methodsMu. func (b *builder) buildWrapper(fn *Function) { var recv *types.Var // wrapper's receiver or thunk's params[0] var start int // first regular param @@ -141,13 +140,7 @@ func (b *builder) buildWrapper(fn *Function) { if _, ptrObj := deptr(r); !ptrObj { v = emitLoad(fn, v) } - callee := fn.Prog.originFunc(fn.object) - if callee.typeparams.Len() > 0 { - fn.Prog.methodsMu.Lock() - callee = callee.instance(receiverTypeArgs(fn.object), b.created) - fn.Prog.methodsMu.Unlock() - } - c.Call.Value = callee + c.Call.Value = fn.Prog.objectMethod(fn.object, b.created) c.Call.Args = append(c.Call.Args, v) } else { c.Call.Method = fn.object @@ -218,7 +211,6 @@ func createBound(prog *Program, obj *types.Func, cr *creator) *Function { } // buildBound builds fn.Body for a bound method closure. -// Acquires fn.Prog.methodsMu. func (b *builder) buildBound(fn *Function) { fn.startBody() createParams(fn, 0) @@ -226,13 +218,7 @@ func (b *builder) buildBound(fn *Function) { recv := fn.FreeVars[0] if !types.IsInterface(recvType(fn.object)) { // concrete - callee := fn.Prog.originFunc(fn.object) - if callee.typeparams.Len() > 0 { - fn.Prog.methodsMu.Lock() - callee = callee.instance(receiverTypeArgs(fn.object), b.created) - fn.Prog.methodsMu.Unlock() - } - c.Call.Value = callee + c.Call.Value = fn.Prog.objectMethod(fn.object, b.created) c.Call.Args = []Value{recv} } else { c.Call.Method = fn.object From 9f1cca3771d970fcbb71b002b6f4f0ea0b70cd01 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 2 Nov 2023 11:04:27 -0400 Subject: [PATCH 080/100] go/ssa: remove Function.Syntax optimization As an optimization, Function.Syntax didn't guarantee to return the actual syntax for a function in all cases, and would instead return a stub that provides Pos/End. The aim was to prevent SSA from keeping ASTs live, but in practice that always happens anyway; and it had the perverse effect of forcing users to set GlobalDebug to get the syntax, which is even more expensive in space. This change retains the (syntax, info, goversion) triad unconditionally. This enables a simplification of the Function.generic, which no longer needs retain them. Change-Id: I41dd22a995d1093d2ecfd9cca743b7606bd5cc8b Reviewed-on: https://go-review.googlesource.com/c/tools/+/539257 Auto-Submit: Alan Donovan Reviewed-by: Tim King Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- go/ssa/builder.go | 12 ++++++++---- go/ssa/builder_test.go | 17 +++++++++-------- go/ssa/create.go | 17 +++-------------- go/ssa/func.go | 23 ++--------------------- go/ssa/instantiate.go | 25 ++++++++----------------- go/ssa/ssa.go | 18 ++++++++++-------- go/ssa/wrappers.go | 2 ++ 7 files changed, 42 insertions(+), 72 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index a8bed151f29..8a797fed778 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -638,13 +638,13 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { Pkg: fn.Pkg, Prog: fn.Prog, syntax: e, + info: fn.info, + goversion: fn.goversion, build: (*builder).buildFromSyntax, topLevelOrigin: nil, // use anonIdx to lookup an anon instance's origin. typeparams: fn.typeparams, // share the parent's type parameters. typeargs: fn.typeargs, // share the parent's type arguments. - info: fn.info, - subst: fn.subst, // share the parent's type substitutions. - goversion: fn.goversion, // share the parent's goversion + subst: fn.subst, // share the parent's type substitutions. } fn.AnonFuncs = append(fn.AnonFuncs, anon) // Build anon immediately, as it may cause fn's locals to escape. @@ -2712,7 +2712,11 @@ func (b *builder) buildPackageInit(fn *Function) { } } } - fn.goversion = "" // The rest of the init function is synthetic. No syntax => no goversion. + + // The rest of the init function is synthetic: + // no syntax, info, goversion. + fn.info = nil + fn.goversion = "" // Call all of the declared init() functions in source order. for _, file := range p.files { diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index e561533a867..bd79621b1d5 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -1119,13 +1119,8 @@ func TestIssue58491Rec(t *testing.T) { } } -// TestSyntax ensures that a function's Syntax is available when -// debug info is enabled. +// TestSyntax ensures that a function's Syntax is available. func TestSyntax(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestSyntax uses type parameters.") - } - const input = `package p type P int @@ -1146,7 +1141,8 @@ func TestSyntax(t *testing.T) { } return (*T)(f3()) } - var _ = F[int] + var g = F[int] + var _ = F[P] // unreferenced => not instantiated ` // Parse @@ -1164,7 +1160,7 @@ func TestSyntax(t *testing.T) { } // Create and build SSA - prog := ssautil.CreateProgram(lprog, ssa.GlobalDebug|ssa.InstantiateGenerics) + prog := ssautil.CreateProgram(lprog, ssa.InstantiateGenerics) prog.Build() // Collect syntax information for all of the functions. @@ -1174,6 +1170,9 @@ func TestSyntax(t *testing.T) { continue } syntax := fn.Syntax() + if got[fn.Name()] != "" { + t.Error("dup") + } got[fn.Name()] = fmt.Sprintf("%T : %s @ %d", syntax, fn.Signature, prog.Fset.Position(syntax.Pos()).Line) } @@ -1187,6 +1186,8 @@ func TestSyntax(t *testing.T) { "F[int]$1": "*ast.FuncLit : func() p.S1 @ 10", "F[int]$1$1": "*ast.FuncLit : func() p.S2 @ 11", "F[int]$2": "*ast.FuncLit : func() p.S3 @ 16", + // ...but no F[P] etc as they are unreferenced. + // (NB: GlobalDebug mode would cause them to be referenced.) } if !reflect.DeepEqual(got, want) { t.Errorf("Expected the functions with signature to be:\n\t%#v.\n Got:\n\t%#v", want, got) diff --git a/go/ssa/create.go b/go/ssa/create.go index df9a28314b0..ae04661787b 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -124,30 +124,19 @@ func createFunction(prog *Program, obj *types.Func, name string, syntax ast.Node Signature: sig, build: (*builder).buildFromSyntax, syntax: syntax, + info: info, + goversion: goversion, pos: obj.Pos(), Pkg: nil, // may be set by caller Prog: prog, typeparams: tparams, - info: info, - goversion: goversion, } if fn.syntax == nil { fn.Synthetic = "from type information" fn.build = (*builder).buildParamsOnly } if tparams.Len() > 0 { - // TODO(adonovan): retain the syntax/info/goversion fields indefinitely - // (i.e. don't clear them after Package.Build). It was a premature - // optimization design to avoid keeping typed syntax live, but the - // typed syntax is always live for some other reason. - // Then 'generic' reduces to a set of instances. - fn.generic = &generic{ - origin: fn, - // Syntax fields may all be empty: - syntax: fn.syntax, - info: fn.info, - goversion: fn.goversion, - } + fn.generic = new(generic) } cr.Add(fn) return fn diff --git a/go/ssa/func.go b/go/ssa/func.go index 77fff71b00f..65ed491bab6 100644 --- a/go/ssa/func.go +++ b/go/ssa/func.go @@ -10,7 +10,6 @@ import ( "bytes" "fmt" "go/ast" - "go/token" "go/types" "io" "os" @@ -247,11 +246,6 @@ func (f *Function) finishBody() { f.currentBlock = nil f.lblocks = nil - // Don't pin the AST in memory (except in debug mode). - if n := f.syntax; n != nil && !f.debugInfo() { - f.syntax = extentNode{n.Pos(), n.End()} - } - // Remove from f.Locals any Allocs that escape to the heap. j := 0 for _, l := range f.Locals { @@ -281,9 +275,7 @@ func (f *Function) finishBody() { // clear remaining builder state f.namedResults = nil // (used by lifting) - f.info = nil f.subst = nil - f.goversion = "" numberRegisters(f) // uses f.namedRegisters } @@ -634,19 +626,8 @@ func (prog *Program) NewFunction(name string, sig *types.Signature, provenance s return &Function{Prog: prog, name: name, Signature: sig, Synthetic: provenance} } -type extentNode [2]token.Pos - -func (n extentNode) Pos() token.Pos { return n[0] } -func (n extentNode) End() token.Pos { return n[1] } - -// Syntax returns an ast.Node whose Pos/End methods provide the -// lexical extent of the function if it was defined by Go source code -// (f.Synthetic==""), or nil otherwise. -// -// If f was built with debug information (see Package.SetDebugRef), -// the result is the *ast.FuncDecl or *ast.FuncLit that declared the -// function. Otherwise, it is an opaque Node providing only position -// information; this avoids pinning the AST in memory. +// Syntax returns the function's syntax (*ast.Func{Decl,Lit) +// if it was produced from syntax. func (f *Function) Syntax() ast.Node { return f.syntax } // identVar returns the variable defined by id. diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index bfcd001a1e3..370284ab72a 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -6,7 +6,6 @@ package ssa import ( "fmt" - "go/ast" "go/types" "sync" @@ -16,15 +15,8 @@ import ( // A generic records information about a generic origin function, // including a cache of existing instantiations. type generic struct { - origin *Function // generic origin; has typeparams but no typeargs - instancesMu sync.Mutex instances map[*typeList]*Function // canonical type arguments to an instance. - - // Syntax info saved from origin. Empty for a synthetic package. - syntax ast.Node // saved reference to FuncDecl - info *types.Info // type information - goversion string // goversion of syntax } // instance returns a Function that is the instantiation of generic @@ -42,7 +34,7 @@ func (fn *Function) instance(targs []types.Type, cr *creator) *Function { defer gen.instancesMu.Unlock() inst, ok := gen.instances[key] if !ok { - inst = createInstance(gen, targs, cr) + inst = createInstance(fn, targs, cr) if gen.instances == nil { gen.instances = make(map[*typeList]*Function) } @@ -51,12 +43,11 @@ func (fn *Function) instance(targs []types.Type, cr *creator) *Function { return inst } -// createInstance returns the instantiation of gen.origin using targs. +// createInstance returns the instantiation of generic function fn using targs. // If the instantiation is created, this is added to cr. // -// Requires gen.instancesMu. -func createInstance(gen *generic, targs []types.Type, cr *creator) *Function { - fn := gen.origin +// Requires fn.generic.instancesMu. +func createInstance(fn *Function, targs []types.Type, cr *creator) *Function { prog := fn.Prog // Compute signature. @@ -88,7 +79,7 @@ func createInstance(gen *generic, targs []types.Type, cr *creator) *Function { ) if prog.mode&InstantiateGenerics != 0 && !prog.parameterized.anyParameterized(targs) { synthetic = fmt.Sprintf("instance of %s", fn.Name()) - if gen.syntax != nil { + if fn.syntax != nil { scope := typeparams.OriginMethod(obj).Scope() subst = makeSubster(prog.ctxt, scope, fn.typeparams, targs, false) build = (*builder).buildFromSyntax @@ -106,7 +97,9 @@ func createInstance(gen *generic, targs []types.Type, cr *creator) *Function { object: obj, Signature: sig, Synthetic: synthetic, - syntax: gen.syntax, + syntax: fn.syntax, // \ + info: fn.info, // } empty for non-created packages + goversion: fn.goversion, // / build: build, topLevelOrigin: fn, pos: obj.Pos(), @@ -114,9 +107,7 @@ func createInstance(gen *generic, targs []types.Type, cr *creator) *Function { Prog: fn.Prog, typeparams: fn.typeparams, // share with origin typeargs: targs, - info: gen.info, // on synthetic packages info is nil. subst: subst, - goversion: gen.goversion, } cr.Add(instance) return instance diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index d9ac23f073d..121ce7c2231 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -317,12 +317,16 @@ type Function struct { Signature *types.Signature pos token.Pos - Synthetic string // provenance of synthetic function; "" for true source functions - syntax ast.Node // *ast.Func{Decl,Lit}; replaced with simple ast.Node after build, unless debug mode - build buildFunc // algorithm to build function body (nil => built) - parent *Function // enclosing function if anon; nil if global - Pkg *Package // enclosing package; nil for shared funcs (wrappers and error.Error) - Prog *Program // enclosing program + // source information + Synthetic string // provenance of synthetic function; "" for true source functions + syntax ast.Node // *ast.Func{Decl,Lit}, if from syntax (incl. generic instances) + info *types.Info // type annotations (iff syntax != nil) + goversion string // Go version of syntax (NB: init is special) + + build buildFunc // algorithm to build function body (nil => built) + parent *Function // enclosing function if anon; nil if global + Pkg *Package // enclosing package; nil for shared funcs (wrappers and error.Error) + Prog *Program // enclosing program // These fields are populated only when the function body is built: @@ -347,9 +351,7 @@ type Function struct { namedResults []*Alloc // tuple of named results targets *targets // linked stack of branch targets lblocks map[*types.Label]*lblock // labelled blocks - info *types.Info // *types.Info to build from. nil for wrappers. subst *subster // non-nil => expand generic body using this type substitution of ground types - goversion string // Go version of syntax (NB: init is special) } // BasicBlock represents an SSA basic block. diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index c366abe77c6..7c7ee4099e3 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go @@ -71,6 +71,7 @@ func createWrapper(prog *Program, sel *selection, cr *creator) *Function { pos: obj.Pos(), // wrappers have no syntax build: (*builder).buildWrapper, + syntax: nil, info: nil, goversion: "", } @@ -202,6 +203,7 @@ func createBound(prog *Program, obj *types.Func, cr *creator) *Function { pos: obj.Pos(), // wrappers have no syntax build: (*builder).buildBound, + syntax: nil, info: nil, goversion: "", } From 227e7287a856943361eb7a64b2f97f7b5bde7f16 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 2 Nov 2023 14:04:12 -0400 Subject: [PATCH 081/100] go/ssa: delete pre-go1.18 obsolete code Change-Id: I9bb055549d8fae3da2b6054576408f5c6687b66c Reviewed-on: https://go-review.googlesource.com/c/tools/+/539395 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI --- go/ssa/builder_generic_test.go | 7 --- go/ssa/builder_go117_test.go | 81 ---------------------------------- go/ssa/builder_test.go | 41 ++++++++++++----- go/ssa/const_test.go | 4 -- go/ssa/coretype_test.go | 4 -- go/ssa/emit.go | 2 +- go/ssa/identical.go | 12 ----- go/ssa/identical_17.go | 12 ----- go/ssa/identical_test.go | 14 ------ go/ssa/instantiate_test.go | 10 ----- go/ssa/methods_test.go | 4 -- go/ssa/parameterized_test.go | 6 --- go/ssa/source_test.go | 17 +++---- go/ssa/subst_test.go | 4 -- 14 files changed, 39 insertions(+), 179 deletions(-) delete mode 100644 go/ssa/builder_go117_test.go delete mode 100644 go/ssa/identical.go delete mode 100644 go/ssa/identical_17.go delete mode 100644 go/ssa/identical_test.go diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go index 9f58349badc..7c43b24c6c9 100644 --- a/go/ssa/builder_generic_test.go +++ b/go/ssa/builder_generic_test.go @@ -16,7 +16,6 @@ import ( "golang.org/x/tools/go/expect" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" - "golang.org/x/tools/internal/typeparams" ) // TestGenericBodies tests that bodies of generic functions and methods containing @@ -32,9 +31,6 @@ import ( // serialized using go/types.Type.String(). // See x/tools/go/expect for details on the syntax. func TestGenericBodies(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestGenericBodies requires type parameters") - } for _, contents := range []string{ ` package p00 @@ -590,9 +586,6 @@ func matchNotes(fset *token.FileSet, notes []*expect.Note, calls map[*ssa.CallCo // TestInstructionString tests serializing instructions via Instruction.String(). func TestInstructionString(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestInstructionString requires type parameters") - } // Tests (ssa.Instruction).String(). Instructions are from a single go file. // The Instructions tested are those that match a comment of the form: // diff --git a/go/ssa/builder_go117_test.go b/go/ssa/builder_go117_test.go deleted file mode 100644 index 69985970596..00000000000 --- a/go/ssa/builder_go117_test.go +++ /dev/null @@ -1,81 +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.17 -// +build go1.17 - -package ssa_test - -import ( - "go/ast" - "go/importer" - "go/parser" - "go/token" - "go/types" - "testing" - - "golang.org/x/tools/go/ssa" - "golang.org/x/tools/go/ssa/ssautil" -) - -func TestBuildPackageGo117(t *testing.T) { - tests := []struct { - name string - src string - importer types.Importer - }{ - {"slice to array pointer", "package p; var s []byte; var _ = (*[4]byte)(s)", nil}, - {"unsafe slice", `package p; import "unsafe"; var _ = unsafe.Add(nil, 0)`, importer.Default()}, - {"unsafe add", `package p; import "unsafe"; var _ = unsafe.Slice((*int)(nil), 0)`, importer.Default()}, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "p.go", tc.src, parser.ParseComments) - if err != nil { - t.Error(err) - } - files := []*ast.File{f} - - pkg := types.NewPackage("p", "") - conf := &types.Config{Importer: tc.importer} - if _, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions); err != nil { - t.Errorf("unexpected error: %v", err) - } - }) - } -} - -func TestBuildPackageFailuresGo117(t *testing.T) { - tests := []struct { - name string - src string - importer types.Importer - }{ - {"slice to array pointer - source is not a slice", "package p; var s [4]byte; var _ = (*[4]byte)(s)", nil}, - {"slice to array pointer - dest pointer elem is not an array", "package p; var s []byte; var _ = (*byte)(s)", nil}, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - fset := token.NewFileSet() - f, err := parser.ParseFile(fset, "p.go", tc.src, parser.ParseComments) - if err != nil { - t.Error(err) - } - files := []*ast.File{f} - - pkg := types.NewPackage("p", "") - conf := &types.Config{Importer: tc.importer} - if _, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions); err == nil { - t.Error("want error, but got nil") - } - }) - } -} diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index bd79621b1d5..2186d2578a9 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go @@ -644,9 +644,6 @@ func h(error) // TODO(taking): Add calls from non-generic functions to instantiations of generic functions. // TODO(taking): Add globals with types that are instantiations of generic functions. func TestGenericDecls(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestGenericDecls only works with type parameters enabled.") - } const input = ` package p @@ -701,9 +698,6 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) } func TestGenericWrappers(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestGenericWrappers only works with type parameters enabled.") - } const input = ` package p @@ -957,10 +951,6 @@ func sliceMax(s []int) []int { return s[a():b():c()] } // TestGenericFunctionSelector ensures generic functions from other packages can be selected. func TestGenericFunctionSelector(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestGenericFunctionSelector uses type parameters.") - } - pkgs := map[string]map[string]string{ "main": {"m.go": `package main; import "a"; func main() { a.F[int](); a.G[int,string](); a.H(0) }`}, "a": {"a.go": `package a; func F[T any](){}; func G[S, T any](){}; func H[T any](a T){} `}, @@ -1193,3 +1183,34 @@ func TestSyntax(t *testing.T) { t.Errorf("Expected the functions with signature to be:\n\t%#v.\n Got:\n\t%#v", want, got) } } + +func TestGo117Builtins(t *testing.T) { + tests := []struct { + name string + src string + importer types.Importer + }{ + {"slice to array pointer", "package p; var s []byte; var _ = (*[4]byte)(s)", nil}, + {"unsafe slice", `package p; import "unsafe"; var _ = unsafe.Add(nil, 0)`, importer.Default()}, + {"unsafe add", `package p; import "unsafe"; var _ = unsafe.Slice((*int)(nil), 0)`, importer.Default()}, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", tc.src, parser.ParseComments) + if err != nil { + t.Error(err) + } + files := []*ast.File{f} + + pkg := types.NewPackage("p", "") + conf := &types.Config{Importer: tc.importer} + if _, _, err := ssautil.BuildPackage(conf, fset, pkg, files, ssa.SanityCheckFunctions); err != nil { + t.Error(err) + } + }) + } +} diff --git a/go/ssa/const_test.go b/go/ssa/const_test.go index 131fe1aced2..d8e0c8a593a 100644 --- a/go/ssa/const_test.go +++ b/go/ssa/const_test.go @@ -19,10 +19,6 @@ import ( ) func TestConstString(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestConstString requires type parameters.") - } - const source = ` package P diff --git a/go/ssa/coretype_test.go b/go/ssa/coretype_test.go index 74fe4db1667..6fda54bf36a 100644 --- a/go/ssa/coretype_test.go +++ b/go/ssa/coretype_test.go @@ -15,10 +15,6 @@ import ( ) func TestCoreType(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestCoreType requires type parameters.") - } - const source = ` package P diff --git a/go/ssa/emit.go b/go/ssa/emit.go index 311bf999793..d77b4407a80 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go @@ -185,7 +185,7 @@ func emitCompare(f *Function, op token.Token, x, y Value, pos token.Pos) Value { // Precondition: neither argument is a named type. func isValuePreserving(ut_src, ut_dst types.Type) bool { // Identical underlying types? - if structTypesIdentical(ut_dst, ut_src) { + if types.IdenticalIgnoreTags(ut_dst, ut_src) { return true } diff --git a/go/ssa/identical.go b/go/ssa/identical.go deleted file mode 100644 index e8026967be8..00000000000 --- a/go/ssa/identical.go +++ /dev/null @@ -1,12 +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 go1.8 -// +build go1.8 - -package ssa - -import "go/types" - -var structTypesIdentical = types.IdenticalIgnoreTags diff --git a/go/ssa/identical_17.go b/go/ssa/identical_17.go deleted file mode 100644 index 575aa5dfc14..00000000000 --- a/go/ssa/identical_17.go +++ /dev/null @@ -1,12 +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 !go1.8 -// +build !go1.8 - -package ssa - -import "go/types" - -var structTypesIdentical = types.Identical diff --git a/go/ssa/identical_test.go b/go/ssa/identical_test.go deleted file mode 100644 index 25484a59c80..00000000000 --- a/go/ssa/identical_test.go +++ /dev/null @@ -1,14 +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 go1.8 -// +build go1.8 - -package ssa_test - -import "testing" - -func TestValueForExprStructConv(t *testing.T) { - testValueForExpr(t, "testdata/structconv.go") -} diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index 9475a37467e..476848d2205 100644 --- a/go/ssa/instantiate_test.go +++ b/go/ssa/instantiate_test.go @@ -16,7 +16,6 @@ import ( "testing" "golang.org/x/tools/go/loader" - "golang.org/x/tools/internal/typeparams" ) // loadProgram creates loader.Program out of p. @@ -53,9 +52,6 @@ func buildPackage(lprog *loader.Program, pkg string, mode BuilderMode) *Package // TestNeedsInstance ensures that new method instances can be created via needsInstance, // that TypeArgs are as expected, and can be accessed via _Instances. func TestNeedsInstance(t *testing.T) { - if !typeparams.Enabled { - return - } const input = ` package p @@ -150,9 +146,6 @@ func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) // TestCallsToInstances checks that calles of calls to generic functions, // without monomorphization, are wrappers around the origin generic function. func TestCallsToInstances(t *testing.T) { - if !typeparams.Enabled { - return - } const input = ` package p @@ -294,9 +287,6 @@ func changeTypeInstrs(b *BasicBlock) int { } func TestInstanceUniqueness(t *testing.T) { - if !typeparams.Enabled { - return - } const input = ` package p diff --git a/go/ssa/methods_test.go b/go/ssa/methods_test.go index 8391cf6d7a2..1b595782f45 100644 --- a/go/ssa/methods_test.go +++ b/go/ssa/methods_test.go @@ -13,14 +13,10 @@ import ( "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/typeparams" ) // Tests that MethodValue returns the expected method. func TestMethodValue(t *testing.T) { - if !typeparams.Enabled { - t.Skip("TestMethodValue requires type parameters") - } input := ` package p diff --git a/go/ssa/parameterized_test.go b/go/ssa/parameterized_test.go index 64c9125f278..7970a013c0e 100644 --- a/go/ssa/parameterized_test.go +++ b/go/ssa/parameterized_test.go @@ -10,15 +10,9 @@ import ( "go/token" "go/types" "testing" - - "golang.org/x/tools/internal/typeparams" ) func TestIsParameterized(t *testing.T) { - if !typeparams.Enabled { - return - } - const source = ` package P type A int diff --git a/go/ssa/source_test.go b/go/ssa/source_test.go index 9a7b30675b5..9cdad2bca70 100644 --- a/go/ssa/source_test.go +++ b/go/ssa/source_test.go @@ -23,7 +23,6 @@ import ( "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" - "golang.org/x/tools/internal/typeparams" ) func TestObjValueLookup(t *testing.T) { @@ -226,6 +225,10 @@ func TestValueForExpr(t *testing.T) { testValueForExpr(t, "testdata/valueforexpr.go") } +func TestValueForExprStructConv(t *testing.T) { + testValueForExpr(t, "testdata/structconv.go") +} + func testValueForExpr(t *testing.T, testfile string) { if runtime.GOOS == "android" { t.Skipf("no testdata dir on %s", runtime.GOOS) @@ -383,19 +386,13 @@ func TestEnclosingFunction(t *testing.T) { {`package main func init() { println(func(){print(900)}) }`, "900", "main.init#1$1"}, - } - if typeparams.Enabled { - tests = append(tests, struct { - input string - substr string - fn string - }{ - `package main + // generics + {`package main type S[T any] struct{} func (*S[T]) Foo() { println(1000) } type P[T any] struct{ *S[T] }`, "1000", "(*main.S[T]).Foo", - }) + }, } for _, test := range tests { conf := loader.Config{Fset: token.NewFileSet()} diff --git a/go/ssa/subst_test.go b/go/ssa/subst_test.go index 14cda54e6da..e4aeaa1c312 100644 --- a/go/ssa/subst_test.go +++ b/go/ssa/subst_test.go @@ -15,10 +15,6 @@ import ( ) func TestSubst(t *testing.T) { - if !typeparams.Enabled { - return - } - const source = ` package P From 942d9acc1ecb8b0ec51d6660adb17ee80ec47090 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 3 Nov 2023 11:33:45 -0400 Subject: [PATCH 082/100] go/ssa: don't treat _ specially in Prog.{Func,Const,Var}Value Because of a (long-since fixed) bug in go/types, the Program accessors that accept types.Objects didn't work for blank-named objects. This created an edge case that needed to be skipped in client code: FuncValue("_") would return nil, for example, even thought there's no reason clients shouldn't be able to analyze the bodies of blank functions. This change inserts all objects, regardless of name, into the Package.objects map to eliminate this edge case. (Of course, declarations must still have non-blank names to appear in the Members mapping.) Change-Id: Icb76655cdf30c487cfe1e36581a0528bff3db6d9 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539675 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/buildssa/buildssa.go | 12 +------ go/analysis/passes/buildssa/buildssa_test.go | 2 +- .../passes/buildssa/testdata/src/a/a.go | 4 +++ go/ssa/create.go | 34 +++++++++---------- go/ssa/interp/testdata/initorder.go | 10 ++++++ go/ssa/ssa.go | 2 +- go/ssa/stdlib_test.go | 5 --- 7 files changed, 33 insertions(+), 36 deletions(-) diff --git a/go/analysis/passes/buildssa/buildssa.go b/go/analysis/passes/buildssa/buildssa.go index 5a348ac245d..f077ea28247 100644 --- a/go/analysis/passes/buildssa/buildssa.go +++ b/go/analysis/passes/buildssa/buildssa.go @@ -26,7 +26,7 @@ var Analyzer = &analysis.Analyzer{ } // SSA provides SSA-form intermediate representation for all the -// non-blank source functions in the current package. +// source functions in the current package. type SSA struct { Pkg *ssa.Package SrcFuncs []*ssa.Function @@ -64,16 +64,6 @@ func run(pass *analysis.Pass) (interface{}, error) { for _, f := range pass.Files { for _, decl := range f.Decls { if fdecl, ok := decl.(*ast.FuncDecl); ok { - - // SSA will not build a Function - // for a FuncDecl named blank. - // That's arguably too strict but - // relaxing it would break uniqueness of - // names of package members. - if fdecl.Name.Name == "_" { - continue - } - // (init functions have distinct Func // objects named "init" and distinct // ssa.Functions named "init#1", ...) diff --git a/go/analysis/passes/buildssa/buildssa_test.go b/go/analysis/passes/buildssa/buildssa_test.go index 52f7e7aa6e3..c61a9f01709 100644 --- a/go/analysis/passes/buildssa/buildssa_test.go +++ b/go/analysis/passes/buildssa/buildssa_test.go @@ -20,7 +20,7 @@ func Test(t *testing.T) { ssainfo := result.(*buildssa.SSA) got := fmt.Sprint(ssainfo.SrcFuncs) - want := `[a.Fib (a.T).fib]` + want := `[a.Fib (a.T).fib a._ a._]` if got != want { t.Errorf("SSA.SrcFuncs = %s, want %s", got, want) for _, f := range ssainfo.SrcFuncs { diff --git a/go/analysis/passes/buildssa/testdata/src/a/a.go b/go/analysis/passes/buildssa/testdata/src/a/a.go index ddb13dacb8c..69d0e864ae0 100644 --- a/go/analysis/passes/buildssa/testdata/src/a/a.go +++ b/go/analysis/passes/buildssa/testdata/src/a/a.go @@ -14,3 +14,7 @@ func (T) fib(x int) int { return Fib(x) } func _() { print("hi") } + +func _() { + print("hello") +} diff --git a/go/ssa/create.go b/go/ssa/create.go index ae04661787b..c4410b1e652 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -60,9 +60,11 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion } case *types.TypeName: - pkg.Members[name] = &Type{ - object: obj, - pkg: pkg, + if name != "_" { + pkg.Members[name] = &Type{ + object: obj, + pkg: pkg, + } } case *types.Const: @@ -72,7 +74,9 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion pkg: pkg, } pkg.objects[obj] = c - pkg.Members[name] = c + if name != "_" { + pkg.Members[name] = c + } case *types.Var: g := &Global{ @@ -83,7 +87,9 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion pos: obj.Pos(), } pkg.objects[obj] = g - pkg.Members[name] = g + if name != "_" { + pkg.Members[name] = g + } case *types.Func: sig := obj.Type().(*types.Signature) @@ -94,7 +100,7 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node, goversion fn := createFunction(pkg.Prog, obj, name, syntax, pkg.info, goversion, &pkg.created) fn.Pkg = pkg pkg.objects[obj] = fn - if sig.Recv() == nil { + if name != "_" && sig.Recv() == nil { pkg.Members[name] = fn // package-level function } @@ -152,9 +158,7 @@ func membersFromDecl(pkg *Package, decl ast.Decl, goversion string) { case token.CONST: for _, spec := range decl.Specs { for _, id := range spec.(*ast.ValueSpec).Names { - if !isBlankIdent(id) { - memberFromObject(pkg, pkg.info.Defs[id], nil, "") - } + memberFromObject(pkg, pkg.info.Defs[id], nil, "") } } @@ -164,26 +168,20 @@ func membersFromDecl(pkg *Package, decl ast.Decl, goversion string) { pkg.initVersion[rhs] = goversion } for _, id := range spec.(*ast.ValueSpec).Names { - if !isBlankIdent(id) { - memberFromObject(pkg, pkg.info.Defs[id], spec, goversion) - } + memberFromObject(pkg, pkg.info.Defs[id], spec, goversion) } } case token.TYPE: for _, spec := range decl.Specs { id := spec.(*ast.TypeSpec).Name - if !isBlankIdent(id) { - memberFromObject(pkg, pkg.info.Defs[id], nil, "") - } + memberFromObject(pkg, pkg.info.Defs[id], nil, "") } } case *ast.FuncDecl: id := decl.Name - if !isBlankIdent(id) { - memberFromObject(pkg, pkg.info.Defs[id], decl, goversion) - } + memberFromObject(pkg, pkg.info.Defs[id], decl, goversion) } } diff --git a/go/ssa/interp/testdata/initorder.go b/go/ssa/interp/testdata/initorder.go index 0f26bed6955..21f0213461f 100644 --- a/go/ssa/interp/testdata/initorder.go +++ b/go/ssa/interp/testdata/initorder.go @@ -33,6 +33,11 @@ func main() { if abcdef != [6]int{0, 1, 2, 3, 4, 5} { panic(abcdef) } + + // Initializers of even blank globals are evaluated. + if g != 1 { + panic(g) + } } var order = makeOrder() @@ -41,6 +46,11 @@ var a, b = next(), next() var c, d = next2() var e, f = next(), next() +var ( + g int + _ = func() int { g = 1; return 0 }() +) + // ------------------------------------------------------------------------ var order2 []string diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 121ce7c2231..a22d85d3bcf 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -56,7 +56,7 @@ type Package struct { Prog *Program // the owning program Pkg *types.Package // the corresponding go/types.Package Members map[string]Member // all package members keyed by name (incl. init and init#%d) - objects map[types.Object]Member // mapping of package objects to members (incl. methods). Contains *NamedConst, *Global, *Function. + objects map[types.Object]Member // mapping of package objects to members (incl. methods). Contains *NamedConst, *Global, *Function (values but not types) init *Function // Func("init"); the package's init function debug bool // include full debug info in this package syntax bool // package was loaded from syntax diff --git a/go/ssa/stdlib_test.go b/go/ssa/stdlib_test.go index 05436a470be..d294fe6b085 100644 --- a/go/ssa/stdlib_test.go +++ b/go/ssa/stdlib_test.go @@ -182,11 +182,6 @@ func srcFunctions(prog *ssa.Program, pkgs []*packages.Package) (res []*ssa.Funct for _, file := range pkg.Syntax { for _, decl := range file.Decls { if decl, ok := decl.(*ast.FuncDecl); ok { - // TODO(adonovan): construct ssa.Functions for blank-named - // functions too, to avoid annoying edge cases like this. - if decl.Name.Name == "_" { - continue - } obj := pkg.TypesInfo.Defs[decl.Name].(*types.Func) if obj == nil { panic("nil *Func") From 7e0917a390289cf88925954e8f468e35b72295c2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 2 Nov 2023 21:58:56 -0400 Subject: [PATCH 083/100] go/ssa: update builder doc comment Also, avoid the terms "ground" and "abstract". Change-Id: I23f85a6f8db4d8fbb5db19600f42c07bf449d2f1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539555 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: Tim King --- go/ssa/builder.go | 135 +++++++++++++++------------------------- go/ssa/create.go | 5 +- go/ssa/doc.go | 3 - go/ssa/methods.go | 12 ++-- go/ssa/ssa.go | 13 ++-- go/ssa/ssautil/visit.go | 2 +- 6 files changed, 66 insertions(+), 104 deletions(-) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 8a797fed778..7ec589f7262 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -4,106 +4,73 @@ package ssa -// This file implements the BUILD phase of SSA construction. +// This file defines the builder, which builds SSA-form IR for function bodies. // -// SSA construction has two phases, CREATE and BUILD. In the CREATE phase -// (create.go), all packages are constructed and type-checked and -// definitions of all package members are created, method-sets are -// computed, and wrapper methods are synthesized. -// ssa.Packages are created in arbitrary order. +// SSA construction has two phases, "create" and "build". First, one +// or more packages are created in any order by a sequence of calls to +// CreatePackage, either from syntax or from mere type information. +// Each created package has a complete set of Members (const, var, +// type, func) that can be accessed through methods like +// Program.FuncValue. // -// In the BUILD phase (builder.go), the builder traverses the AST of -// each Go source function and generates SSA instructions for the -// function body. Initializer expressions for package-level variables -// are emitted to the package's init() function in the order specified -// by go/types.Info.InitOrder, then code for each function in the -// package is generated in lexical order. -// The BUILD phases for distinct packages are independent and are -// executed in parallel. +// It is not necessary to call CreatePackage for all dependencies of +// each syntax package, only for its direct imports. (In future +// perhaps even this restriction may be lifted.) // -// TODO(adonovan): indeed, building functions is now embarrassingly parallel. -// Audit for concurrency then benchmark using more goroutines. +// Second, packages created from syntax are built, by one or more +// calls to Package.Build, which may be concurrent; or by a call to +// Program.Build, which builds all packages in parallel. Building +// traverses the type-annotated syntax tree of each function body and +// creates SSA-form IR, a control-flow graph of instructions, +// populating fields such as Function.Body, .Params, and others. // -// State: +// Building may create additional methods, including: +// - wrapper methods (e.g. for embeddding, or implicit &recv) +// - bound method closures (e.g. for use(recv.f)) +// - thunks (e.g. for use(I.f) or use(T.f)) +// - generic instances (e.g. to produce f[int] from f[any]). +// As these methods are created, they are added to the build queue, +// and then processed in turn, until a fixed point is reached, +// Since these methods might belong to packages that were not +// created (by a call to CreatePackage), their Pkg field is unset. // -// The Package's and Program's indices (maps) are populated and -// mutated during the CREATE phase, but during the BUILD phase they -// remain constant. The sole exception is Prog.methodSets and its -// related maps, which are protected by a dedicated mutex. +// Instances of generic functions may be either instantiated (f[int] +// is a copy of f[T] with substitutions) or wrapped (f[int] delegates +// to f[T]), depending on the availability of generic syntax and the +// InstantiateGenerics mode flag. // -// Generic functions declared in a package P can be instantiated from functions -// outside of P. This happens independently of the CREATE and BUILD phase of P. +// Each package has an initializer function named "init" that calls +// the initializer functions of each direct import, computes and +// assigns the initial value of each global variable, and calls each +// source-level function named "init". (These generate SSA functions +// named "init#1", "init#2", etc.) // -// Synthetics: +// Runtime types // -// During the BUILD phase new functions can be created and built. These include: -// - wrappers (wrappers, bounds, thunks) -// - generic function instantiations -// These functions do not belong to a specific Pkg (Pkg==nil). Instead the -// Package that led to them being CREATED is obligated to ensure these -// are BUILT during the BUILD phase of the Package. +// Each MakeInterface operation is a conversion from a non-interface +// type to an interface type. The semantics of this operation requires +// a runtime type descriptor, which is the type portion of an +// interface, and the value abstracted by reflect.Type. // -// Runtime types: +// The program accumulates all non-parameterized types that are +// encountered as MakeInterface operands, along with all types that +// may be derived from them using reflection. This set is available as +// Program.RuntimeTypes, and the methods of these types may be +// reachable via interface calls or reflection even if they are never +// referenced from the SSA IR. (In practice, algorithms such as RTA +// that compute reachability from package main perform their own +// tracking of runtime types at a finer grain, so this feature is not +// very useful.) // -// A concrete type is a type that is fully monomorphized with concrete types, -// i.e. it cannot reach a TypeParam type. -// Some concrete types require full runtime type information. Cases -// include checking whether a type implements an interface or -// interpretation by the reflect package. All such types that may require -// this information will have all of their method sets built and will be added to Prog.methodSets. -// A type T is considered to require runtime type information if it is -// a runtime type and has a non-empty method set and either: -// - T flows into a MakeInterface instructions, -// - T appears in a concrete exported member, or -// - T is a type reachable from a type S that has non-empty method set. -// For any such type T, method sets must be created before the BUILD -// phase of the package is done. -// -// Function literals: -// -// The BUILD phase of a function literal (anonymous function) is tied to the -// BUILD phase of the enclosing parent function. The FreeVars of an anonymous -// function are discovered by building the anonymous function. This in turn -// changes which variables must be bound in a MakeClosure instruction in the -// parent. Anonymous functions also track where they are referred to in their -// parent function. -// -// Create and build: -// -// Construction happens in two phases, "create" and "build". -// Package and Function data structures are created by CreatePackage. -// However, fields of Function such as Body, Params, and others are -// populated only during building, which happens later (or never). -// -// A complete Program is built (in parallel) by calling Program.Build, -// but individual packages may built by calling Package.Build. -// -// The Function.build fields determines the algorithm for building the -// function body. It is cleared to mark that building is complete. +// Function literals // // Anonymous functions must be built as soon as they are encountered, // as it may affect locals of the enclosing function, but they are not // marked 'built' until the end of the outermost enclosing function. // (Among other things, this causes them to be logged in top-down order.) // -// The {start,finish}Body functions must be called (in that order) -// around construction of the Body. -// -// Building a package may trigger the creation of new functions for -// wrapper methods and instantiations. The Package.Build operation -// will build these additional functions, and any that they in turn -// create, until it converges. -// -// Program.MethodValue may also trigger the creation of new functions, -// and it too must build iterately until it converges. -// -// Program.NewFunction: -// -// This is a low level operation for creating functions that do not exist in -// the source. Use with caution. -// -// TODO(taking): Use consistent terminology for "concrete". -// TODO(taking): Use consistent terminology for "monomorphization"/"instantiate"/"expand". +// The Function.build fields determines the algorithm for building the +// function body. It is cleared to mark that building is complete. import ( "fmt" diff --git a/go/ssa/create.go b/go/ssa/create.go index c4410b1e652..eaaf4695e85 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -198,7 +198,7 @@ func (c *creator) Add(fn *Function) { func (c *creator) At(i int) *Function { return (*c)[i] } func (c *creator) Len() int { return len(*c) } -// CreatePackage constructs and returns an SSA Package from the +// CreatePackage creates and returns an SSA Package from the // specified type-checked, error-free file ASTs, and populates its // Members mapping. // @@ -221,7 +221,7 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * objects: make(map[types.Object]Member), Pkg: pkg, syntax: info != nil, - // transient values (CREATE and BUILD phases) + // transient values (cleared after Package.Build) info: info, files: files, initVersion: make(map[ast.Expr]string), @@ -241,7 +241,6 @@ func (prog *Program) CreatePackage(pkg *types.Package, files []*ast.File, info * p.Members[p.init.name] = p.init p.created.Add(p.init) - // CREATE phase. // Allocate all package members: vars, funcs, consts and types. if len(files) > 0 { // Go source package. diff --git a/go/ssa/doc.go b/go/ssa/doc.go index a687de45e26..56bc2fbc165 100644 --- a/go/ssa/doc.go +++ b/go/ssa/doc.go @@ -116,9 +116,6 @@ // The ssa/ssautil package provides various utilities that depend only // on the public API of this package. // -// TODO(adonovan): Consider the exceptional control-flow implications -// of defer and recover(). -// // TODO(adonovan): write a how-to document for all the various cases // of trying to determine corresponding elements across the four // domains of source locations, ast.Nodes, types.Objects, diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 2d8c260cf34..2700d26a72c 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -15,8 +15,8 @@ import ( ) // MethodValue returns the Function implementing method sel, building -// wrapper methods on demand. It returns nil if sel denotes an -// abstract (interface or parameterized) method. +// wrapper methods on demand. It returns nil if sel denotes an +// interface or generic method. // // Precondition: sel.Kind() == MethodVal. // @@ -29,11 +29,11 @@ func (prog *Program) MethodValue(sel *types.Selection) *Function { } T := sel.Recv() if types.IsInterface(T) { - return nil // abstract method (interface, possibly type param) + return nil // interface method or type parameter } if prog.parameterized.isParameterized(T) { - return nil // abstract method (generic) + return nil // generic method } if prog.mode&LogSource != 0 { @@ -97,7 +97,7 @@ func (prog *Program) objectMethod(obj *types.Func, cr *creator) *Function { // LookupMethod returns the implementation of the method of type T // identified by (pkg, name). It returns nil if the method exists but -// is abstract, and panics if T has no such method. +// is an interface method or generic method, and panics if T has no such method. func (prog *Program) LookupMethod(T types.Type, pkg *types.Package, name string) *Function { sel := prog.MethodSets.MethodSet(T).Lookup(pkg, name) if sel == nil { @@ -280,7 +280,7 @@ func forEachReachable(msets *typeutil.MethodSetCache, T types.Type, f func(types } case *typeparams.TypeParam, *typeparams.Union: - // Type parameters cannot be reached from ground types. + // forEachReachable must not be called on parameterized types. panic(T) default: diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index a22d85d3bcf..e913c1da1f2 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -303,8 +303,8 @@ type Node interface { // // A generic function is a function or method that has uninstantiated type // parameters (TypeParams() != nil). Consider a hypothetical generic -// method, (*Map[K,V]).Get. It may be instantiated with all ground -// (non-parameterized) types as (*Map[string,int]).Get or with +// method, (*Map[K,V]).Get. It may be instantiated with all +// non-parameterized types as (*Map[string,int]).Get or with // parameterized types as (*Map[string,U]).Get, where U is a type parameter. // In both instantiations, Origin() refers to the instantiated generic // method, (*Map[K,V]).Get, TypeParams() refers to the parameters [K,V] of @@ -344,14 +344,13 @@ type Function struct { topLevelOrigin *Function // the origin function if this is an instance of a source function. nil if Parent()!=nil. generic *generic // instances of this function, if generic - // The following fields are set transiently during building, - // then cleared. + // The following fields are cleared after building. currentBlock *BasicBlock // where to emit code vars map[*types.Var]Value // addresses of local variables namedResults []*Alloc // tuple of named results targets *targets // linked stack of branch targets lblocks map[*types.Label]*lblock // labelled blocks - subst *subster // non-nil => expand generic body using this type substitution of ground types + subst *subster // type parameter substitutions (if non-nil) } // BasicBlock represents an SSA basic block. @@ -1400,7 +1399,7 @@ type anInstruction struct { // represents a dynamically dispatched call to an interface method. // In this mode, Value is the interface value and Method is the // interface's abstract method. The interface value may be a type -// parameter. Note: an abstract method may be shared by multiple +// parameter. Note: an interface method may be shared by multiple // interfaces due to embedding; Value.Type() provides the specific // interface used for this call. // @@ -1418,7 +1417,7 @@ type anInstruction struct { // the last element of Args is a slice. type CallCommon struct { Value Value // receiver (invoke mode) or func value (call mode) - Method *types.Func // abstract method (invoke mode) + Method *types.Func // interface method (invoke mode) Args []Value // actual parameters (in static method call, includes receiver) pos token.Pos // position of CallExpr.Lparen, iff explicit in source } diff --git a/go/ssa/ssautil/visit.go b/go/ssa/ssautil/visit.go index 3bd26a0bf4c..3cdd3462271 100644 --- a/go/ssa/ssautil/visit.go +++ b/go/ssa/ssautil/visit.go @@ -83,7 +83,7 @@ func AllFunctions(prog *ssa.Program) map[*ssa.Function]bool { // Historically, Program.RuntimeTypes used to include the type // of any exported member of a package loaded from syntax that - // has a non-parameterized (ground) type, plus all types + // has a non-parameterized type, plus all types // reachable from that type using reflection, even though // these runtime types may not be required for them. // From 53cd36bc1c3c0ddb4bdc5efff30d12720f2c08ad Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 4 Nov 2023 10:48:04 -0400 Subject: [PATCH 084/100] gopls: remove dead code The protocol.Parse* methods look reasonable but we can always revive them if/when we need them. Change-Id: Ieb2dd98bbc1b216e4527d1fbdd675c6447b1a7ea Reviewed-on: https://go-review.googlesource.com/c/tools/+/539660 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/debug/info.go | 99 ------------------------- gopls/internal/lsp/protocol/enums.go | 57 -------------- gopls/internal/lsp/protocol/span.go | 4 - gopls/internal/vulncheck/vulntest/db.go | 7 -- 4 files changed, 167 deletions(-) diff --git a/gopls/internal/lsp/debug/info.go b/gopls/internal/lsp/debug/info.go index 34e6dd4e2b1..579e54978b7 100644 --- a/gopls/internal/lsp/debug/info.go +++ b/gopls/internal/lsp/debug/info.go @@ -11,13 +11,9 @@ import ( "fmt" "io" "os" - "reflect" "runtime" "runtime/debug" - "sort" "strings" - - "golang.org/x/tools/gopls/internal/lsp/source" ) type PrintMode int @@ -155,104 +151,9 @@ type field struct { var fields []field -// find all the options. The presumption is that the Options are nested structs -// and that pointers don't need to be dereferenced -func swalk(t reflect.Type, ix []int, indent string) { - switch t.Kind() { - case reflect.Struct: - for i := 0; i < t.NumField(); i++ { - fld := t.Field(i) - ixx := append(append([]int{}, ix...), i) - swalk(fld.Type, ixx, indent+". ") - } - default: - // everything is either a struct or a field (that's an assumption about Options) - fields = append(fields, field{ix}) - } -} - type sessionOption struct { Name string Type string Current string Default string } - -func showOptions(o *source.Options) []sessionOption { - var out []sessionOption - t := reflect.TypeOf(*o) - swalk(t, []int{}, "") - v := reflect.ValueOf(*o) - do := reflect.ValueOf(*source.DefaultOptions()) - for _, f := range fields { - val := v.FieldByIndex(f.index) - def := do.FieldByIndex(f.index) - tx := t.FieldByIndex(f.index) - is := strVal(val) - was := strVal(def) - out = append(out, sessionOption{ - Name: tx.Name, - Type: tx.Type.String(), - Current: is, - Default: was, - }) - } - sort.Slice(out, func(i, j int) bool { - rd := out[i].Current == out[i].Default - ld := out[j].Current == out[j].Default - if rd != ld { - return ld - } - return out[i].Name < out[j].Name - }) - return out -} - -func strVal(val reflect.Value) string { - switch val.Kind() { - case reflect.Bool: - return fmt.Sprintf("%v", val.Interface()) - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return fmt.Sprintf("%v", val.Interface()) - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return fmt.Sprintf("%v", val.Interface()) - case reflect.Uintptr, reflect.UnsafePointer: - return fmt.Sprintf("0x%x", val.Pointer()) - case reflect.Complex64, reflect.Complex128: - return fmt.Sprintf("%v", val.Complex()) - case reflect.Array, reflect.Slice: - ans := []string{} - for i := 0; i < val.Len(); i++ { - ans = append(ans, strVal(val.Index(i))) - } - sort.Strings(ans) - return fmt.Sprintf("%v", ans) - case reflect.Chan, reflect.Func, reflect.Ptr: - return val.Kind().String() - case reflect.Struct: - var x source.Analyzer - if val.Type() != reflect.TypeOf(x) { - return val.Kind().String() - } - // this is sort of ugly, but usable - str := val.FieldByName("Analyzer").Elem().FieldByName("Doc").String() - ix := strings.Index(str, "\n") - if ix == -1 { - ix = len(str) - } - return str[:ix] - case reflect.String: - return fmt.Sprintf("%q", val.Interface()) - case reflect.Map: - ans := []string{} - iter := val.MapRange() - for iter.Next() { - k := iter.Key() - v := iter.Value() - ans = append(ans, fmt.Sprintf("%s:%s, ", strVal(k), strVal(v))) - } - sort.Strings(ans) - return fmt.Sprintf("%v", ans) - } - return fmt.Sprintf("??%s??", val.Type()) -} diff --git a/gopls/internal/lsp/protocol/enums.go b/gopls/internal/lsp/protocol/enums.go index 82398e22189..87c14d8d553 100644 --- a/gopls/internal/lsp/protocol/enums.go +++ b/gopls/internal/lsp/protocol/enums.go @@ -129,103 +129,46 @@ func formatEnum(f fmt.State, c rune, i int, names []string, unknown string) { } } -func parseEnum(s string, names []string) int { - for i, name := range names { - if s == name { - return i - } - } - return 0 -} - func (e TextDocumentSyncKind) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesTextDocumentSyncKind[:], "TextDocumentSyncKind") } -func ParseTextDocumentSyncKind(s string) TextDocumentSyncKind { - return TextDocumentSyncKind(parseEnum(s, namesTextDocumentSyncKind[:])) -} - func (e MessageType) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesMessageType[:], "MessageType") } -func ParseMessageType(s string) MessageType { - return MessageType(parseEnum(s, namesMessageType[:])) -} - func (e FileChangeType) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesFileChangeType[:], "FileChangeType") } -func ParseFileChangeType(s string) FileChangeType { - return FileChangeType(parseEnum(s, namesFileChangeType[:])) -} - -func ParseWatchKind(s string) WatchKind { - return WatchKind(parseEnum(s, namesWatchKind[:])) -} - func (e CompletionTriggerKind) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesCompletionTriggerKind[:], "CompletionTriggerKind") } -func ParseCompletionTriggerKind(s string) CompletionTriggerKind { - return CompletionTriggerKind(parseEnum(s, namesCompletionTriggerKind[:])) -} - func (e DiagnosticSeverity) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesDiagnosticSeverity[:], "DiagnosticSeverity") } -func ParseDiagnosticSeverity(s string) DiagnosticSeverity { - return DiagnosticSeverity(parseEnum(s, namesDiagnosticSeverity[:])) -} - func (e DiagnosticTag) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesDiagnosticTag[:], "DiagnosticTag") } -func ParseDiagnosticTag(s string) DiagnosticTag { - return DiagnosticTag(parseEnum(s, namesDiagnosticTag[:])) -} - func (e CompletionItemKind) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesCompletionItemKind[:], "CompletionItemKind") } -func ParseCompletionItemKind(s string) CompletionItemKind { - return CompletionItemKind(parseEnum(s, namesCompletionItemKind[:])) -} - func (e InsertTextFormat) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesInsertTextFormat[:], "InsertTextFormat") } -func ParseInsertTextFormat(s string) InsertTextFormat { - return InsertTextFormat(parseEnum(s, namesInsertTextFormat[:])) -} - func (e DocumentHighlightKind) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesDocumentHighlightKind[:], "DocumentHighlightKind") } -func ParseDocumentHighlightKind(s string) DocumentHighlightKind { - return DocumentHighlightKind(parseEnum(s, namesDocumentHighlightKind[:])) -} - func (e SymbolKind) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesSymbolKind[:], "SymbolKind") } -func ParseSymbolKind(s string) SymbolKind { - return SymbolKind(parseEnum(s, namesSymbolKind[:])) -} - func (e TextDocumentSaveReason) Format(f fmt.State, c rune) { formatEnum(f, c, int(e), namesTextDocumentSaveReason[:], "TextDocumentSaveReason") } - -func ParseTextDocumentSaveReason(s string) TextDocumentSaveReason { - return TextDocumentSaveReason(parseEnum(s, namesTextDocumentSaveReason[:])) -} diff --git a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go index d484f8f7413..5e1a7dab207 100644 --- a/gopls/internal/lsp/protocol/span.go +++ b/gopls/internal/lsp/protocol/span.go @@ -23,10 +23,6 @@ func (u DocumentURI) SpanURI() span.URI { return span.URIFromURI(string(u)) // normalizing conversion } -func IsPoint(r Range) bool { - return r.Start.Line == r.End.Line && r.Start.Character == r.End.Character -} - // CompareLocation defines a three-valued comparison over locations, // lexicographically ordered by (URI, Range). func CompareLocation(x, y Location) int { diff --git a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go index a4ea54b95fc..bda6d898a50 100644 --- a/gopls/internal/vulncheck/vulntest/db.go +++ b/gopls/internal/vulncheck/vulntest/db.go @@ -120,13 +120,6 @@ func generateEntries(_ context.Context, archive *txtar.Archive) ([]osv.Entry, er return entries, nil } -func writeVulns(outPath string, vulns []osv.Entry, indent bool) error { - if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil { - return fmt.Errorf("failed to create directory %q: %s", filepath.Dir(outPath), err) - } - return writeJSON(outPath+".json", vulns, indent) -} - func writeEntriesByID(idDir string, entries []osv.Entry, indent bool) error { // Write a directory containing entries by ID. if err := os.MkdirAll(idDir, 0755); err != nil { From 25924d09f1e592efc4c45f8587639cc271e8a1d1 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 3 Nov 2023 18:57:02 -0400 Subject: [PATCH 085/100] go/analysis/passes/nilness: fix bug in method value The expression use(err.Error) generates an ssa.TypeAssert to implement the nil check on the interface value err, but the instruction has no position. This caused nilness to report an invalid diagnostic, and gopls to crash (since it is strict about such things). This change causes SSA to use the position of the Error token, and documents it. And it causes nilness to discard nilness findings against synthesized (!Instruction.Pos.IsValid) operations. Plus, a test. Change-Id: I00081dc9ae8987240061f83ef816e9a21f5d606d Reviewed-on: https://go-review.googlesource.com/c/tools/+/539659 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: Tim King Reviewed-by: Robert Findley --- go/analysis/passes/nilness/nilness.go | 14 +++++++++----- go/analysis/passes/nilness/testdata/src/b/b.go | 9 +++++++++ go/ssa/builder.go | 2 +- go/ssa/ssa.go | 11 ++++++----- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/go/analysis/passes/nilness/nilness.go b/go/analysis/passes/nilness/nilness.go index d1ca0748e86..c4999f7a9db 100644 --- a/go/analysis/passes/nilness/nilness.go +++ b/go/analysis/passes/nilness/nilness.go @@ -38,11 +38,15 @@ func run(pass *analysis.Pass) (interface{}, error) { func runFunc(pass *analysis.Pass, fn *ssa.Function) { reportf := func(category string, pos token.Pos, format string, args ...interface{}) { - pass.Report(analysis.Diagnostic{ - Pos: pos, - Category: category, - Message: fmt.Sprintf(format, args...), - }) + // We ignore nil-checking ssa.Instructions + // that don't correspond to syntax. + if pos.IsValid() { + pass.Report(analysis.Diagnostic{ + Pos: pos, + Category: category, + Message: fmt.Sprintf(format, args...), + }) + } } // notNil reports an error if v is provably nil. diff --git a/go/analysis/passes/nilness/testdata/src/b/b.go b/go/analysis/passes/nilness/testdata/src/b/b.go index d31f6fb9047..3e686a6cbc4 100644 --- a/go/analysis/passes/nilness/testdata/src/b/b.go +++ b/go/analysis/passes/nilness/testdata/src/b/b.go @@ -30,3 +30,12 @@ func i(x []int) { _ = *a } } + +func _(err error) { + if err == nil { + err.Error() // want "nil dereference in dynamic method call" + + // SSA uses TypeAssert for the nil check in a method value: + _ = err.Error // want "nil dereference in type assertion" + } +} diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 7ec589f7262..b64c50b4349 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -821,7 +821,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { } else { // non-type param interface // Emit nil check: typeassert v.(I). - emitTypeAssert(fn, v, rt, token.NoPos) + emitTypeAssert(fn, v, rt, e.Sel.Pos()) } } if targs := receiverTypeArgs(obj); len(targs) > 0 { diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index e913c1da1f2..58a641a1fdb 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -1077,11 +1077,12 @@ type Next struct { // Type() reflects the actual type of the result, possibly a // 2-types.Tuple; AssertedType is the asserted type. // -// Pos() returns the ast.CallExpr.Lparen if the instruction arose from -// an explicit T(e) conversion; the ast.TypeAssertExpr.Lparen if the -// instruction arose from an explicit e.(T) operation; or the -// ast.CaseClause.Case if the instruction arose from a case of a -// type-switch statement. +// Depending on the TypeAssert's purpose, Pos may return: +// - the ast.CallExpr.Lparen of an explicit T(e) conversion; +// - the ast.TypeAssertExpr.Lparen of an explicit e.(T) operation; +// - the ast.CaseClause.Case of a case of a type-switch statement; +// - the Ident(m).NamePos of an interface method value i.m +// (for which TypeAssert may be used to effect the nil check). // // Example printed form: // From 28813181554eccb458cbee41ff5cb639af0abb3f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 1 Nov 2023 14:13:49 -0400 Subject: [PATCH 086/100] gopls/internal/lsp/source: enable nilness Analyzer in gopls This change enables the nilness Analyser in gopls, which was previously disabled due to the high asymptotic cost of SSA construction in the buildssa Analyzer. That problem has since been fixed by a series of changes to go/ssa to avoid the need to call CreatePackage for indirect dependencies. There is still work to do to reduce the cost of SSA-based analyzers that use facts (not that we have any today), as these must be run on the entire workspace, and the cost of "deep" fact encoding is a hotspot; see CL 513375. There is also still work to do to port the changes to go/ssa and buildssa into ir and buildir, the forks of these packages used by honnef.co/staticcheck. It does use facts. Still, progress. Plus a test. Change-Id: I61bb045752a53ef19ae4651deee25c6d948ce2fc Reviewed-on: https://go-review.googlesource.com/c/tools/+/538802 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/analysis/validate.go | 2 ++ gopls/doc/analyzers.md | 2 +- gopls/internal/lsp/cache/analysis.go | 2 +- gopls/internal/lsp/source/api_json.go | 9 +++++---- gopls/internal/lsp/source/options.go | 2 +- .../regtest/marker/testdata/diagnostics/analyzers.txt | 8 +++++++- .../regtest/marker/testdata/references/issue60676.txt | 3 ++- 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/go/analysis/validate.go b/go/analysis/validate.go index 9da5692af5e..4f2c4045622 100644 --- a/go/analysis/validate.go +++ b/go/analysis/validate.go @@ -19,6 +19,8 @@ import ( // that the Requires graph is acyclic; // that analyzer fact types are unique; // that each fact type is a pointer. +// +// Analyzer names need not be unique, though this may be confusing. func Validate(analyzers []*Analyzer) error { // Map each fact type to its sole generating analyzer. factTypes := make(map[reflect.Type]*Analyzer) diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 55c199ce3eb..ef1449013f6 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -386,7 +386,7 @@ and: panic(p) } -**Disabled by default. Enable it by setting `"analyses": {"nilness": true}`.** +**Enabled by default.** ## **printf** diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go index 600444094ae..ae666ba9111 100644 --- a/gopls/internal/lsp/cache/analysis.go +++ b/gopls/internal/lsp/cache/analysis.go @@ -442,7 +442,7 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, if !ok { // Although this 'skip' operation is logically sound, // it is nonetheless surprising that its absence should - // cause #60909 since none of the analyzers added for + // cause #60909 since none of the analyzers currently added for // requirements (e.g. ctrlflow, inspect, buildssa) // is capable of reporting diagnostics. if summary := root.summary.Actions[stableNames[a]]; summary != nil { diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 429453edf42..b6e5fbd3b85 100644 --- a/gopls/internal/lsp/source/api_json.go +++ b/gopls/internal/lsp/source/api_json.go @@ -338,7 +338,7 @@ var GeneratedAPIJSON = &APIJSON{ { Name: "\"nilness\"", Doc: "check for redundant or impossible nil comparisons\n\nThe nilness checker inspects the control-flow graph of each function in\na package and reports nil pointer dereferences, degenerate nil\npointers, and panics with nil values. A degenerate comparison is of the form\nx==nil or x!=nil where x is statically known to be nil or non-nil. These are\noften a mistake, especially in control flow related to errors. Panics with nil\nvalues are checked because they are not detectable by\n\n\tif r := recover(); r != nil {\n\nThis check reports conditions such as:\n\n\tif f == nil { // impossible condition (f is a function)\n\t}\n\nand:\n\n\tp := &v\n\t...\n\tif p != nil { // tautological condition\n\t}\n\nand:\n\n\tif p == nil {\n\t\tprint(*p) // nil dereference\n\t}\n\nand:\n\n\tif p == nil {\n\t\tpanic(p)\n\t}", - Default: "false", + Default: "true", }, { Name: "\"printf\"", @@ -1093,9 +1093,10 @@ var GeneratedAPIJSON = &APIJSON{ Default: true, }, { - Name: "nilness", - Doc: "check for redundant or impossible nil comparisons\n\nThe nilness checker inspects the control-flow graph of each function in\na package and reports nil pointer dereferences, degenerate nil\npointers, and panics with nil values. A degenerate comparison is of the form\nx==nil or x!=nil where x is statically known to be nil or non-nil. These are\noften a mistake, especially in control flow related to errors. Panics with nil\nvalues are checked because they are not detectable by\n\n\tif r := recover(); r != nil {\n\nThis check reports conditions such as:\n\n\tif f == nil { // impossible condition (f is a function)\n\t}\n\nand:\n\n\tp := &v\n\t...\n\tif p != nil { // tautological condition\n\t}\n\nand:\n\n\tif p == nil {\n\t\tprint(*p) // nil dereference\n\t}\n\nand:\n\n\tif p == nil {\n\t\tpanic(p)\n\t}", - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilness", + Name: "nilness", + Doc: "check for redundant or impossible nil comparisons\n\nThe nilness checker inspects the control-flow graph of each function in\na package and reports nil pointer dereferences, degenerate nil\npointers, and panics with nil values. A degenerate comparison is of the form\nx==nil or x!=nil where x is statically known to be nil or non-nil. These are\noften a mistake, especially in control flow related to errors. Panics with nil\nvalues are checked because they are not detectable by\n\n\tif r := recover(); r != nil {\n\nThis check reports conditions such as:\n\n\tif f == nil { // impossible condition (f is a function)\n\t}\n\nand:\n\n\tp := &v\n\t...\n\tif p != nil { // tautological condition\n\t}\n\nand:\n\n\tif p == nil {\n\t\tprint(*p) // nil dereference\n\t}\n\nand:\n\n\tif p == nil {\n\t\tpanic(p)\n\t}", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilness", + Default: true, }, { Name: "printf", diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index cb8f0811076..74f9bed8d00 100644 --- a/gopls/internal/lsp/source/options.go +++ b/gopls/internal/lsp/source/options.go @@ -1632,7 +1632,7 @@ func defaultAnalyzers() map[string]*Analyzer { atomicalign.Analyzer.Name: {Analyzer: atomicalign.Analyzer, Enabled: true}, deepequalerrors.Analyzer.Name: {Analyzer: deepequalerrors.Analyzer, Enabled: true}, fieldalignment.Analyzer.Name: {Analyzer: fieldalignment.Analyzer, Enabled: false}, - nilness.Analyzer.Name: {Analyzer: nilness.Analyzer, Enabled: false}, + nilness.Analyzer.Name: {Analyzer: nilness.Analyzer, Enabled: true}, shadow.Analyzer.Name: {Analyzer: shadow.Analyzer, Enabled: false}, sortslice.Analyzer.Name: {Analyzer: sortslice.Analyzer, Enabled: true}, testinggoroutine.Analyzer.Name: {Analyzer: testinggoroutine.Analyzer, Enabled: true}, diff --git a/gopls/internal/regtest/marker/testdata/diagnostics/analyzers.txt b/gopls/internal/regtest/marker/testdata/diagnostics/analyzers.txt index e98674b94f4..837a1163a52 100644 --- a/gopls/internal/regtest/marker/testdata/diagnostics/analyzers.txt +++ b/gopls/internal/regtest/marker/testdata/diagnostics/analyzers.txt @@ -1,5 +1,5 @@ Test of warning diagnostics from various analyzers: -copylocks, printf, slog, tests, and timeformat. +copylocks, printf, slog, tests, timeformat, and nilness. -- go.mod -- module example.com @@ -49,3 +49,9 @@ func _() { fmt.Println(now.Format("2006-02-01")) //@diag("2006-02-01", re"2006-02-01 should be 2006-01-02") } +// nilness +func _(ptr *int) { + if ptr == nil { + _ = *ptr //@diag("*ptr", re"nil dereference in load") + } +} diff --git a/gopls/internal/regtest/marker/testdata/references/issue60676.txt b/gopls/internal/regtest/marker/testdata/references/issue60676.txt index cacf6fd4cff..98f608ee10c 100644 --- a/gopls/internal/regtest/marker/testdata/references/issue60676.txt +++ b/gopls/internal/regtest/marker/testdata/references/issue60676.txt @@ -62,8 +62,9 @@ func _() { } x.G = "hi" //@refs("G", GDef, "G") _ = x.E //@refs("E", EDef, "E") +} - var y b.BI +func _(y b.BI) { _ = y.M //@refs("M", MDef, "M") _ = y.N //@refs("N", NDef, "N") } From 4df4d8ddaa05895d3d17abf5d4ed290a83fdd317 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 4 Nov 2023 12:32:56 -0400 Subject: [PATCH 087/100] internal/cmd/deadcode: support -json, -format=template This change adds support for JSON output and text/template formatting of output records, in the manner of 'go list (-f|-json)'. Plus test. Also, the -generated flag is now enabled by default, and it affects all output modes. Plus test. Updates golang/go#63501 Change-Id: I1374abad78d800f92739de5c75b28e6e5189caa1 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539661 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- internal/cmd/deadcode/deadcode.go | 134 ++++++++++++++---- internal/cmd/deadcode/deadcode_test.go | 47 ++++-- internal/cmd/deadcode/doc.go | 37 ++++- .../cmd/deadcode/testdata/generated.txtar | 28 ++++ internal/cmd/deadcode/testdata/jsonflag.txtar | 20 +++ internal/cmd/deadcode/testdata/lineflag.txtar | 4 +- 6 files changed, 231 insertions(+), 39 deletions(-) create mode 100644 internal/cmd/deadcode/testdata/generated.txtar create mode 100644 internal/cmd/deadcode/testdata/jsonflag.txtar diff --git a/internal/cmd/deadcode/deadcode.go b/internal/cmd/deadcode/deadcode.go index ecb9f9f12a8..e2235ba0af0 100644 --- a/internal/cmd/deadcode/deadcode.go +++ b/internal/cmd/deadcode/deadcode.go @@ -7,11 +7,14 @@ package main import ( + "bytes" _ "embed" + "encoding/json" "flag" "fmt" "go/ast" "go/token" + "html/template" "io" "log" "os" @@ -36,8 +39,9 @@ var ( tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)") filterFlag = flag.String("filter", "", "report only packages matching this regular expression (default: module of first package)") - generatedFlag = flag.Bool("generated", true, "report dead functions in generated Go files") - lineFlag = flag.Bool("line", false, "show output in a line-oriented format") + generatedFlag = flag.Bool("generated", false, "include dead functions in generated Go files") + formatFlag = flag.String("format", "", "format output records using template") + jsonFlag = flag.Bool("json", false, "output JSON records") cpuProfile = flag.String("cpuprofile", "", "write CPU profile to this file") memProfile = flag.String("memprofile", "", "write memory profile to this file") ) @@ -91,6 +95,18 @@ func main() { }() } + var tmpl *template.Template + if *formatFlag != "" { + if *jsonFlag { + log.Fatalf("you cannot specify both -format=template and -json") + } + var err error + tmpl, err = template.New("deadcode").Parse(*formatFlag) + if err != nil { + log.Fatalf("invalid -format: %v", err) + } + } + // Load, parse, and type-check the complete program(s). cfg := &packages.Config{ BuildFlags: []string{"-tags=" + *tagsFlag}, @@ -108,17 +124,15 @@ func main() { log.Fatalf("packages contain errors") } - // (Optionally) gather names of generated files. + // Gather names of generated files. generated := make(map[string]bool) - if !*generatedFlag { - packages.Visit(initial, nil, func(p *packages.Package) { - for _, file := range p.Syntax { - if isGenerated(file) { - generated[p.Fset.File(file.Pos()).Name()] = true - } + 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 == "" { @@ -193,12 +207,6 @@ func main() { posn := prog.Fset.Position(fn.Pos()) - // If -generated=false, skip functions declared in generated Go files. - // (Functions called by them may still be reported as dead.) - if generated[posn.Filename] { - continue - } - if !reachablePosn[posn] { reachablePosn[posn] = true // suppress dups with same pos @@ -212,6 +220,8 @@ func main() { } } + var packages []jsonPackage + // Report dead functions grouped by packages. // TODO(adonovan): use maps.Keys, twice. pkgpaths := make([]string, 0, len(byPkgPath)) @@ -243,18 +253,68 @@ func main() { return xposn.Line < yposn.Line }) - if *lineFlag { - // line-oriented output - for _, fn := range fns { - fmt.Println(fn) + var functions []jsonFunction + for _, fn := range fns { + posn := prog.Fset.Position(fn.Pos()) + + // Without -generated, skip functions declared in + // generated Go files. + // (Functions called by them may still be reported.) + gen := generated[posn.Filename] + if gen && !*generatedFlag { + continue } - } else { - // functions grouped by package - fmt.Printf("package %q\n", pkgpath) - for _, fn := range fns { - fmt.Printf("\tfunc %s\n", fn.RelString(fn.Pkg.Pkg)) + + functions = append(functions, jsonFunction{ + Name: fn.String(), + RelName: fn.RelString(fn.Pkg.Pkg), + Posn: posn.String(), + Generated: gen, + }) + } + packages = append(packages, jsonPackage{ + Path: pkgpath, + Funcs: functions, + }) + } + + // Format the output, in the manner of 'go list (-json|-f=template)'. + switch { + case *jsonFlag: + // -json + out, err := json.MarshalIndent(packages, "", "\t") + if err != nil { + log.Fatalf("internal error: %v", err) + } + os.Stdout.Write(out) + + case tmpl != nil: + // -format=template + for _, p := range packages { + var buf bytes.Buffer + if err := tmpl.Execute(&buf, p); err != nil { + log.Fatal(err) + } + if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' { + buf.WriteByte('\n') + } + os.Stdout.Write(buf.Bytes()) + } + + default: + // functions grouped by package + for _, pkg := range packages { + seen := false + for _, fn := range pkg.Funcs { + if !seen { + seen = true + fmt.Printf("package %q\n", pkg.Path) + } + fmt.Printf("\tfunc %s\n", fn.RelName) + } + if seen { + fmt.Println() } - fmt.Println() } } } @@ -297,3 +357,23 @@ func generator(file *ast.File) (string, bool) { } return "", false } + +// -- output protocol (for JSON or text/template) -- + +// Keep in sync with doc comment! + +type jsonFunction struct { + Name string // name (with package qualifier) + RelName string // name (sans package qualifier) + Posn string // position in form "filename:line:col" + Generated bool // function is declared in a generated .go file +} + +func (f jsonFunction) String() string { return f.Name } + +type jsonPackage struct { + Path string + Funcs []jsonFunction +} + +func (p jsonPackage) String() string { return p.Path } diff --git a/internal/cmd/deadcode/deadcode_test.go b/internal/cmd/deadcode/deadcode_test.go index ab8c81c86f0..17c588b134e 100644 --- a/internal/cmd/deadcode/deadcode_test.go +++ b/internal/cmd/deadcode/deadcode_test.go @@ -48,8 +48,9 @@ func Test(t *testing.T) { // Parse archive comment as directives of these forms: // // deadcode args... command-line arguments - // [!]want "quoted" expected/unwanted string in output + // [!]want arg expected/unwanted string in output // + // Args may be Go-quoted strings. var args []string want := make(map[string]bool) // string -> sense for _, line := range strings.Split(string(ar.Comment), "\n") { @@ -58,17 +59,18 @@ func Test(t *testing.T) { continue // skip blanks and comments } - fields := strings.Fields(line) - switch kind := fields[0]; kind { + words, err := words(line) + if err != nil { + t.Fatalf("cannot break line into words: %v (%s)", err, line) + } + switch kind := words[0]; kind { case "deadcode": - args = fields[1:] // lossy wrt spaces + args = words[1:] case "want", "!want": - rest := line[len(kind):] - str, err := strconv.Unquote(strings.TrimSpace(rest)) - if err != nil { - t.Fatalf("bad %s directive <<%s>>", kind, line) + if len(words) != 2 { + t.Fatalf("'want' directive needs argument <<%s>>", line) } - want[str] = kind[0] != '!' + want[words[1]] = kind[0] != '!' default: t.Fatalf("%s: invalid directive %q", filename, kind) } @@ -129,3 +131,30 @@ func buildDeadcode(t *testing.T) string { } return bin } + +// words breaks a string into words, respecting +// Go string quotations around words with spaces. +func words(s string) ([]string, error) { + var words []string + for s != "" { + if s[0] == ' ' { + s = s[1:] + continue + } + var word string + if s[0] == '"' || s[0] == '`' { + prefix, err := strconv.QuotedPrefix(s) + if err != nil { + return nil, err + } + s = s[len(prefix):] + word, _ = strconv.Unquote(prefix) + } else { + prefix, rest, _ := strings.Cut(s, " ") + s = rest + word = prefix + } + words = append(words, word) + } + return words, nil +} diff --git a/internal/cmd/deadcode/doc.go b/internal/cmd/deadcode/doc.go index cdd24e958d9..e125c44714b 100644 --- a/internal/cmd/deadcode/doc.go +++ b/internal/cmd/deadcode/doc.go @@ -39,6 +39,10 @@ will fail to recognize that calls to a linkname-annotated function with no body in fact dispatch to the function named in the annotation. This may result in the latter function being spuriously reported as dead. +By default, the tool does not report dead functions in generated files, +as determined by the special comment described in +https://go.dev/s/generatedcode. Use the -generated flag to include them. + In any case, just because a function is reported as dead does not mean it is unconditionally safe to delete it. For example, a dead function may be referenced (by another dead function), and a dead method may be @@ -48,9 +52,40 @@ Some judgement is required. The analysis is valid only for a single GOOS/GOARCH/-tags configuration, so a function reported as dead may be live in a different configuration. Consider running the tool once for each configuration of interest. -Use the -line flag to emit a line-oriented output that makes it +Consider using a line-oriented output format (see below) to make it easier to compute the intersection of results across all runs. +# Output + +The command supports three output formats. + +With no flags, the command prints dead functions grouped by package. + +With the -json flag, the command prints an array of JSON Package +objects, as defined by this schema: + + type Package struct { + Path string + Funcs []Function + } + + type Function struct { + Name string // name (with package qualifier) + RelName string // name (sans package qualifier) + Posn string // position in form "filename:line:col" + Generated bool // function is declared in a generated .go file + } + +With the -format=template flag, the command executes the specified template +on each Package record. So, this template produces a result similar to the +default format: + + -format='{{printf "package %q\n" .Path}}{{range .Funcs}}{{println "\tfunc " .RelName}}{{end}}{{println}}' + +And this template shows only the list of source positions of dead functions: + + -format='{{range .Funcs}}{{println .Posn}}{{end}}' + THIS TOOL IS EXPERIMENTAL and its interface may change. At some point it may be published at cmd/deadcode. In the meantime, please give us feedback at github.com/golang/go/issues. diff --git a/internal/cmd/deadcode/testdata/generated.txtar b/internal/cmd/deadcode/testdata/generated.txtar new file mode 100644 index 00000000000..4a50a6eb543 --- /dev/null +++ b/internal/cmd/deadcode/testdata/generated.txtar @@ -0,0 +1,28 @@ +# Test of -generated flag output. + + deadcode example.com +!want "main" + want "Dead1" +!want "Dead2" + + deadcode -generated example.com +!want "main" + want "Dead1" + want "Dead2" + +-- go.mod -- +module example.com +go 1.18 + +-- main.go -- +package main + +func main() {} +func Dead1() {} + +-- gen.go -- +// Code generated by hand. DO NOT EDIT. + +package main + +func Dead2() {} \ No newline at end of file diff --git a/internal/cmd/deadcode/testdata/jsonflag.txtar b/internal/cmd/deadcode/testdata/jsonflag.txtar new file mode 100644 index 00000000000..cbf80ec1322 --- /dev/null +++ b/internal/cmd/deadcode/testdata/jsonflag.txtar @@ -0,0 +1,20 @@ +# Very minimal test of -json flag. + +deadcode -json example.com/p + + want `"Path": "example.com/p",` + want `"Name": "example.com/p.Dead",` + want `"RelName": "Dead",` + want `"Generated": false` + +-- go.mod -- +module example.com +go 1.18 + +-- p/p.go -- +package main + +func Dead() {} + +func main() {} + diff --git a/internal/cmd/deadcode/testdata/lineflag.txtar b/internal/cmd/deadcode/testdata/lineflag.txtar index b817e4cde90..e629e0573b1 100644 --- a/internal/cmd/deadcode/testdata/lineflag.txtar +++ b/internal/cmd/deadcode/testdata/lineflag.txtar @@ -1,6 +1,6 @@ -# Test of -line output. +# Test of line-oriented output. - deadcode -line -filter= example.com + deadcode "-format={{range .Funcs}}{{println .Name}}{{end}}" -filter= example.com want "(example.com.T).Goodbye" !want "(example.com.T).Hello" From 2ddaad75d3d3415451d28d5b297384478a20f970 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Wed, 1 Nov 2023 16:14:51 -0400 Subject: [PATCH 088/100] gopls/internal/lsp/cache: isolate getWorkspaceInformation from Session getWorkspaceInformation will be a foundation of the zero-config gopls algorithm, which we will want to be unit-testable. Therefore, separate it from the session (it needs only a go command runner and file source). While at it, separate workspace information from folder information. For golang/go#57979 Change-Id: I79d6ad622dbd6f76afe979972a21b852f68ecf4d Reviewed-on: https://go-review.googlesource.com/c/tools/+/538803 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Auto-Submit: Robert Findley Reviewed-by: Hyang-Ah Hana Kim --- gopls/internal/lsp/cache/session.go | 8 +++---- gopls/internal/lsp/cache/view.go | 35 +++++++++++++---------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 4a03578e412..aa8196ba432 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -101,7 +101,7 @@ func (s *Session) createView(ctx context.Context, folder *Folder, seqID uint64) index := atomic.AddInt64(&viewIndex, 1) // Get immutable workspace information. - info, err := s.getWorkspaceInformation(ctx, folder.Dir, folder.Options) + info, err := getWorkspaceInformation(ctx, s.gocmdRunner, s, folder) if err != nil { return nil, nil, nil, err } @@ -419,7 +419,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // synchronously to change processing? Can we assume that the env did not // change, and derive go.work using a combination of the configured // GOWORK value and filesystem? - info, err := s.getWorkspaceInformation(ctx, view.folder.Dir, view.folder.Options) + info, err := getWorkspaceInformation(ctx, s.gocmdRunner, s, view.folder) if err != nil { // Catastrophic failure, equivalent to a failure of session // initialization and therefore should almost never happen. One @@ -430,9 +430,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // TODO(rfindley): consider surfacing this error more loudly. We // could report a bug, but it's not really a bug. event.Error(ctx, "fetching workspace information", err) - } - - if info != view.workspaceInformation { + } else if *info != *view.workspaceInformation { if err := s.updateViewLocked(ctx, view, view.folder); err != nil { // More catastrophic failure. The view may or may not still exist. // The best we can do is log and move on. diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 26889d071c9..7223eecad0a 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -61,7 +61,7 @@ type View struct { // Workspace information. The fields below are immutable, and together with // options define the build list. Any change to these fields results in a new // View. - workspaceInformation // Go environment information + *workspaceInformation // Go environment information importsState *importsState @@ -122,9 +122,6 @@ type View struct { // // This type is compared to see if the View needs to be reconstructed. type workspaceInformation struct { - // folder is the LSP workspace folder. - folder span.URI - // `go env` variables that need to be tracked by gopls. goEnv @@ -901,27 +898,25 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]sourc return v.snapshot, v.snapshot.Acquire() } -func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, options *source.Options) (workspaceInformation, error) { - if err := checkPathCase(folder.Filename()); err != nil { - return workspaceInformation{}, fmt.Errorf("invalid workspace folder path: %w; check that the casing of the configured workspace folder path agrees with the casing reported by the operating system", err) +func getWorkspaceInformation(ctx context.Context, runner *gocommand.Runner, fs source.FileSource, folder *Folder) (*workspaceInformation, error) { + if err := checkPathCase(folder.Dir.Filename()); err != nil { + return nil, fmt.Errorf("invalid workspace folder path: %w; check that the casing of the configured workspace folder path agrees with the casing reported by the operating system", err) } + info := new(workspaceInformation) var err error - info := workspaceInformation{ - folder: folder, - } inv := gocommand.Invocation{ - WorkingDir: folder.Filename(), - Env: options.EnvSlice(), + WorkingDir: folder.Dir.Filename(), + Env: folder.Options.EnvSlice(), } - info.goversion, err = gocommand.GoVersion(ctx, inv, s.gocmdRunner) + info.goversion, err = gocommand.GoVersion(ctx, inv, runner) if err != nil { return info, err } - info.goversionOutput, err = gocommand.GoVersionOutput(ctx, inv, s.gocmdRunner) + info.goversionOutput, err = gocommand.GoVersionOutput(ctx, inv, runner) if err != nil { return info, err } - if err := info.load(ctx, folder.Filename(), options.EnvSlice(), s.gocmdRunner); err != nil { + if err := info.load(ctx, folder.Dir.Filename(), folder.Options.EnvSlice(), runner); err != nil { return info, err } // The value of GOPACKAGESDRIVER is not returned through the go command. @@ -933,15 +928,15 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, // filterFunc is the path filter function for this workspace folder. Notably, // it is relative to folder (which is specified by the user), not root. - filterFunc := pathExcludedByFilterFunc(folder.Filename(), info.gomodcache, options) - info.gomod, err = findWorkspaceModFile(ctx, folder, s, filterFunc) + filterFunc := pathExcludedByFilterFunc(folder.Dir.Filename(), info.gomodcache, folder.Options) + info.gomod, err = findWorkspaceModFile(ctx, folder.Dir, fs, filterFunc) if err != nil { return info, err } // Check if the workspace is within any GOPATH directory. for _, gp := range filepath.SplitList(info.gopath) { - if source.InDir(filepath.Join(gp, "src"), folder.Filename()) { + if source.InDir(filepath.Join(gp, "src"), folder.Dir.Filename()) { info.inGOPATH = true break } @@ -955,10 +950,10 @@ func (s *Session) getWorkspaceInformation(ctx context.Context, folder span.URI, // // TODO(golang/go#57514): eliminate the expandWorkspaceToModule setting // entirely. - if options.ExpandWorkspaceToModule && info.gomod != "" { + if folder.Options.ExpandWorkspaceToModule && info.gomod != "" { info.goCommandDir = span.URIFromPath(filepath.Dir(info.gomod.Filename())) } else { - info.goCommandDir = folder + info.goCommandDir = folder.Dir } return info, nil } From d2c415da4dcab6c8aba4e7a79b7eb24131b05599 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 2 Nov 2023 14:08:27 -0400 Subject: [PATCH 089/100] gopls/internal/lsp/cache: pass workspace information into createView As a first step toward inverting control of View construction, pass in the workspace information rather than querying it inside createView. This avoids a redundant call to getWorkspaceInformation (which invokes the Go command) during invalidation. For golang/go#57979 Change-Id: I6e5308dd7c57c32f1a03c09f501aac8ffeb369e8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539657 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/cache/session.go | 20 +++++++++----------- gopls/internal/lsp/cache/view.go | 6 +++++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index aa8196ba432..925bacd824a 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -84,7 +84,11 @@ func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, source.Sn return nil, nil, nil, source.ErrViewExists } } - view, snapshot, release, err := s.createView(ctx, folder, 0) + info, err := getWorkspaceInformation(ctx, s.gocmdRunner, s, folder) + if err != nil { + return nil, nil, nil, err + } + view, snapshot, release, err := s.createView(ctx, info, folder, 0) if err != nil { return nil, nil, nil, err } @@ -97,15 +101,9 @@ func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, source.Sn // TODO(rfindley): clarify that createView can never be cancelled (with the // possible exception of server shutdown). // On success, the caller becomes responsible for calling the release function once. -func (s *Session) createView(ctx context.Context, folder *Folder, seqID uint64) (*View, *snapshot, func(), error) { +func (s *Session) createView(ctx context.Context, info *workspaceInformation, folder *Folder, seqID uint64) (*View, *snapshot, func(), error) { index := atomic.AddInt64(&viewIndex, 1) - // Get immutable workspace information. - info, err := getWorkspaceInformation(ctx, s.gocmdRunner, s, folder) - if err != nil { - return nil, nil, nil, err - } - gowork, _ := info.GOWORK() wsModFiles, wsModFilesErr := computeWorkspaceModFiles(ctx, info.gomod, gowork, info.effectiveGO111MODULE(), s) @@ -289,7 +287,7 @@ func (s *Session) RemoveView(view *View) { // // If the resulting error is non-nil, the view may or may not have already been // dropped from the session. -func (s *Session) updateViewLocked(ctx context.Context, view *View, folder *Folder) error { +func (s *Session) updateViewLocked(ctx context.Context, view *View, info *workspaceInformation, folder *Folder) error { // Preserve the snapshot ID if we are recreating the view. view.snapshotMu.Lock() if view.snapshot == nil { @@ -304,7 +302,7 @@ func (s *Session) updateViewLocked(ctx context.Context, view *View, folder *Fold return fmt.Errorf("view %q not found", view.id) } - v, snapshot, release, err := s.createView(ctx, folder, seqID) + v, snapshot, release, err := s.createView(ctx, info, folder, seqID) if err != nil { // we have dropped the old view, but could not create the new one // this should not happen and is very bad, but we still need to clean @@ -431,7 +429,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // could report a bug, but it's not really a bug. event.Error(ctx, "fetching workspace information", err) } else if *info != *view.workspaceInformation { - if err := s.updateViewLocked(ctx, view, view.folder); err != nil { + if err := s.updateViewLocked(ctx, view, info, view.folder); err != nil { // More catastrophic failure. The view may or may not still exist. // The best we can do is log and move on. event.Error(ctx, "recreating view", err) diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 7223eecad0a..b0157ee8b6c 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -431,7 +431,11 @@ func (s *Session) SetFolderOptions(ctx context.Context, uri span.URI, options *s if v.folder.Dir == uri { folder2 := *v.folder folder2.Options = options - if err := s.updateViewLocked(ctx, v, &folder2); err != nil { + info, err := getWorkspaceInformation(ctx, s.gocmdRunner, s, &folder2) + if err != nil { + return err + } + if err := s.updateViewLocked(ctx, v, info, &folder2); err != nil { return err } } From 8b89cfa70c18ad01bf86c5f69e3e2f8f4ed9c98e Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 3 Nov 2023 14:31:35 -0400 Subject: [PATCH 090/100] gopls/internal/lsp/cache: remove forceReloadMetadata from clone We need to simplify Clone for zero config gopls. One source of complexity is the 'forceReloadMetadata' parameter, which reaches inside the clone internals to force invalidation of metadata, and must be threaded through the call stack, originating from the 'InvalidateMetadata' action on a file. Instead, just recreate the View, which will have the same side effect of invoking `go list`. As a nice side-effect, we can actually use the FromRegenerateCgo diagnostic source, which allows us to avoid the unbounded Await in TestRegenerateCgo. For golang/go#57979 Change-Id: I7fa01ed17a9407dfb98844eeec69b1fdbbd27aa7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539658 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/session.go | 44 ++++++++++--------- gopls/internal/lsp/cache/snapshot.go | 4 +- gopls/internal/lsp/cache/view.go | 6 +-- gopls/internal/lsp/command.go | 36 ++++++++++++--- gopls/internal/lsp/diagnostics.go | 9 +++- gopls/internal/lsp/source/view.go | 3 -- .../regtest/codelens/codelens_test.go | 6 ++- 7 files changed, 71 insertions(+), 37 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 925bacd824a..47b69af8bc5 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -287,28 +287,34 @@ func (s *Session) RemoveView(view *View) { // // If the resulting error is non-nil, the view may or may not have already been // dropped from the session. -func (s *Session) updateViewLocked(ctx context.Context, view *View, info *workspaceInformation, folder *Folder) error { +func (s *Session) updateViewLocked(ctx context.Context, view *View, info *workspaceInformation, folder *Folder) (*View, error) { // Preserve the snapshot ID if we are recreating the view. view.snapshotMu.Lock() if view.snapshot == nil { view.snapshotMu.Unlock() panic("updateView called after View was already shut down") } + // TODO(rfindley): we should probably increment the sequence ID here. seqID := view.snapshot.sequenceID // Preserve sequence IDs when updating a view in place. view.snapshotMu.Unlock() i := s.dropView(view) if i == -1 { - return fmt.Errorf("view %q not found", view.id) + return nil, fmt.Errorf("view %q not found", view.id) } - v, snapshot, release, err := s.createView(ctx, info, folder, seqID) + var ( + snapshot *snapshot + release func() + err error + ) + view, snapshot, release, err = s.createView(ctx, info, folder, seqID) if err != nil { // we have dropped the old view, but could not create the new one // this should not happen and is very bad, but we still need to clean // up the view array if it happens s.views = removeElement(s.views, i) - return err + return nil, err } defer release() @@ -318,13 +324,13 @@ func (s *Session) updateViewLocked(ctx context.Context, view *View, info *worksp // behavior when configuration is changed mid-session. // // Ensure the new snapshot observes all open files. - for _, o := range v.fs.Overlays() { + for _, o := range view.fs.Overlays() { _, _ = snapshot.ReadFile(ctx, o.URI()) } // substitute the new view into the array where the old view was - s.views[i] = v - return nil + s.views[i] = view + return view, nil } // removeElement removes the ith element from the slice replacing it with the last element. @@ -362,6 +368,14 @@ func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModifica return err } +// ResetView resets the best view for the given URI. +func (s *Session) ResetView(ctx context.Context, uri span.URI) (*View, error) { + s.viewMu.Lock() + defer s.viewMu.Unlock() + v := bestViewForURI(uri, s.views) + return s.updateViewLocked(ctx, v, v.workspaceInformation, v.folder) +} + // DidModifyFiles reports a file modification to the session. It returns // the new snapshots after the modifications have been applied, paired with // the affected file URIs for those snapshots. @@ -401,7 +415,6 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // Change, InvalidateMetadata, and UnknownFileAction actions do not cause // us to re-evaluate views. redoViews := (c.Action != source.Change && - c.Action != source.InvalidateMetadata && c.Action != source.UnknownFileAction) if redoViews { @@ -429,7 +442,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // could report a bug, but it's not really a bug. event.Error(ctx, "fetching workspace information", err) } else if *info != *view.workspaceInformation { - if err := s.updateViewLocked(ctx, view, info, view.folder); err != nil { + if _, err := s.updateViewLocked(ctx, view, info, view.folder); err != nil { // More catastrophic failure. The view may or may not still exist. // The best we can do is log and move on. event.Error(ctx, "recreating view", err) @@ -441,13 +454,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif // Collect information about views affected by these changes. views := make(map[*View]map[span.URI]source.FileHandle) affectedViews := map[span.URI][]*View{} - // forceReloadMetadata records whether any change is the magic - // source.InvalidateMetadata action. - forceReloadMetadata := false for _, c := range changes { - if c.Action == source.InvalidateMetadata { - forceReloadMetadata = true - } // Build the list of affected views. var changedViews []*View for _, view := range s.views { @@ -487,7 +494,7 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif var releases []func() viewToSnapshot := map[*View]*snapshot{} for view, changed := range views { - snapshot, release := view.invalidateContent(ctx, changed, forceReloadMetadata) + snapshot, release := view.invalidateContent(ctx, changed) releases = append(releases, release) viewToSnapshot[view] = snapshot } @@ -574,11 +581,6 @@ func (fs *overlayFS) updateOverlays(ctx context.Context, changes []source.FileMo defer fs.mu.Unlock() for _, c := range changes { - // Don't update overlays for metadata invalidations. - if c.Action == source.InvalidateMetadata { - continue - } - o, ok := fs.overlays[c.URI] // If the file is not opened in an overlay and the change is on disk, diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index d7f51dd9d21..ba1322d2ea6 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -1813,7 +1813,7 @@ func inVendor(uri span.URI) bool { return found && strings.Contains(after, "/") } -func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source.FileHandle, forceReloadMetadata bool) (*snapshot, func()) { +func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source.FileHandle) (*snapshot, func()) { ctx, done := event.Start(ctx, "cache.snapshot.clone") defer done() @@ -1975,7 +1975,7 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source result.unloadableFiles.Remove(uri) } - invalidateMetadata = invalidateMetadata || forceReloadMetadata || reinit + invalidateMetadata = invalidateMetadata || reinit anyImportDeleted = anyImportDeleted || importDeleted // Mark all of the package IDs containing the given file. diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index b0157ee8b6c..2565151a4df 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -435,7 +435,7 @@ func (s *Session) SetFolderOptions(ctx context.Context, uri span.URI, options *s if err != nil { return err } - if err := s.updateViewLocked(ctx, v, info, &folder2); err != nil { + if _, err := s.updateViewLocked(ctx, v, info, &folder2); err != nil { return err } } @@ -871,7 +871,7 @@ func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadEr // a callback which the caller must invoke to release that snapshot. // // newOptions may be nil, in which case options remain unchanged. -func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]source.FileHandle, forceReloadMetadata bool) (*snapshot, func()) { +func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]source.FileHandle) (*snapshot, func()) { // Detach the context so that content invalidation cannot be canceled. ctx = xcontext.Detach(ctx) @@ -893,7 +893,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]sourc prevSnapshot.AwaitInitialized(ctx) // Save one lease of the cloned snapshot in the view. - v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes, forceReloadMetadata) + v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes) prevReleaseSnapshot() v.destroy(prevSnapshot, "View.invalidateContent") diff --git a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go index 8fe1ec82391..da9d44e0c21 100644 --- a/gopls/internal/lsp/command.go +++ b/gopls/internal/lsp/command.go @@ -17,6 +17,7 @@ import ( "runtime/pprof" "sort" "strings" + "sync" "golang.org/x/mod/modfile" "golang.org/x/tools/go/ast/astutil" @@ -214,12 +215,37 @@ func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs func (c *commandHandler) RegenerateCgo(ctx context.Context, args command.URIArg) error { return c.run(ctx, commandConfig{ progress: "Regenerating Cgo", - }, func(ctx context.Context, deps commandDeps) error { - mod := source.FileModification{ - URI: args.URI.SpanURI(), - Action: source.InvalidateMetadata, + }, func(ctx context.Context, _ commandDeps) error { + var wg sync.WaitGroup // tracks work done on behalf of this function, incl. diagnostics + wg.Add(1) + defer wg.Done() + + // Track progress on this operation for testing. + if c.s.Options().VerboseWorkDoneProgress { + work := c.s.progress.Start(ctx, DiagnosticWorkTitle(FromRegenerateCgo), "Calculating file diagnostics...", nil, nil) + go func() { + wg.Wait() + work.End(ctx, "Done.") + }() + } + + // Resetting the view causes cgo to be regenerated via `go list`. + v, err := c.s.session.ResetView(ctx, args.URI.SpanURI()) + if err != nil { + return err + } + + snapshot, release, err := v.Snapshot() + if err != nil { + return err } - return c.s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo) + wg.Add(1) + go func() { + c.s.diagnoseSnapshot(snapshot, nil, true, 0) + release() + wg.Done() + }() + return nil }) } diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go index c19788fbc54..4ea02cff9bf 100644 --- a/gopls/internal/lsp/diagnostics.go +++ b/gopls/internal/lsp/diagnostics.go @@ -178,6 +178,9 @@ func (s *Server) diagnoseSnapshots(snapshots map[source.Snapshot][]span.URI, onD // If changedURIs is non-empty, it is a set of recently changed files that // should be diagnosed immediately, and onDisk reports whether these file // changes came from a change to on-disk files. +// +// TODO(rfindley): eliminate the onDisk parameter, which looks misplaced. If we +// don't want to diagnose changes on disk, filter out the changedURIs. func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.URI, onDisk bool, delay time.Duration) { ctx := snapshot.BackgroundContext() ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", source.SnapshotLabels(snapshot)...) @@ -203,8 +206,10 @@ func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.U return } - s.diagnoseChangedFiles(ctx, snapshot, changedURIs, onDisk) - s.publishDiagnostics(ctx, false, snapshot) + if len(changedURIs) > 0 { + s.diagnoseChangedFiles(ctx, snapshot, changedURIs, onDisk) + s.publishDiagnostics(ctx, false, snapshot) + } if delay < minDelay { delay = 0 diff --git a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go index 7851d130a6c..46c2eeeb609 100644 --- a/gopls/internal/lsp/source/view.go +++ b/gopls/internal/lsp/source/view.go @@ -738,7 +738,6 @@ const ( Save Create Delete - InvalidateMetadata ) func (a FileAction) String() string { @@ -755,8 +754,6 @@ func (a FileAction) String() string { return "Create" case Delete: return "Delete" - case InvalidateMetadata: - return "InvalidateMetadata" default: return "Unknown" } diff --git a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go index b72e598c913..107db1a2c29 100644 --- a/gopls/internal/regtest/codelens/codelens_test.go +++ b/gopls/internal/regtest/codelens/codelens_test.go @@ -10,6 +10,7 @@ import ( "golang.org/x/tools/gopls/internal/bug" "golang.org/x/tools/gopls/internal/hooks" + "golang.org/x/tools/gopls/internal/lsp" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/gopls/internal/lsp/tests/compare" @@ -343,6 +344,9 @@ func Foo() { // Regenerate cgo, fixing the diagnostic. env.ExecuteCodeLensCommand("cgo.go", command.RegenerateCgo, nil) - env.Await(NoDiagnostics(ForFile("cgo.go"))) + env.OnceMet( + CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromRegenerateCgo), 1, true), + NoDiagnostics(ForFile("cgo.go")), + ) }) } From 4124316da0c55abb58bd81da62d6424cc92f7e59 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 3 Nov 2023 15:13:01 -0400 Subject: [PATCH 091/100] gopls/internal/lsp/cache: remove baseCtx from the View Views don't do work, so should have no need of a context. Instead, chain the contexts inside of clone. Also, fix a latent bug where the context used in the view importsState is the snapshot backgroundCtx rather than view baseCtx: the importsState outlives the snapshot, so should not reference the snapshot context. Fortunately, the context was thus far only used for logging. For golang/go#57979 Change-Id: Icf55d69e82f19b3fd52ca7d9266df2b5589bb36e Reviewed-on: https://go-review.googlesource.com/c/tools/+/539676 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/lsp/cache/session.go | 3 +-- gopls/internal/lsp/cache/snapshot.go | 7 ++++--- gopls/internal/lsp/cache/view.go | 6 +----- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go index 47b69af8bc5..e565f19e1a6 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -118,7 +118,6 @@ func (s *Session) createView(ctx context.Context, info *workspaceInformation, fo folder: folder, initialWorkspaceLoad: make(chan struct{}), initializationSema: make(chan struct{}, 1), - baseCtx: baseCtx, moduleUpgrades: map[span.URI]map[string]string{}, vulns: map[span.URI]*vulncheck.Result{}, parseCache: s.parseCache, @@ -126,7 +125,7 @@ func (s *Session) createView(ctx context.Context, info *workspaceInformation, fo workspaceInformation: info, } v.importsState = &importsState{ - ctx: backgroundCtx, + ctx: baseCtx, processEnv: &imports.ProcessEnv{ GocmdRunner: s.gocmdRunner, SkipPathInScan: func(dir string) bool { diff --git a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go index ba1322d2ea6..47dd1ab714f 100644 --- a/gopls/internal/lsp/cache/snapshot.go +++ b/gopls/internal/lsp/cache/snapshot.go @@ -45,6 +45,7 @@ import ( "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/persistent" "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/xcontext" ) type snapshot struct { @@ -1813,20 +1814,20 @@ func inVendor(uri span.URI) bool { return found && strings.Contains(after, "/") } -func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]source.FileHandle) (*snapshot, func()) { +func (s *snapshot) clone(ctx context.Context, changes map[span.URI]source.FileHandle) (*snapshot, func()) { ctx, done := event.Start(ctx, "cache.snapshot.clone") defer done() s.mu.Lock() defer s.mu.Unlock() - bgCtx, cancel := context.WithCancel(bgCtx) + backgroundCtx, cancel := context.WithCancel(event.Detach(xcontext.Detach(s.backgroundCtx))) result := &snapshot{ sequenceID: s.sequenceID + 1, globalID: nextSnapshotID(), store: s.store, view: s.view, - backgroundCtx: bgCtx, + backgroundCtx: backgroundCtx, cancel: cancel, builtin: s.builtin, initialized: s.initialized, diff --git a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go index 2565151a4df..246669c3290 100644 --- a/gopls/internal/lsp/cache/view.go +++ b/gopls/internal/lsp/cache/view.go @@ -52,10 +52,6 @@ type View struct { gocmdRunner *gocommand.Runner // limits go command concurrency - // baseCtx is the context handed to NewView. This is the parent of all - // background contexts created for this view. - baseCtx context.Context - folder *Folder // Workspace information. The fields below are immutable, and together with @@ -893,7 +889,7 @@ func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]sourc prevSnapshot.AwaitInitialized(ctx) // Save one lease of the cloned snapshot in the view. - v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, v.baseCtx, changes) + v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, changes) prevReleaseSnapshot() v.destroy(prevSnapshot, "View.invalidateContent") From 118c362a56589bbbb0a0950d2d5e8850d5366609 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 3 Nov 2023 15:45:48 -0400 Subject: [PATCH 092/100] gopls/internal/lsp/source: fix signatureHelp with pointer receivers I just ran into this today. Alas, it took us too long to fix this. Fixes golang/go#61189 Change-Id: Iac28a6ba30d099cb2bb6f9d761ee658e190aa07c Reviewed-on: https://go-review.googlesource.com/c/tools/+/539677 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/source/types_format.go | 6 +++++- .../marker/testdata/signature/generic.txt | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 gopls/internal/regtest/marker/testdata/signature/generic.txt diff --git a/gopls/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go index 1fcad26bb11..8ea81f5db27 100644 --- a/gopls/internal/lsp/source/types_format.go +++ b/gopls/internal/lsp/source/types_format.go @@ -312,7 +312,11 @@ func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj * return types.TypeString(obj.Type(), qf), nil // in generic function } if decl.Recv != nil && len(decl.Recv.List) > 0 { - if x, _, _, _ := typeparams.UnpackIndexExpr(decl.Recv.List[0].Type); x != nil { + rtype := decl.Recv.List[0].Type + if e, ok := rtype.(*ast.StarExpr); ok { + rtype = e.X + } + if x, _, _, _ := typeparams.UnpackIndexExpr(rtype); x != nil { return types.TypeString(obj.Type(), qf), nil // in method of generic type } } diff --git a/gopls/internal/regtest/marker/testdata/signature/generic.txt b/gopls/internal/regtest/marker/testdata/signature/generic.txt new file mode 100644 index 00000000000..e99abbf1dad --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/signature/generic.txt @@ -0,0 +1,21 @@ +This test checks signature help on generic signatures. + +-- g.go -- +package g + +type M[K comparable, V any] map[K]V + +// golang/go#61189: signatureHelp must handle pointer receivers. +func (m *M[K, V]) Get(k K) V { + return (*m)[k] +} + +func Get[K comparable, V any](m M[K, V], k K) V { + return m[k] +} + +func _() { + var m M[int, string] + _ = m.Get(0) //@signature("(", "Get(k int) string", 0) + _ = Get(m, 0) //@signature("0", "Get(m M[int, string], k int) string", 1) +} From 2638d6633698ef021f5b0802ae79c64eeddd8bf7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 6 Nov 2023 17:01:39 -0500 Subject: [PATCH 093/100] internal/cmd/deadcode: omit package/func keywords in default output ...and don't quote the package path. Updates golang/go#63501 Change-Id: Ic4f52bf74d7ddd185f2179deed55118971bfa7ed Reviewed-on: https://go-review.googlesource.com/c/tools/+/540218 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- internal/cmd/deadcode/deadcode.go | 4 ++-- internal/cmd/deadcode/doc.go | 2 +- internal/cmd/deadcode/testdata/basic.txtar | 12 ++++++------ internal/cmd/deadcode/testdata/filterflag.txtar | 10 +++++----- internal/cmd/deadcode/testdata/testflag.txtar | 10 +++++----- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/cmd/deadcode/deadcode.go b/internal/cmd/deadcode/deadcode.go index e2235ba0af0..e01c3aaf00b 100644 --- a/internal/cmd/deadcode/deadcode.go +++ b/internal/cmd/deadcode/deadcode.go @@ -308,9 +308,9 @@ func main() { for _, fn := range pkg.Funcs { if !seen { seen = true - fmt.Printf("package %q\n", pkg.Path) + fmt.Println(pkg.Path) } - fmt.Printf("\tfunc %s\n", fn.RelName) + fmt.Printf("\t%s\n", fn.RelName) } if seen { fmt.Println() diff --git a/internal/cmd/deadcode/doc.go b/internal/cmd/deadcode/doc.go index e125c44714b..c4874d6e262 100644 --- a/internal/cmd/deadcode/doc.go +++ b/internal/cmd/deadcode/doc.go @@ -80,7 +80,7 @@ With the -format=template flag, the command executes the specified template on each Package record. So, this template produces a result similar to the default format: - -format='{{printf "package %q\n" .Path}}{{range .Funcs}}{{println "\tfunc " .RelName}}{{end}}{{println}}' + -format='{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .RelName}}{{end}}{{println}}' And this template shows only the list of source positions of dead functions: diff --git a/internal/cmd/deadcode/testdata/basic.txtar b/internal/cmd/deadcode/testdata/basic.txtar index c31d656820b..b0b380a0ecf 100644 --- a/internal/cmd/deadcode/testdata/basic.txtar +++ b/internal/cmd/deadcode/testdata/basic.txtar @@ -2,13 +2,13 @@ deadcode -filter= example.com - want "func (T).Goodbye" -!want "func (T).Hello" - want "func unreferenced" + want "(T).Goodbye" +!want "(T).Hello" + want "unreferenced" - want "func Scanf" - want "func Printf" -!want "func Println" + want "Scanf" + want "Printf" +!want "Println" -- go.mod -- module example.com diff --git a/internal/cmd/deadcode/testdata/filterflag.txtar b/internal/cmd/deadcode/testdata/filterflag.txtar index ca1ec43fcde..70198f750e8 100644 --- a/internal/cmd/deadcode/testdata/filterflag.txtar +++ b/internal/cmd/deadcode/testdata/filterflag.txtar @@ -2,12 +2,12 @@ deadcode -filter=other.net example.com - want `package "other.net"` - want `func Dead` -!want `func Live` + want `other.net` + want `Dead` +!want `Live` -!want `package "example.com"` -!want `func unreferenced` +!want `example.com` +!want `unreferenced` -- go.work -- use example.com diff --git a/internal/cmd/deadcode/testdata/testflag.txtar b/internal/cmd/deadcode/testdata/testflag.txtar index 1ebfd1455c5..6f0c7611a08 100644 --- a/internal/cmd/deadcode/testdata/testflag.txtar +++ b/internal/cmd/deadcode/testdata/testflag.txtar @@ -2,12 +2,12 @@ deadcode -test -filter=example.com example.com/p - want "func Dead" -!want "func Live1" -!want "func Live2" + want "Dead" +!want "Live1" +!want "Live2" - want "func ExampleDead" -!want "func ExampleLive" + want "ExampleDead" +!want "ExampleLive" -- go.mod -- module example.com From b753e58b842175872367e87e377c4669b5037a0c Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 6 Nov 2023 16:21:55 -0500 Subject: [PATCH 094/100] internal/lsp/helper: fix misspelled "Code generated" comment ...as defined by https://go.dev/s/generatedcode. Change-Id: Ia6fee7f28a540d2a4a05590887fa78af19d5fc7e Reviewed-on: https://go-review.googlesource.com/c/tools/+/540217 Reviewed-by: Peter Weinberger LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/helper/helper.go | 2 +- gopls/internal/lsp/server_gen.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/internal/lsp/helper/helper.go b/gopls/internal/lsp/helper/helper.go index 84ac7aa7ba4..187908f3d4d 100644 --- a/gopls/internal/lsp/helper/helper.go +++ b/gopls/internal/lsp/helper/helper.go @@ -57,7 +57,7 @@ var tmpl = `// Copyright 2021 The Go Authors. All rights reserved. package lsp -// code generated by helper. DO NOT EDIT. +// Code generated by gopls/internal/lsp/helper. DO NOT EDIT. import ( "context" diff --git a/gopls/internal/lsp/server_gen.go b/gopls/internal/lsp/server_gen.go index 285faa26db9..7ed9190b789 100644 --- a/gopls/internal/lsp/server_gen.go +++ b/gopls/internal/lsp/server_gen.go @@ -4,7 +4,7 @@ package lsp -// code generated by helper. DO NOT EDIT. +// Code generated by gopls/internal/lsp/helper. DO NOT EDIT. import ( "context" From c538b4e9949aa032ec14c82465b7d70fa34a97fd Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 6 Nov 2023 19:14:32 -0500 Subject: [PATCH 095/100] internal/cmd/deadcode: add -whylive=function flag The -whylive flag explains why a function is live (not dead code), by displaying an arbitrary shortest path to the function from one of the main entrypoints. Call paths from main packages (not tests), from main functions (not init functions), and involving only static calls are preferred. The output logic is unified so that the usual mode and -whylive both compute a list of JSON objects, which are then formatted by -json or -format=template. Also, a test. Also, fix the test framework to correctly handle a sequence of commands(!). Fixes golang/go#61263 Change-Id: I6aafc851c9c57e27a0a8f0d723e20fb2f8b57ad7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/540219 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- internal/cmd/deadcode/deadcode.go | 313 ++++++++++++++---- internal/cmd/deadcode/deadcode_test.go | 118 ++++--- internal/cmd/deadcode/doc.go | 68 +++- internal/cmd/deadcode/testdata/jsonflag.txtar | 5 +- internal/cmd/deadcode/testdata/whylive.txtar | 56 ++++ 5 files changed, 441 insertions(+), 119 deletions(-) create mode 100644 internal/cmd/deadcode/testdata/whylive.txtar diff --git a/internal/cmd/deadcode/deadcode.go b/internal/cmd/deadcode/deadcode.go index e01c3aaf00b..890fcc14f19 100644 --- a/internal/cmd/deadcode/deadcode.go +++ b/internal/cmd/deadcode/deadcode.go @@ -14,7 +14,7 @@ import ( "fmt" "go/ast" "go/token" - "html/template" + "go/types" "io" "log" "os" @@ -23,7 +23,9 @@ import ( "runtime/pprof" "sort" "strings" + "text/template" + "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph/rta" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/ssa" @@ -40,6 +42,7 @@ var ( filterFlag = flag.String("filter", "", "report only packages matching this regular expression (default: module of first package)") generatedFlag = flag.Bool("generated", false, "include dead functions in generated Go files") + whyLiveFlag = flag.String("whylive", "", "show a path from main to the named function") formatFlag = flag.String("format", "", "format output records using template") jsonFlag = flag.Bool("json", false, "output JSON records") cpuProfile = flag.String("cpuprofile", "", "write CPU profile to this file") @@ -95,14 +98,12 @@ func main() { }() } - var tmpl *template.Template + // Reject bad output options early. if *formatFlag != "" { if *jsonFlag { log.Fatalf("you cannot specify both -format=template and -json") } - var err error - tmpl, err = template.New("deadcode").Parse(*formatFlag) - if err != nil { + if _, err := template.New("deadcode").Parse(*formatFlag); err != nil { log.Fatalf("invalid -format: %v", err) } } @@ -162,8 +163,8 @@ func main() { } // Compute the reachabilty from main. - // (We don't actually build a call graph.) - res := rta.Analyze(roots, false) + // (Build a call graph only for -whylive.) + res := rta.Analyze(roots, *whyLiveFlag != "") // Subtle: the -test flag causes us to analyze test variants // such as "package p as compiled for p.test" or even "for q.test". @@ -178,11 +179,77 @@ func main() { // to packages "p" and "p [p.test]" were parsed only once.) reachablePosn := make(map[token.Position]bool) for fn := range res.Reachable { - if fn.Pos().IsValid() { + if fn.Pos().IsValid() || fn.Name() == "init" { reachablePosn[prog.Fset.Position(fn.Pos())] = true } } + // The -whylive=fn flag causes deadcode to explain why a function + // 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.String() == *whyLiveFlag { + targets[fn] = true + } + } + if len(targets) == 0 { + // Function is not part of the program. + // + // TODO(adonovan): improve the UX here in case + // of spelling or syntax mistakes. Some ideas: + // - a cmd/callgraph command to enumerate + // available functions. + // - a deadcode -live flag to compute the complement. + // - a syntax hint: example.com/pkg.Func or (example.com/pkg.Type).Method + // - report the element of AllFunctions with the smallest + // Levenshtein distance from *whyLiveFlag. + // - permit -whylive=regexp. But beware of spurious + // matches (e.g. fmt.Print matches fmt.Println) + // and the annoyance of having to quote parens (*T).f. + log.Fatalf("function %q not found in program", *whyLiveFlag) + } + + // Opt: remove the unreachable ones. + for fn := range targets { + if !reachablePosn[prog.Fset.Position(fn.Pos())] { + delete(targets, fn) + } + } + if len(targets) == 0 { + log.Fatalf("function %s is dead code", *whyLiveFlag) + } + + root, path := pathSearch(roots, res, targets) + if root == nil { + // RTA doesn't add callgraph edges for reflective calls. + log.Fatalf("%s is reachable only through reflection", *whyLiveFlag) + } + if len(path) == 0 { + // No edges => one of the targets is a root. + // Rather than (confusingly) print nothing, make this an error. + log.Fatalf("%s is a root", root.Func) + } + + // Build a list of jsonEdge records + // to print as -json or -format=template. + var edges []any + for _, edge := range path { + edges = append(edges, jsonEdge{ + Initial: cond(len(edges) == 0, edge.Caller.Func.String(), ""), + Kind: cond(isStaticCall(edge), "static", "dynamic"), + Posn: toJSONPosition(prog.Fset.Position(edge.Site.Pos())), + Callee: edge.Callee.Func.String(), + }) + } + format := `{{if .Initial}}{{printf "%19s%s\n" "" .Initial}}{{end}}{{printf "%8s@L%.4d --> %s" .Kind .Posn.Line .Callee}}` + if *formatFlag != "" { + format = *formatFlag + } + printObjects(format, edges) + return + } + // Group unreachable functions by package path. byPkgPath := make(map[string]map[*ssa.Function]bool) for fn := range ssautil.AllFunctions(prog) { @@ -220,14 +287,9 @@ func main() { } } - var packages []jsonPackage - - // Report dead functions grouped by packages. - // TODO(adonovan): use maps.Keys, twice. - pkgpaths := make([]string, 0, len(byPkgPath)) - for pkgpath := range byPkgPath { - pkgpaths = append(pkgpaths, pkgpath) - } + // Build array of jsonPackage objects. + var packages []any + pkgpaths := keys(byPkgPath) sort.Strings(pkgpaths) for _, pkgpath := range pkgpaths { if !filter.MatchString(pkgpath) { @@ -240,10 +302,7 @@ func main() { // declaration order. This tends to keep related // methods such as (T).Marshal and (*T).Unmarshal // together better than sorting. - fns := make([]*ssa.Function, 0, len(m)) - for fn := range m { - fns = append(fns, fn) - } + fns := keys(m) sort.Slice(fns, func(i, j int) bool { xposn := prog.Fset.Position(fns[i].Pos()) yposn := prog.Fset.Position(fns[j].Pos()) @@ -268,7 +327,7 @@ func main() { functions = append(functions, jsonFunction{ Name: fn.String(), RelName: fn.RelString(fn.Pkg.Pkg), - Posn: posn.String(), + Posn: toJSONPosition(posn), Generated: gen, }) } @@ -278,44 +337,37 @@ func main() { }) } - // Format the output, in the manner of 'go list (-json|-f=template)'. - switch { - case *jsonFlag: - // -json - out, err := json.MarshalIndent(packages, "", "\t") + // Default format: functions grouped by package. + format := `{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .RelName}}{{end}}{{println}}` + if *formatFlag != "" { + format = *formatFlag + } + printObjects(format, packages) +} + +// printObjects formats an array of objects, either as JSON or using a +// template, following the manner of 'go list (-json|-f=template)'. +func printObjects(format string, objects []any) { + if *jsonFlag { + out, err := json.MarshalIndent(objects, "", "\t") if err != nil { log.Fatalf("internal error: %v", err) } os.Stdout.Write(out) + return + } - case tmpl != nil: - // -format=template - for _, p := range packages { - var buf bytes.Buffer - if err := tmpl.Execute(&buf, p); err != nil { - log.Fatal(err) - } - if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' { - buf.WriteByte('\n') - } - os.Stdout.Write(buf.Bytes()) + // -format=template. Parse can't fail: we checked it earlier. + tmpl := template.Must(template.New("deadcode").Parse(format)) + for _, object := range objects { + var buf bytes.Buffer + if err := tmpl.Execute(&buf, object); err != nil { + log.Fatal(err) } - - default: - // functions grouped by package - for _, pkg := range packages { - seen := false - for _, fn := range pkg.Funcs { - if !seen { - seen = true - fmt.Println(pkg.Path) - } - fmt.Printf("\t%s\n", fn.RelName) - } - if seen { - fmt.Println() - } + if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' { + buf.WriteByte('\n') } + os.Stdout.Write(buf.Bytes()) } } @@ -358,15 +410,117 @@ func generator(file *ast.File) (string, bool) { return "", false } +// pathSearch returns the shortest path from one of the roots to one +// of the targets (along with the root itself), or zero if no path was found. +func pathSearch(roots []*ssa.Function, res *rta.Result, targets map[*ssa.Function]bool) (*callgraph.Node, []*callgraph.Edge) { + // Search breadth-first (for shortest path) from the root. + // + // We don't use the virtual CallGraph.Root node as we wish to + // choose the order in which we search entrypoints: + // non-test packages before test packages, + // main functions before init functions. + + // Sort roots into preferred order. + importsTesting := func(fn *ssa.Function) bool { + isTesting := func(p *types.Package) bool { return p.Path() == "testing" } + return containsFunc(fn.Pkg.Pkg.Imports(), isTesting) + } + sort.Slice(roots, func(i, j int) bool { + x, y := roots[i], roots[j] + xtest := importsTesting(x) + ytest := importsTesting(y) + if xtest != ytest { + return !xtest // non-tests before tests + } + xinit := x.Name() == "init" + yinit := y.Name() == "init" + if xinit != yinit { + return !xinit // mains before inits + } + return false + }) + + search := func(allowDynamic bool) (*callgraph.Node, []*callgraph.Edge) { + // seen maps each encountered node to its predecessor on the + // path to a root node, or to nil for root itself. + seen := make(map[*callgraph.Node]*callgraph.Edge) + bfs := func(root *callgraph.Node) []*callgraph.Edge { + queue := []*callgraph.Node{root} + seen[root] = nil + for len(queue) > 0 { + node := queue[0] + queue = queue[1:] + + // found a path? + if targets[node.Func] { + path := []*callgraph.Edge{} // non-nil in case len(path)=0 + for { + edge := seen[node] + if edge == nil { + reverse(path) + return path + } + path = append(path, edge) + node = edge.Caller + } + } + + for _, edge := range node.Out { + if allowDynamic || isStaticCall(edge) { + if _, ok := seen[edge.Callee]; !ok { + seen[edge.Callee] = edge + queue = append(queue, edge.Callee) + } + } + } + } + return nil + } + for _, rootFn := range roots { + root := res.CallGraph.Nodes[rootFn] + if path := bfs(root); path != nil { + return root, path + } + } + return nil, nil + } + + for _, allowDynamic := range []bool{false, true} { + if root, path := search(allowDynamic); path != nil { + return root, path + } + } + + return nil, nil +} + +// -- utilities -- + +func isStaticCall(edge *callgraph.Edge) bool { + return edge.Site != nil && edge.Site.Common().StaticCallee() != nil +} + +func toJSONPosition(posn token.Position) jsonPosition { + return jsonPosition{posn.Filename, posn.Line, posn.Column} +} + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +} + // -- output protocol (for JSON or text/template) -- // Keep in sync with doc comment! type jsonFunction struct { - Name string // name (with package qualifier) - RelName string // name (sans package qualifier) - Posn string // position in form "filename:line:col" - Generated bool // function is declared in a generated .go file + Name string // name (with package qualifier) + RelName string // name (sans package qualifier) + Posn jsonPosition // file/line/column of declaration + Generated bool // function is declared in a generated .go file } func (f jsonFunction) String() string { return f.Name } @@ -377,3 +531,50 @@ type jsonPackage struct { } func (p jsonPackage) String() string { return p.Path } + +type jsonEdge struct { + Initial string `json:",omitempty"` // initial entrypoint (main or init); first edge only + Kind string // = static | dynamic + Posn jsonPosition + Callee string +} + +type jsonPosition struct { + File string + Line, Col int +} + +func (p jsonPosition) String() string { + return fmt.Sprintf("%s:%d:%d", p.File, p.Line, p.Col) +} + +// -- from the future -- + +// TODO(adonovan): use go1.22's slices and maps packages. + +func containsFunc[S ~[]E, E any](s S, f func(E) bool) bool { + return indexFunc(s, f) >= 0 +} + +func indexFunc[S ~[]E, E any](s S, f func(E) bool) int { + for i := range s { + if f(s[i]) { + return i + } + } + return -1 +} + +func reverse[S ~[]E, E any](s S) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} + +func keys[M ~map[K]V, K comparable, V any](m M) []K { + r := make([]K, 0, len(m)) + for k := range m { + r = append(r, k) + } + return r +} diff --git a/internal/cmd/deadcode/deadcode_test.go b/internal/cmd/deadcode/deadcode_test.go index 17c588b134e..f17a1227362 100644 --- a/internal/cmd/deadcode/deadcode_test.go +++ b/internal/cmd/deadcode/deadcode_test.go @@ -45,15 +45,33 @@ func Test(t *testing.T) { t.Fatal(err) } + // Write the archive files to the temp directory. + tmpdir := t.TempDir() + for _, f := range ar.Files { + filename := filepath.Join(tmpdir, f.Name) + if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filename, f.Data, 0666); err != nil { + t.Fatal(err) + } + } + // Parse archive comment as directives of these forms: // - // deadcode args... command-line arguments - // [!]want arg expected/unwanted string in output + // [!]deadcode args... command-line arguments + // [!]want arg expected/unwanted string in output (or stderr) // // Args may be Go-quoted strings. - var args []string - want := make(map[string]bool) // string -> sense - for _, line := range strings.Split(string(ar.Comment), "\n") { + type testcase struct { + linenum int + args []string + wantErr bool + want map[string]bool // string -> sense + } + var cases []*testcase + var current *testcase + for i, line := range strings.Split(string(ar.Comment), "\n") { line = strings.TrimSpace(line) if line == "" || line[0] == '#' { continue // skip blanks and comments @@ -64,55 +82,64 @@ func Test(t *testing.T) { t.Fatalf("cannot break line into words: %v (%s)", err, line) } switch kind := words[0]; kind { - case "deadcode": - args = words[1:] + case "deadcode", "!deadcode": + current = &testcase{ + linenum: i + 1, + want: make(map[string]bool), + args: words[1:], + wantErr: kind[0] == '!', + } + cases = append(cases, current) case "want", "!want": + if current == nil { + t.Fatalf("'want' directive must be after 'deadcode'") + } if len(words) != 2 { t.Fatalf("'want' directive needs argument <<%s>>", line) } - want[words[1]] = kind[0] != '!' + current.want[words[1]] = kind[0] != '!' default: t.Fatalf("%s: invalid directive %q", filename, kind) } } - // Write the archive files to the temp directory. - tmpdir := t.TempDir() - for _, f := range ar.Files { - filename := filepath.Join(tmpdir, f.Name) - if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(filename, f.Data, 0666); err != nil { - t.Fatal(err) - } - } - - // Run the command. - cmd := exec.Command(exe, args...) - cmd.Stdout = new(bytes.Buffer) - cmd.Stderr = new(bytes.Buffer) - cmd.Dir = tmpdir - cmd.Env = append(os.Environ(), "GOPROXY=", "GO111MODULE=on") - if err := cmd.Run(); err != nil { - t.Fatalf("deadcode failed: %v (stderr=%s)", err, cmd.Stderr) - } - - // Check each want directive. - got := fmt.Sprint(cmd.Stdout) - for str, sense := range want { - ok := true - if strings.Contains(got, str) != sense { - if sense { - t.Errorf("missing %q", str) + for _, tc := range cases { + t.Run(fmt.Sprintf("L%d", tc.linenum), func(t *testing.T) { + // Run the command. + cmd := exec.Command(exe, tc.args...) + cmd.Stdout = new(bytes.Buffer) + cmd.Stderr = new(bytes.Buffer) + cmd.Dir = tmpdir + cmd.Env = append(os.Environ(), "GOPROXY=", "GO111MODULE=on") + var got string + if err := cmd.Run(); err != nil { + if !tc.wantErr { + t.Fatalf("deadcode failed: %v (stderr=%s)", err, cmd.Stderr) + } + got = fmt.Sprint(cmd.Stderr) } else { - t.Errorf("unwanted %q", str) + if tc.wantErr { + t.Fatalf("deadcode succeeded unexpectedly (stdout=%s)", cmd.Stdout) + } + got = fmt.Sprint(cmd.Stdout) } - ok = false - } - if !ok { - t.Errorf("got: <<%s>>", got) - } + + // Check each want directive. + for str, sense := range tc.want { + ok := true + if strings.Contains(got, str) != sense { + if sense { + t.Errorf("missing %q", str) + } else { + t.Errorf("unwanted %q", str) + } + ok = false + } + if !ok { + t.Errorf("got: <<%s>>", got) + } + } + }) } }) } @@ -137,10 +164,7 @@ func buildDeadcode(t *testing.T) string { func words(s string) ([]string, error) { var words []string for s != "" { - if s[0] == ' ' { - s = s[1:] - continue - } + s = strings.TrimSpace(s) var word string if s[0] == '"' || s[0] == '`' { prefix, err := strconv.QuotedPrefix(s) diff --git a/internal/cmd/deadcode/doc.go b/internal/cmd/deadcode/doc.go index c4874d6e262..3c0e7119200 100644 --- a/internal/cmd/deadcode/doc.go +++ b/internal/cmd/deadcode/doc.go @@ -61,20 +61,8 @@ The command supports three output formats. With no flags, the command prints dead functions grouped by package. -With the -json flag, the command prints an array of JSON Package -objects, as defined by this schema: - - type Package struct { - Path string - Funcs []Function - } - - type Function struct { - Name string // name (with package qualifier) - RelName string // name (sans package qualifier) - Posn string // position in form "filename:line:col" - Generated bool // function is declared in a generated .go file - } +With the -json flag, the command prints an array of Package +objects, as defined by the JSON schema (see below). With the -format=template flag, the command executes the specified template on each Package record. So, this template produces a result similar to the @@ -86,6 +74,58 @@ And this template shows only the list of source positions of dead functions: -format='{{range .Funcs}}{{println .Posn}}{{end}}' +# Why is a function not dead? + +The -whylive=function flag explain why the named function is not dead +by showing an arbitrary shortest path to it from one of the main functions. +(To enumerate the functions in a program, or for more sophisticated +call graph queries, use golang.org/x/tools/cmd/callgraph.) + +Fully static call paths are preferred over paths involving dynamic +edges, even if longer. Paths starting from a non-test package are +preferred over those from tests. Paths from main functions are +preferred over paths from init functions. + +The result is a list of Edge objects (see JSON schema below). +Again, the -json and -format=template flags may be used to control +the formatting of the list of Edge objects. +The default format shows, for each edge in the path, whether the call +is static or dynamic, and its source line number. For example: + + $ deadcode -whylive="(*bytes.Buffer).String" -test ./internal/cmd/deadcode/... + golang.org/x/tools/internal/cmd/deadcode.main + static@L0321 --> (*golang.org/x/tools/go/ssa.Function).RelString + static@L0428 --> (*golang.org/x/tools/go/ssa.Function).relMethod + static@L0452 --> golang.org/x/tools/go/ssa.relType + static@L0047 --> go/types.TypeString + static@L0051 --> (*bytes.Buffer).String + +# JSON schema + + type Package struct { + Path string // import path of package + Funcs []Function // list of dead functions within it + } + + type Function struct { + Name string // name (with package qualifier) + RelName string // name (sans package qualifier) + Posn Position // file/line/column of function declaration + Generated bool // function is declared in a generated .go file + } + + type Edge struct { + Initial string // initial entrypoint (main or init); first edge only + Kind string // = static | dynamic + Posn Position // file/line/column of call site + Callee string // target of the call + } + + type Position struct { + File string // name of file + Line, Col int // line and byte index, both 1-based + } + THIS TOOL IS EXPERIMENTAL and its interface may change. At some point it may be published at cmd/deadcode. In the meantime, please give us feedback at github.com/golang/go/issues. diff --git a/internal/cmd/deadcode/testdata/jsonflag.txtar b/internal/cmd/deadcode/testdata/jsonflag.txtar index cbf80ec1322..f0f3ab21bd0 100644 --- a/internal/cmd/deadcode/testdata/jsonflag.txtar +++ b/internal/cmd/deadcode/testdata/jsonflag.txtar @@ -6,6 +6,8 @@ deadcode -json example.com/p want `"Name": "example.com/p.Dead",` want `"RelName": "Dead",` want `"Generated": false` + want `"Line": 5,` + want `"Col": 6` -- go.mod -- module example.com @@ -14,7 +16,6 @@ go 1.18 -- p/p.go -- package main -func Dead() {} - func main() {} +func Dead() {} diff --git a/internal/cmd/deadcode/testdata/whylive.txtar b/internal/cmd/deadcode/testdata/whylive.txtar new file mode 100644 index 00000000000..9e7b0e6e4af --- /dev/null +++ b/internal/cmd/deadcode/testdata/whylive.txtar @@ -0,0 +1,56 @@ +# Test of -whylive flag. + +# The -whylive argument must be live. + +!deadcode -whylive=example.com.d example.com + want "function example.com.d is dead code" + +# A fully static path is preferred, even if longer. + + deadcode -whylive=example.com.c example.com + want " example.com.main" + want " static@L0004 --> example.com.a" + want " static@L0009 --> example.com.b" + want " static@L0012 --> example.com.c" + +# Dynamic edges are followed if necessary. +# (Note that main is preferred over init.) + + deadcode -whylive=example.com.f example.com + want " example.com.main" + want "dynamic@L0006 --> example.com.e" + want " static@L0017 --> example.com.f" + +# Degenerate case where target is itself a root. + +!deadcode -whylive=example.com.main example.com + want "example.com.main is a root" + +-- go.mod -- +module example.com +go 1.18 + +-- main.go -- +package main + +func main() { + a() + println(c, e) // c, e are address-taken + (func ())(nil)() // potential dynamic call to c, e +} +func a() { + b() +} +func b() { + c() +} +func c() +func d() +func e() { + f() +} +func f() + +func init() { + (func ())(nil)() // potential dynamic call to c, e +} \ No newline at end of file From e7fb31ad45aafe6733523da75a71131305a27ad9 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 7 Nov 2023 09:48:13 -0500 Subject: [PATCH 096/100] internal/cmd/deadcode: rename -format to -f (following go list) Change-Id: Ie87af973b853584082c298c339e995a8030f2d9b Reviewed-on: https://go-review.googlesource.com/c/tools/+/540475 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- internal/cmd/deadcode/deadcode.go | 10 +++++----- internal/cmd/deadcode/doc.go | 8 ++++---- internal/cmd/deadcode/testdata/lineflag.txtar | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/cmd/deadcode/deadcode.go b/internal/cmd/deadcode/deadcode.go index 890fcc14f19..58b42c25180 100644 --- a/internal/cmd/deadcode/deadcode.go +++ b/internal/cmd/deadcode/deadcode.go @@ -43,7 +43,7 @@ var ( filterFlag = flag.String("filter", "", "report only packages matching this regular expression (default: module of first package)") generatedFlag = flag.Bool("generated", false, "include dead functions in generated Go files") whyLiveFlag = flag.String("whylive", "", "show a path from main to the named function") - formatFlag = flag.String("format", "", "format output records using template") + formatFlag = flag.String("f", "", "format output records using template") jsonFlag = flag.Bool("json", false, "output JSON records") cpuProfile = flag.String("cpuprofile", "", "write CPU profile to this file") memProfile = flag.String("memprofile", "", "write memory profile to this file") @@ -101,10 +101,10 @@ func main() { // Reject bad output options early. if *formatFlag != "" { if *jsonFlag { - log.Fatalf("you cannot specify both -format=template and -json") + log.Fatalf("you cannot specify both -f=template and -json") } if _, err := template.New("deadcode").Parse(*formatFlag); err != nil { - log.Fatalf("invalid -format: %v", err) + log.Fatalf("invalid -f: %v", err) } } @@ -232,7 +232,7 @@ func main() { } // Build a list of jsonEdge records - // to print as -json or -format=template. + // to print as -json or -f=template. var edges []any for _, edge := range path { edges = append(edges, jsonEdge{ @@ -357,7 +357,7 @@ func printObjects(format string, objects []any) { return } - // -format=template. Parse can't fail: we checked it earlier. + // -f=template. Parse can't fail: we checked it earlier. tmpl := template.Must(template.New("deadcode").Parse(format)) for _, object := range objects { var buf bytes.Buffer diff --git a/internal/cmd/deadcode/doc.go b/internal/cmd/deadcode/doc.go index 3c0e7119200..8d28eb31288 100644 --- a/internal/cmd/deadcode/doc.go +++ b/internal/cmd/deadcode/doc.go @@ -64,15 +64,15 @@ With no flags, the command prints dead functions grouped by package. With the -json flag, the command prints an array of Package objects, as defined by the JSON schema (see below). -With the -format=template flag, the command executes the specified template +With the -f=template flag, the command executes the specified template on each Package record. So, this template produces a result similar to the default format: - -format='{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .RelName}}{{end}}{{println}}' + -f='{{println .Path}}{{range .Funcs}}{{printf "\t%s\n" .RelName}}{{end}}{{println}}' And this template shows only the list of source positions of dead functions: - -format='{{range .Funcs}}{{println .Posn}}{{end}}' + -f='{{range .Funcs}}{{println .Posn}}{{end}}' # Why is a function not dead? @@ -87,7 +87,7 @@ preferred over those from tests. Paths from main functions are preferred over paths from init functions. The result is a list of Edge objects (see JSON schema below). -Again, the -json and -format=template flags may be used to control +Again, the -json and -f=template flags may be used to control the formatting of the list of Edge objects. The default format shows, for each edge in the path, whether the call is static or dynamic, and its source line number. For example: diff --git a/internal/cmd/deadcode/testdata/lineflag.txtar b/internal/cmd/deadcode/testdata/lineflag.txtar index e629e0573b1..51940ad3274 100644 --- a/internal/cmd/deadcode/testdata/lineflag.txtar +++ b/internal/cmd/deadcode/testdata/lineflag.txtar @@ -1,6 +1,6 @@ # Test of line-oriented output. - deadcode "-format={{range .Funcs}}{{println .Name}}{{end}}" -filter= example.com + deadcode "-f={{range .Funcs}}{{println .Name}}{{end}}" -filter= example.com want "(example.com.T).Goodbye" !want "(example.com.T).Hello" From 51df92b224f5bbb7d2cabb52c0c77410b2d590e7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 6 Nov 2023 13:27:49 -0500 Subject: [PATCH 097/100] go/ssa: two minor cleanups 1. inline sole uses of addMethod, createMethodSet. 2. simplify MethodValue call to LookupMethod in go/callgraph. Change-Id: Ifa94d0dd7c2a522551c0d76341213dc9c8a73501 Reviewed-on: https://go-review.googlesource.com/c/tools/+/540216 Reviewed-by: Tim King LUCI-TryBot-Result: Go LUCI --- go/callgraph/rta/rta.go | 2 +- go/callgraph/vta/vta.go | 3 ++ go/ssa/methods.go | 84 ++++++++++++++++------------------------- 3 files changed, 37 insertions(+), 52 deletions(-) diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go index 36fe93f6056..d0ae0fccf57 100644 --- a/go/callgraph/rta/rta.go +++ b/go/callgraph/rta/rta.go @@ -232,7 +232,7 @@ func (r *rta) visitDynCall(site ssa.CallInstruction) { func (r *rta) addInvokeEdge(site ssa.CallInstruction, C types.Type) { // Ascertain the concrete method of C to be called. imethod := site.Common().Method - cmethod := r.prog.MethodValue(r.prog.MethodSets.MethodSet(C).Lookup(imethod.Pkg(), imethod.Name())) + cmethod := r.prog.LookupMethod(C, imethod.Pkg(), imethod.Name()) r.addEdge(site.Parent(), site, cmethod, true) } diff --git a/go/callgraph/vta/vta.go b/go/callgraph/vta/vta.go index 58393600337..2303fcfa0a8 100644 --- a/go/callgraph/vta/vta.go +++ b/go/callgraph/vta/vta.go @@ -154,6 +154,9 @@ func propFunc(p propType, c ssa.CallInstruction, cache methodCache) []*ssa.Funct // ssa.Program.MethodSets and ssa.Program.MethodValue // APIs. The cache is used to speed up querying of // methods of a type as the mentioned APIs are expensive. +// +// TODO(adonovan): Program.MethodValue already does this kind of +// caching. Is this really necessary? type methodCache map[types.Type]map[string][]*ssa.Function // methods returns methods of a type `t` named `name`. First consults diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 2700d26a72c..03ef62521d9 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -42,9 +42,39 @@ func (prog *Program) MethodValue(sel *types.Selection) *Function { var cr creator - prog.methodsMu.Lock() - m := prog.addMethod(prog.createMethodSet(T), sel, &cr) - prog.methodsMu.Unlock() + m := func() *Function { + prog.methodsMu.Lock() + defer prog.methodsMu.Unlock() + + // Get or create SSA method set. + mset, ok := prog.methodSets.At(T).(*methodSet) + if !ok { + mset = &methodSet{mapping: make(map[string]*Function)} + prog.methodSets.Set(T, mset) + } + + // Get or create SSA method. + id := sel.Obj().Id() + fn, ok := mset.mapping[id] + if !ok { + obj := sel.Obj().(*types.Func) + _, ptrObj := deptr(recvType(obj)) + _, ptrRecv := deptr(T) + needsPromotion := len(sel.Index()) > 1 + needsIndirection := !ptrObj && ptrRecv + if needsPromotion || needsIndirection { + fn = createWrapper(prog, toSelection(sel), &cr) + } else { + fn = prog.objectMethod(obj, &cr) + } + if fn.Signature.Recv() == nil { + panic(fn) + } + mset.mapping[id] = fn + } + + return fn + }() b := builder{created: &cr} b.iterate() @@ -111,54 +141,6 @@ type methodSet struct { mapping map[string]*Function // populated lazily } -// Precondition: T is a concrete type, e.g. !isInterface(T) and not parameterized. -// Requires prog.methodsMu. -func (prog *Program) createMethodSet(T types.Type) *methodSet { - if prog.mode&SanityCheckFunctions != 0 { - if types.IsInterface(T) || prog.parameterized.isParameterized(T) { - panic("type is interface or parameterized") - } - } - - mset, ok := prog.methodSets.At(T).(*methodSet) - if !ok { - mset = &methodSet{mapping: make(map[string]*Function)} - prog.methodSets.Set(T, mset) - } - return mset -} - -// Adds any created functions to cr. -// Precondition: T is a concrete type, e.g. !isInterface(T) and not parameterized. -// Requires prog.methodsMu. -func (prog *Program) addMethod(mset *methodSet, sel *types.Selection, cr *creator) *Function { - if sel.Kind() == types.MethodExpr { - panic(sel) - } - id := sel.Obj().Id() - fn := mset.mapping[id] - if fn == nil { - sel := toSelection(sel) - obj := sel.obj.(*types.Func) - - _, ptrObj := deptr(recvType(obj)) - _, ptrRecv := deptr(sel.recv) - - needsPromotion := len(sel.index) > 1 - needsIndirection := !ptrObj && ptrRecv - if needsPromotion || needsIndirection { - fn = createWrapper(prog, sel, cr) - } else { - fn = prog.objectMethod(obj, cr) - } - if fn.Signature.Recv() == nil { - panic(fn) // missing receiver - } - mset.mapping[id] = fn - } - return fn -} - // RuntimeTypes returns a new unordered slice containing all types in // the program for which a runtime type is required. // From bbf8380961d57b5bb9347781a1718de28a09f6ae Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 3 Nov 2023 11:44:46 -0400 Subject: [PATCH 098/100] gopls/internal/regtest/marker: use golden diffs for suggested fixes Following the model of codeactionerr, use diffs to reduce the verbosity and redundancy of golden content, for the suggestedfix marker. Also, since all suggested fixes should be of kind 'quickfix', remove the unnecessary kind parameter. Finally, clean up some stale comments. For golang/go#54845 Change-Id: I2eb08e4415dcff4acba604bf97b16c0a82c0a658 Reviewed-on: https://go-review.googlesource.com/c/tools/+/539656 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/lsp/regtest/marker.go | 18 ++----- .../marker/testdata/diagnostics/typeerr.txt | 15 +++--- .../marker/testdata/stubmethods/basic.txt | 23 ++++----- .../testdata/stubmethods/issue61693.txt | 27 ++++------ .../testdata/stubmethods/issue61830.txt | 30 ++++------- .../testdata/suggestedfix/self_assignment.txt | 19 +++---- .../testdata/suggestedfix/undeclared.txt | 50 ++++++++----------- .../testdata/suggestedfix/unusedrequire.txt | 11 ++-- .../suggestedfix/unusedrequire_gowork.txt | 22 +++++--- 9 files changed, 92 insertions(+), 123 deletions(-) diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index d07c6d070e3..eea6e15d283 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -249,9 +249,6 @@ var update = flag.Bool("update", false, "if set, update test data during marker // to have exactly one associated code action of the specified kind. // This action is executed for its editing effects on the source files. // Like rename, the golden directory contains the expected transformed files. -// TODO(rfindley): we probably only need 'suggestedfix' for quick-fixes. All -// other actions should use codeaction markers. In that case, we can remove -// the 'kind' parameter. // // - rank(location, ...completionItem): executes a textDocument/completion // request at the given location, and verifies that each expected @@ -387,15 +384,11 @@ var update = flag.Bool("update", false, "if set, update test data during marker // // Existing marker tests (in ../testdata) to port: // - CallHierarchy -// - Completions -// - CompletionSnippets -// - CaseSensitiveCompletions -// - RankCompletions // - SemanticTokens -// - FunctionExtractions +// - SuggestedFixes // - MethodExtractions -// - Renames // - InlayHints +// - Renames // - SelectionRanges func RunMarkerTests(t *testing.T, dir string) { // The marker tests must be able to run go/packages.Load. @@ -407,7 +400,6 @@ func RunMarkerTests(t *testing.T, dir string) { } // Opt: use a shared cache. - // TODO(rfindley): opt: use a memoize store with no eviction. cache := cache.New(nil) for _, test := range tests { @@ -2035,7 +2027,7 @@ func (mark marker) consumeExtraNotes(name string, f func(marker)) { // kind, golden) marker. It acts like @diag(location, regexp), to set // the expectation of a diagnostic, but then it applies the first code // action of the specified kind suggested by the matched diagnostic. -func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, actionKind string, golden *Golden) { +func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) { loc.Range.End = loc.Range.Start // diagnostics ignore end position. // Find and remove the matching diagnostic. diag, ok := removeDiagnostic(mark, loc, re) @@ -2045,14 +2037,14 @@ func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, a } // Apply the fix it suggests. - changed, err := codeAction(mark.run.env, loc.URI, diag.Range, actionKind, &diag) + changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag) if err != nil { mark.errorf("suggestedfix failed: %v. (Use @suggestedfixerr for expected errors.)", err) return } // Check the file state. - checkChangedFiles(mark, changed, golden) + checkDiffs(mark, changed, golden) } // codeAction executes a textDocument/codeAction request for the specified diff --git a/gopls/internal/regtest/marker/testdata/diagnostics/typeerr.txt b/gopls/internal/regtest/marker/testdata/diagnostics/typeerr.txt index 345c48e420a..c14b9d734ba 100644 --- a/gopls/internal/regtest/marker/testdata/diagnostics/typeerr.txt +++ b/gopls/internal/regtest/marker/testdata/diagnostics/typeerr.txt @@ -19,15 +19,12 @@ package a func f(x int) { append("") //@diag(re`""`, re"a slice") - x := 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", "quickfix", fix) + x := 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) } -- @fix/typeerr.go -- -package a - -func f(x int) { - append("") //@diag(re`""`, re"a slice") - - x = 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", "quickfix", fix) -} - +--- before ++++ after +@@ -6 +6 @@ +- x := 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) ++ x = 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) diff --git a/gopls/internal/regtest/marker/testdata/stubmethods/basic.txt b/gopls/internal/regtest/marker/testdata/stubmethods/basic.txt index 253ecd79cda..9a651288306 100644 --- a/gopls/internal/regtest/marker/testdata/stubmethods/basic.txt +++ b/gopls/internal/regtest/marker/testdata/stubmethods/basic.txt @@ -9,16 +9,15 @@ package a type C int -var _ error = C(0) //@suggestedfix(re"C.0.", re"missing method Error", "quickfix", stub) - +var _ error = C(0) //@suggestedfix(re"C.0.", re"missing method Error", stub) -- @stub/a/a.go -- -package a - -type C int - -// Error implements error. -func (C) Error() string { - panic("unimplemented") -} - -var _ error = C(0) //@suggestedfix(re"C.0.", re"missing method Error", "quickfix", stub) +--- before ++++ after +@@ -3 +3,6 @@ +-type C int ++type C int ++ ++// Error implements error. ++func (C) Error() string { ++ panic("unimplemented") ++} diff --git a/gopls/internal/regtest/marker/testdata/stubmethods/issue61693.txt b/gopls/internal/regtest/marker/testdata/stubmethods/issue61693.txt index 8dda66293e9..f767b656b42 100644 --- a/gopls/internal/regtest/marker/testdata/stubmethods/issue61693.txt +++ b/gopls/internal/regtest/marker/testdata/stubmethods/issue61693.txt @@ -15,21 +15,16 @@ func F(err ...error) {} func _() { var x error - F(x, C(0)) //@suggestedfix(re"C.0.", re"missing method Error", "quickfix", stub) + F(x, C(0)) //@suggestedfix(re"C.0.", re"missing method Error", stub) } -- @stub/main.go -- -package main - -type C int - -// Error implements error. -func (C) Error() string { - panic("unimplemented") -} - -func F(err ...error) {} - -func _() { - var x error - F(x, C(0)) //@suggestedfix(re"C.0.", re"missing method Error", "quickfix", stub) -} +--- before ++++ after +@@ -3 +3,6 @@ +-type C int ++type C int ++ ++// Error implements error. ++func (C) Error() string { ++ panic("unimplemented") ++} diff --git a/gopls/internal/regtest/marker/testdata/stubmethods/issue61830.txt b/gopls/internal/regtest/marker/testdata/stubmethods/issue61830.txt index 43633557d89..3e6fab1bb00 100644 --- a/gopls/internal/regtest/marker/testdata/stubmethods/issue61830.txt +++ b/gopls/internal/regtest/marker/testdata/stubmethods/issue61830.txt @@ -14,23 +14,15 @@ type I interface { type A struct{} -var _ I = &A{} //@suggestedfix(re"&A..", re"missing method M", "quickfix", stub) +var _ I = &A{} //@suggestedfix(re"&A..", re"missing method M", stub) -- @stub/p.go -- -package p - -import "io" - -type B struct{} - -type I interface { - M(io.Reader, B) -} - -type A struct{} - -// M implements I. -func (*A) M(io.Reader, B) { - panic("unimplemented") -} - -var _ I = &A{} //@suggestedfix(re"&A..", re"missing method M", "quickfix", stub) +--- before ++++ after +@@ -11 +11,6 @@ +-type A struct{} ++type A struct{} ++ ++// M implements I. ++func (*A) M(io.Reader, B) { ++ panic("unimplemented") ++} diff --git a/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt index 241a80a99c2..1003ef21ffa 100644 --- a/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt +++ b/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt @@ -9,20 +9,13 @@ import ( func goodbye() { s := "hiiiiiii" - s = s //@suggestedfix("s = s", re"self-assignment", "quickfix", fix) + s = s //@suggestedfix("s = s", re"self-assignment", fix) log.Print(s) } -- @fix/a.go -- -package suggestedfix - -import ( - "log" -) - -func goodbye() { - s := "hiiiiiii" - //@suggestedfix("s = s", re"self-assignment", "quickfix", fix) - log.Print(s) -} - +--- before ++++ after +@@ -9 +9 @@ +- s = s //@suggestedfix("s = s", re"self-assignment", fix) ++ //@suggestedfix("s = s", re"self-assignment", fix) diff --git a/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt index 6dc27eefd85..e2c15675b98 100644 --- a/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt +++ b/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt @@ -9,54 +9,46 @@ go 1.12 package p func a() { - z, _ := 1+y, 11 //@suggestedfix("y", re"(undeclared name|undefined): y", "quickfix", a) + z, _ := 1+y, 11 //@suggestedfix("y", re"(undeclared name|undefined): y", a) _ = z } -- @a/a.go -- -package p - -func a() { - y := - z, _ := 1+y, 11 //@suggestedfix("y", re"(undeclared name|undefined): y", "quickfix", a) - _ = z -} - +--- before ++++ after +@@ -3 +3,2 @@ +-func a() { ++func a() { ++ y := -- b.go -- package p func b() { if 100 < 90 { - } else if 100 > n+2 { //@suggestedfix("n", re"(undeclared name|undefined): n", "quickfix", b) + } else if 100 > n+2 { //@suggestedfix("n", re"(undeclared name|undefined): n", b) } } -- @b/b.go -- -package p - -func b() { - n := - if 100 < 90 { - } else if 100 > n+2 { //@suggestedfix("n", re"(undeclared name|undefined): n", "quickfix", b) - } -} - +--- before ++++ after +@@ -3 +3,2 @@ +-func b() { ++func b() { ++ n := -- c.go -- package p func c() { - for i < 200 { //@suggestedfix("i", re"(undeclared name|undefined): i", "quickfix", c) + for i < 200 { //@suggestedfix("i", re"(undeclared name|undefined): i", c) } r() //@diag("r", re"(undeclared name|undefined): r") } -- @c/c.go -- -package p - -func c() { - i := - for i < 200 { //@suggestedfix("i", re"(undeclared name|undefined): i", "quickfix", c) - } - r() //@diag("r", re"(undeclared name|undefined): r") -} - +--- before ++++ after +@@ -3 +3,2 @@ +-func c() { ++func c() { ++ i := diff --git a/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt index 6317b73f067..c9f6eee5c3a 100644 --- a/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt +++ b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt @@ -13,12 +13,15 @@ module mod.com go 1.14 -require example.com v1.0.0 //@suggestedfix("require", re"not used", "quickfix", a) +require example.com v1.0.0 //@suggestedfix("require", re"not used", a) -- @a/a/go.mod -- -module mod.com - -go 1.14 +--- before ++++ after +@@ -4,3 +4 @@ +- +-require example.com v1.0.0 //@suggestedfix("require", re"not used", a) +- -- a/main.go -- package main func main() {} diff --git a/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt index 8a090d7fa48..35ed16c8d9d 100644 --- a/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt +++ b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt @@ -23,12 +23,15 @@ module mod.com/a go 1.14 -require example.com v1.0.0 //@suggestedfix("require", re"not used", "quickfix", a) +require example.com v1.0.0 //@suggestedfix("require", re"not used", a) -- @a/a/go.mod -- -module mod.com/a - -go 1.14 +--- before ++++ after +@@ -4,3 +4 @@ +- +-require example.com v1.0.0 //@suggestedfix("require", re"not used", a) +- -- a/main.go -- package main func main() {} @@ -38,12 +41,15 @@ module mod.com/b go 1.14 -require example.com v1.0.0 //@suggestedfix("require", re"not used", "quickfix", b) +require example.com v1.0.0 //@suggestedfix("require", re"not used", b) -- @b/b/go.mod -- -module mod.com/b - -go 1.14 +--- before ++++ after +@@ -4,3 +4 @@ +- +-require example.com v1.0.0 //@suggestedfix("require", re"not used", b) +- -- b/main.go -- package main func main() {} From 38ed81a6b5538535b3f83203cf3bf0c6289e2291 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 3 Nov 2023 12:50:15 -0400 Subject: [PATCH 099/100] gopls/internal/regtest/marker: porting extract tests Port the extraction tests, which involved a mixture of suggestedfix, extractmethod, and extractfunc markers (notably, nothing was running the extractfunc markers!). To do this, extend the new codeaction markers to accept an option slice of titles to use for filtering. Modify the method/func extraction tests to reduce verbosity and long lines. For golang/go#54845 Change-Id: I319d1c731a4cdb4ad952944667bc57d781ad176d Reviewed-on: https://go-review.googlesource.com/c/tools/+/539481 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/lsp_test.go | 52 --- gopls/internal/lsp/regtest/marker.go | 58 ++- .../extract/extract_method/extract_basic.go | 24 -- .../extract_method/extract_basic.go.golden | 364 ------------------ .../extract/extract_method/extract_context.go | 20 - .../extract_method/extract_context.go.golden | 52 --- .../extract_variable/extract_basic_lit.go | 6 - .../extract_basic_lit.go.golden | 18 - .../extract_variable/extract_func_call.go | 9 - .../extract_func_call.go.golden | 24 -- .../extract/extract_variable/extract_scope.go | 13 - .../extract_variable/extract_scope.go.golden | 32 -- .../internal/lsp/testdata/summary.txt.golden | 3 +- gopls/internal/lsp/tests/tests.go | 52 +-- .../testdata/codeaction/extract_method.txt | 244 ++++++++++++ .../testdata/codeaction/extract_variable.txt | 83 ++++ 16 files changed, 379 insertions(+), 675 deletions(-) delete mode 100644 gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go delete mode 100644 gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden delete mode 100644 gopls/internal/lsp/testdata/extract/extract_method/extract_context.go delete mode 100644 gopls/internal/lsp/testdata/extract/extract_method/extract_context.go.golden delete mode 100644 gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go delete mode 100644 gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden delete mode 100644 gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go delete mode 100644 gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden delete mode 100644 gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go delete mode 100644 gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden create mode 100644 gopls/internal/regtest/marker/testdata/codeaction/extract_method.txt create mode 100644 gopls/internal/regtest/marker/testdata/codeaction/extract_variable.txt diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go index 34f3330520d..8ef127d8ff4 100644 --- a/gopls/internal/lsp/lsp_test.go +++ b/gopls/internal/lsp/lsp_test.go @@ -345,58 +345,6 @@ func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.S } } -func (r *runner) MethodExtraction(t *testing.T, start span.Span, end span.Span) { - uri := start.URI() - m, err := r.data.Mapper(uri) - if err != nil { - t.Fatal(err) - } - spn := span.New(start.URI(), start.Start(), end.End()) - rng, err := m.SpanRange(spn) - if err != nil { - t.Fatal(err) - } - actionsRaw, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.URIFromSpanURI(uri), - }, - Range: rng, - Context: protocol.CodeActionContext{ - Only: []protocol.CodeActionKind{"refactor.extract"}, - }, - }) - if err != nil { - t.Fatal(err) - } - var actions []protocol.CodeAction - for _, action := range actionsRaw { - if action.Command.Title == "Extract method" { - actions = append(actions, action) - } - } - // Hack: We assume that we only get one matching code action per range. - // TODO(rstambler): Support multiple code actions per test. - if len(actions) == 0 || len(actions) > 1 { - t.Fatalf("unexpected number of code actions, want 1, got %v", len(actions)) - } - _, err = r.server.ExecuteCommand(r.ctx, &protocol.ExecuteCommandParams{ - Command: actions[0].Command.Command, - Arguments: actions[0].Command.Arguments, - }) - if err != nil { - t.Fatal(err) - } - res := <-r.editRecv - for u, got := range res { - want := r.data.Golden(t, "methodextraction_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { - return got, nil - }) - if diff := compare.Bytes(want, got); diff != "" { - t.Errorf("method extraction failed for %s:\n%s", u.Filename(), diff) - } - } -} - func (r *runner) InlayHints(t *testing.T, spn span.Span) { uri := spn.URI() filename := uri.Filename() diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index eea6e15d283..45ecc74e41a 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -153,15 +153,20 @@ var update = flag.Bool("update", false, "if set, update test data during marker // completion candidate produced at the given location with provided label // results in the given golden state. // -// - codeaction(start, end, kind, golden): specifies a code action to request -// for the given range. To support multi-line ranges, the range is defined -// to be between start.Start and end.End. The golden directory contains -// changed file content after the code action is applied. +// - codeaction(start, end, kind, golden, ...titles): specifies a code action +// to request for the given range. To support multi-line ranges, the range +// is defined to be between start.Start and end.End. The golden directory +// contains changed file content after the code action is applied. +// If titles are provided, they are used to filter the matching code +// action. // -// - codeactionedit(range, kind, golden): a shorter form of codeaction. -// Invokes a code action of the given kind for the given in-line range, and -// compares the resulting formatted unified *edits* (notably, not the full -// file content) with the golden directory. +// TODO(rfindley): consolidate with codeactionedit, via a @loc2 marker that +// allows binding multi-line locations. +// +// - codeactionedit(range, kind, golden, ...titles): a shorter form of +// codeaction. Invokes a code action of the given kind for the given +// in-line range, and compares the resulting formatted unified *edits* +// (notably, not the full file content) with the golden directory. // // - codeactionerr(start, end, kind, wantError): specifies a codeaction that // fails with an error that matches the expectation. @@ -381,12 +386,15 @@ var update = flag.Bool("update", false, "if set, update test data during marker // - Provide some means by which locations in the standard library // (or builtin.go) can be named, so that, for example, we can we // can assert that MyError implements the built-in error type. +// - If possible, improve handling for optional arguments. Rather than have +// multiple variations of a marker, it would be nice to support a more +// flexible signature: can codeaction, codeactionedit, codeactionerr, and +// suggestedfix be consolidated? // // Existing marker tests (in ../testdata) to port: // - CallHierarchy // - SemanticTokens // - SuggestedFixes -// - MethodExtractions // - InlayHints // - Renames // - SelectionRanges @@ -1924,13 +1932,13 @@ func applyDocumentChanges(env *Env, changes []protocol.DocumentChanges, fileChan return nil } -func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden) { +func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden, titles ...string) { // Request the range from start.Start to end.End. loc := start loc.Range.End = end.Range.End // Apply the fix it suggests. - changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil) + changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) if err != nil { mark.errorf("codeAction failed: %v", err) return @@ -1940,8 +1948,8 @@ func codeActionMarker(mark marker, start, end protocol.Location, actionKind stri checkChangedFiles(mark, changed, g) } -func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden) { - changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil) +func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden, titles ...string) { + changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) if err != nil { mark.errorf("codeAction failed: %v", err) return @@ -1953,7 +1961,7 @@ func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr wantError) { loc := start loc.Range.End = end.Range.End - _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil) + _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, nil) wantErr.check(mark, err) } @@ -2037,7 +2045,7 @@ func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, g } // Apply the fix it suggests. - changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag) + changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) if err != nil { mark.errorf("suggestedfix failed: %v. (Use @suggestedfixerr for expected errors.)", err) return @@ -2054,8 +2062,8 @@ func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, g // The resulting map contains resulting file contents after the code action is // applied. Currently, this function does not support code actions that return // edits directly; it only supports code action commands. -func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic) (map[string][]byte, error) { - changes, err := codeActionChanges(env, uri, rng, actionKind, diag) +func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) (map[string][]byte, error) { + changes, err := codeActionChanges(env, uri, rng, actionKind, diag, titles) if err != nil { return nil, err } @@ -2069,7 +2077,8 @@ func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKi // codeActionChanges executes a textDocument/codeAction request for the // specified location and kind, and captures the resulting document changes. // If diag is non-nil, it is used as the code action context. -func codeActionChanges(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic) ([]protocol.DocumentChanges, error) { +// If titles is non-empty, the code action title must be present among the provided titles. +func codeActionChanges(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) ([]protocol.DocumentChanges, error) { // Request all code actions that apply to the diagnostic. // (The protocol supports filtering using Context.Only={actionKind} // but we can give a better error if we don't filter.) @@ -2093,14 +2102,23 @@ func codeActionChanges(env *Env, uri protocol.DocumentURI, rng protocol.Range, a var candidates []protocol.CodeAction for _, act := range actions { if act.Kind == protocol.CodeActionKind(actionKind) { - candidates = append(candidates, act) + if len(titles) > 0 { + for _, f := range titles { + if act.Title == f { + candidates = append(candidates, act) + break + } + } + } else { + candidates = append(candidates, act) + } } } if len(candidates) != 1 { for _, act := range actions { env.T.Logf("found CodeAction Kind=%s Title=%q", act.Kind, act.Title) } - return nil, fmt.Errorf("found %d CodeActions of kind %s for this diagnostic, want 1", len(candidates), actionKind) + return nil, fmt.Errorf("found %d CodeActions of kind %s matching filters %v for this diagnostic, want 1", len(candidates), actionKind, titles) } action := candidates[0] diff --git a/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go b/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go deleted file mode 100644 index c9a8d9dce38..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go +++ /dev/null @@ -1,24 +0,0 @@ -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} diff --git a/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden b/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden deleted file mode 100644 index 3310d973e01..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_method/extract_basic.go.golden +++ /dev/null @@ -1,364 +0,0 @@ --- functionextraction_extract_basic_13_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(a *A) int { - sum := a.x + a.y - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_basic_14_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(sum int) int { - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_basic_18_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func newFunction(a A) bool { - return a.x < a.y -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- functionextraction_extract_basic_22_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := newFunction(a) //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(a A) int { - sum := a.x + a.y - return sum -} - --- functionextraction_extract_basic_23_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return newFunction(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func newFunction(sum int) int { - return sum -} - --- functionextraction_extract_basic_9_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return newFunction(a) //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func newFunction(a *A) bool { - return a.x < a.y -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_basic_13_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a *A) newMethod() int { - sum := a.x + a.y - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_basic_14_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (*A) newMethod(sum int) int { - return sum -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_basic_18_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) newMethod() bool { - return a.x < a.y -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - --- methodextraction_extract_basic_22_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.newMethod() //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) newMethod() int { - sum := a.x + a.y - return sum -} - --- methodextraction_extract_basic_23_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return a.newMethod(sum) //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (A) newMethod(sum int) int { - return sum -} - --- methodextraction_extract_basic_9_2 -- -package extract - -type A struct { - x int - y int -} - -func (a *A) XLessThanYP() bool { - return a.newMethod() //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a *A) newMethod() bool { - return a.x < a.y -} - -func (a *A) AddP() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - -func (a A) XLessThanY() bool { - return a.x < a.y //@extractmethod("return", "a.y"),extractfunc("return", "a.y") -} - -func (a A) Add() int { - sum := a.x + a.y //@extractmethod("sum", "a.y"),extractfunc("sum", "a.y") - return sum //@extractmethod("return", "sum"),extractfunc("return", "sum") -} - diff --git a/gopls/internal/lsp/testdata/extract/extract_method/extract_context.go b/gopls/internal/lsp/testdata/extract/extract_method/extract_context.go deleted file mode 100644 index 1fd7197d5fc..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_method/extract_context.go +++ /dev/null @@ -1,20 +0,0 @@ -package extract - -import "context" - -type B struct { - x int - y int -} - -func (b *B) AddP(ctx context.Context) (int, error) { - sum := b.x + b.y - return sum, ctx.Err() //@extractmethod("return", "ctx.Err()"),extractfunc("return", "ctx.Err()") -} - -func (b *B) LongList(ctx context.Context) (int, error) { - p1 := 1 - p2 := 1 - p3 := 1 - return p1 + p2 + p3, ctx.Err() //@extractmethod("return", "ctx.Err()"),extractfunc("return", "ctx.Err()") -} diff --git a/gopls/internal/lsp/testdata/extract/extract_method/extract_context.go.golden b/gopls/internal/lsp/testdata/extract/extract_method/extract_context.go.golden deleted file mode 100644 index 1a51a132f49..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_method/extract_context.go.golden +++ /dev/null @@ -1,52 +0,0 @@ --- methodextraction_extract_context_12_2 -- -package extract - -import "context" - -type B struct { - x int - y int -} - -func (b *B) AddP(ctx context.Context) (int, error) { - sum := b.x + b.y - return b.newMethod(ctx, sum) //@extractmethod("return", "ctx.Err()"),extractfunc("return", "ctx.Err()") -} - -func (*B) newMethod(ctx context.Context, sum int) (int, error) { - return sum, ctx.Err() -} - -func (b *B) LongList(ctx context.Context) (int, error) { - p1 := 1 - p2 := 1 - p3 := 1 - return p1 + p2 + p3, ctx.Err() //@extractmethod("return", "ctx.Err()"),extractfunc("return", "ctx.Err()") -} - --- methodextraction_extract_context_19_2 -- -package extract - -import "context" - -type B struct { - x int - y int -} - -func (b *B) AddP(ctx context.Context) (int, error) { - sum := b.x + b.y - return sum, ctx.Err() //@extractmethod("return", "ctx.Err()"),extractfunc("return", "ctx.Err()") -} - -func (b *B) LongList(ctx context.Context) (int, error) { - p1 := 1 - p2 := 1 - p3 := 1 - return b.newMethod(ctx, p1, p2, p3) //@extractmethod("return", "ctx.Err()"),extractfunc("return", "ctx.Err()") -} - -func (*B) newMethod(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { - return p1 + p2 + p3, ctx.Err() -} - diff --git a/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go deleted file mode 100644 index cbb70a04cd1..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go +++ /dev/null @@ -1,6 +0,0 @@ -package extract - -func _() { - var _ = 1 + 2 //@suggestedfix("1", "refactor.extract", "") - var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract", "") -} diff --git a/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden deleted file mode 100644 index 3fd9b328711..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_variable/extract_basic_lit.go.golden +++ /dev/null @@ -1,18 +0,0 @@ --- suggestedfix_extract_basic_lit_4_10 -- -package extract - -func _() { - x := 1 - var _ = x + 2 //@suggestedfix("1", "refactor.extract", "") - var _ = 3 + 4 //@suggestedfix("3 + 4", "refactor.extract", "") -} - --- suggestedfix_extract_basic_lit_5_10 -- -package extract - -func _() { - var _ = 1 + 2 //@suggestedfix("1", "refactor.extract", "") - x := 3 + 4 - var _ = x //@suggestedfix("3 + 4", "refactor.extract", "") -} - diff --git a/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go deleted file mode 100644 index a20b45f5869..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go +++ /dev/null @@ -1,9 +0,0 @@ -package extract - -import "strconv" - -func _() { - x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") - str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") -} diff --git a/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden deleted file mode 100644 index d59c0ee99f2..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_variable/extract_func_call.go.golden +++ /dev/null @@ -1,24 +0,0 @@ --- suggestedfix_extract_func_call_6_8 -- -package extract - -import "strconv" - -func _() { - x := append([]int{}, 1) - x0 := x //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") - str := "1" - b, err := strconv.Atoi(str) //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") -} - --- suggestedfix_extract_func_call_8_12 -- -package extract - -import "strconv" - -func _() { - x0 := append([]int{}, 1) //@suggestedfix("append([]int{}, 1)", "refactor.extract", "") - str := "1" - x, x1 := strconv.Atoi(str) - b, err := x, x1 //@suggestedfix("strconv.Atoi(str)", "refactor.extract", "") -} - diff --git a/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go b/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go deleted file mode 100644 index c14ad709212..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go +++ /dev/null @@ -1,13 +0,0 @@ -package extract - -import "go/ast" - -func _() { - x0 := 0 - if true { - y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") - } - if true { - x1 := !false //@suggestedfix("!false", "refactor.extract", "") - } -} diff --git a/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden b/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden deleted file mode 100644 index 1c2f64b7df7..00000000000 --- a/gopls/internal/lsp/testdata/extract/extract_variable/extract_scope.go.golden +++ /dev/null @@ -1,32 +0,0 @@ --- suggestedfix_extract_scope_11_9 -- -package extract - -import "go/ast" - -func _() { - x0 := 0 - if true { - y := ast.CompositeLit{} //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") - } - if true { - x := !false - x1 := x //@suggestedfix("!false", "refactor.extract", "") - } -} - --- suggestedfix_extract_scope_8_8 -- -package extract - -import "go/ast" - -func _() { - x0 := 0 - if true { - x := ast.CompositeLit{} - y := x //@suggestedfix("ast.CompositeLit{}", "refactor.extract", "") - } - if true { - x1 := !false //@suggestedfix("!false", "refactor.extract", "") - } -} - diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden index 2f610b5f2ba..b48a44d4dff 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,8 +1,7 @@ -- summary -- CallHierarchyCount = 2 SemanticTokenCount = 3 -SuggestedFixCount = 45 -MethodExtractionCount = 8 +SuggestedFixCount = 39 InlayHintsCount = 5 RenamesCount = 45 SelectionRangesCount = 3 diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go index 822a39b205e..d310a2331cc 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -51,23 +51,21 @@ var UpdateGolden = flag.Bool("golden", false, "Update golden files") type CallHierarchy = map[span.Span]*CallHierarchyResult type SemanticTokens = []span.Span type SuggestedFixes = map[span.Span][]SuggestedFix -type MethodExtractions = map[span.Span]span.Span type Renames = map[span.Span]string type InlayHints = []span.Span type AddImport = map[span.URI]string type SelectionRanges = []span.Span type Data struct { - Config packages.Config - Exported *packagestest.Exported - CallHierarchy CallHierarchy - SemanticTokens SemanticTokens - SuggestedFixes SuggestedFixes - MethodExtractions MethodExtractions - Renames Renames - InlayHints InlayHints - AddImport AddImport - SelectionRanges SelectionRanges + Config packages.Config + Exported *packagestest.Exported + CallHierarchy CallHierarchy + SemanticTokens SemanticTokens + SuggestedFixes SuggestedFixes + Renames Renames + InlayHints InlayHints + AddImport AddImport + SelectionRanges SelectionRanges fragments map[string]string dir string @@ -90,7 +88,6 @@ type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) SemanticTokens(*testing.T, span.Span) SuggestedFix(*testing.T, span.Span, []SuggestedFix, int) - MethodExtraction(*testing.T, span.Span, span.Span) InlayHints(*testing.T, span.Span) Rename(*testing.T, span.Span, string) AddImport(*testing.T, span.URI, string) @@ -180,11 +177,10 @@ func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*tes func load(t testing.TB, mode string, dir string) *Data { datum := &Data{ - CallHierarchy: make(CallHierarchy), - Renames: make(Renames), - SuggestedFixes: make(SuggestedFixes), - MethodExtractions: make(MethodExtractions), - AddImport: make(AddImport), + CallHierarchy: make(CallHierarchy), + Renames: make(Renames), + SuggestedFixes: make(SuggestedFixes), + AddImport: make(AddImport), dir: dir, fragments: map[string]string{}, @@ -321,7 +317,6 @@ func load(t testing.TB, mode string, dir string) *Data { "inlayHint": datum.collectInlayHints, "rename": datum.collectRenames, "suggestedfix": datum.collectSuggestedFixes, - "extractmethod": datum.collectMethodExtractions, "incomingcalls": datum.collectIncomingCalls, "outgoingcalls": datum.collectOutgoingCalls, "addimport": datum.collectAddImports, @@ -414,20 +409,6 @@ func Run(t *testing.T, tests Tests, data *Data) { } }) - t.Run("MethodExtraction", func(t *testing.T) { - t.Helper() - for start, end := range data.MethodExtractions { - // Check if we should skip this spn if the -modfile flag is not available. - if shouldSkip(data, start.URI()) { - continue - } - t.Run(SpanName(start), func(t *testing.T) { - t.Helper() - tests.MethodExtraction(t, start, end) - }) - } - }) - t.Run("InlayHints", func(t *testing.T) { t.Helper() for _, src := range data.InlayHints { @@ -487,7 +468,6 @@ func checkData(t *testing.T, data *Data) { fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy)) fmt.Fprintf(buf, "SemanticTokenCount = %v\n", len(data.SemanticTokens)) fmt.Fprintf(buf, "SuggestedFixCount = %v\n", len(data.SuggestedFixes)) - fmt.Fprintf(buf, "MethodExtractionCount = %v\n", len(data.MethodExtractions)) fmt.Fprintf(buf, "InlayHintsCount = %v\n", len(data.InlayHints)) fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames)) fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges)) @@ -583,12 +563,6 @@ func (data *Data) collectSuggestedFixes(spn span.Span, actionKind, fix string) { data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], SuggestedFix{actionKind, fix}) } -func (data *Data) collectMethodExtractions(start span.Span, end span.Span) { - if _, ok := data.MethodExtractions[start]; !ok { - data.MethodExtractions[start] = end - } -} - func (data *Data) collectSelectionRanges(spn span.Span) { data.SelectionRanges = append(data.SelectionRanges, spn) } diff --git a/gopls/internal/regtest/marker/testdata/codeaction/extract_method.txt b/gopls/internal/regtest/marker/testdata/codeaction/extract_method.txt new file mode 100644 index 00000000000..3fc9f58923a --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/extract_method.txt @@ -0,0 +1,244 @@ +This test exercises function and method extraction. + +-- flags -- +-ignore_extra_diags + +-- basic.go -- +package extract + +//@codeactionedit(A_XLessThanYP, "refactor.extract", meth1, "Extract method") +//@codeactionedit(A_XLessThanYP, "refactor.extract", func1, "Extract function") +//@codeactionedit(A_AddP1, "refactor.extract", meth2, "Extract method") +//@codeactionedit(A_AddP1, "refactor.extract", func2, "Extract function") +//@codeactionedit(A_AddP2, "refactor.extract", meth3, "Extract method") +//@codeactionedit(A_AddP2, "refactor.extract", func3, "Extract function") +//@codeactionedit(A_XLessThanY, "refactor.extract", meth4, "Extract method") +//@codeactionedit(A_XLessThanY, "refactor.extract", func4, "Extract function") +//@codeactionedit(A_Add1, "refactor.extract", meth5, "Extract method") +//@codeactionedit(A_Add1, "refactor.extract", func5, "Extract function") +//@codeactionedit(A_Add2, "refactor.extract", meth6, "Extract method") +//@codeactionedit(A_Add2, "refactor.extract", func6, "Extract function") + +type A struct { + x int + y int +} + +func (a *A) XLessThanYP() bool { + return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) +} + +func (a *A) AddP() int { + sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) + return sum //@loc(A_AddP2, re`return.*sum`) +} + +func (a A) XLessThanY() bool { + return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) +} + +func (a A) Add() int { + sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) + return sum //@loc(A_Add2, re`return.*sum`) +} + +-- @func1/basic.go -- +--- before ++++ after +@@ -22 +22,5 @@ +- return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) ++ return newFunction(a) //@loc(A_XLessThanYP, re`return.*a\.y`) ++} ++ ++func newFunction(a *A) bool { ++ return a.x < a.y +-- @func2/basic.go -- +--- before ++++ after +@@ -26,2 +26,7 @@ +- sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) ++ sum := newFunction(a) //@loc(A_AddP1, re`sum.*a\.y`) +- return sum //@loc(A_AddP2, re`return.*sum`) ++ return sum //@loc(A_AddP2, re`return.*sum`) ++} ++ ++func newFunction(a *A) int { ++ sum := a.x + a.y ++ return sum +-- @func3/basic.go -- +--- before ++++ after +@@ -27 +27,5 @@ +- return sum //@loc(A_AddP2, re`return.*sum`) ++ return newFunction(sum) //@loc(A_AddP2, re`return.*sum`) ++} ++ ++func newFunction(sum int) int { ++ return sum +-- @func4/basic.go -- +--- before ++++ after +@@ -31 +31,5 @@ +- return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) ++ return newFunction(a) //@loc(A_XLessThanY, re`return.*a\.y`) ++} ++ ++func newFunction(a A) bool { ++ return a.x < a.y +-- @func5/basic.go -- +--- before ++++ after +@@ -35 +35 @@ +- sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) ++ sum := newFunction(a) //@loc(A_Add1, re`sum.*a\.y`) +@@ -39 +39,5 @@ ++func newFunction(a A) int { ++ sum := a.x + a.y ++ return sum ++} ++ +-- @func6/basic.go -- +--- before ++++ after +@@ -36 +36 @@ +- return sum //@loc(A_Add2, re`return.*sum`) ++ return newFunction(sum) //@loc(A_Add2, re`return.*sum`) +@@ -39 +39,4 @@ ++func newFunction(sum int) int { ++ return sum ++} ++ +-- @meth1/basic.go -- +--- before ++++ after +@@ -22 +22,5 @@ +- return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) ++ return a.newMethod() //@loc(A_XLessThanYP, re`return.*a\.y`) ++} ++ ++func (a *A) newMethod() bool { ++ return a.x < a.y +-- @meth2/basic.go -- +--- before ++++ after +@@ -26,2 +26,7 @@ +- sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) ++ sum := a.newMethod() //@loc(A_AddP1, re`sum.*a\.y`) +- return sum //@loc(A_AddP2, re`return.*sum`) ++ return sum //@loc(A_AddP2, re`return.*sum`) ++} ++ ++func (a *A) newMethod() int { ++ sum := a.x + a.y ++ return sum +-- @meth3/basic.go -- +--- before ++++ after +@@ -27 +27,5 @@ +- return sum //@loc(A_AddP2, re`return.*sum`) ++ return a.newMethod(sum) //@loc(A_AddP2, re`return.*sum`) ++} ++ ++func (*A) newMethod(sum int) int { ++ return sum +-- @meth4/basic.go -- +--- before ++++ after +@@ -31 +31,5 @@ +- return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) ++ return a.newMethod() //@loc(A_XLessThanY, re`return.*a\.y`) ++} ++ ++func (a A) newMethod() bool { ++ return a.x < a.y +-- @meth5/basic.go -- +--- before ++++ after +@@ -35 +35 @@ +- sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) ++ sum := a.newMethod() //@loc(A_Add1, re`sum.*a\.y`) +@@ -39 +39,5 @@ ++func (a A) newMethod() int { ++ sum := a.x + a.y ++ return sum ++} ++ +-- @meth6/basic.go -- +--- before ++++ after +@@ -36 +36 @@ +- return sum //@loc(A_Add2, re`return.*sum`) ++ return a.newMethod(sum) //@loc(A_Add2, re`return.*sum`) +@@ -39 +39,4 @@ ++func (A) newMethod(sum int) int { ++ return sum ++} ++ +-- context.go -- +package extract + +import "context" + +//@codeactionedit(B_AddP, "refactor.extract", contextMeth1, "Extract method") +//@codeactionedit(B_AddP, "refactor.extract", contextFunc1, "Extract function") +//@codeactionedit(B_LongList, "refactor.extract", contextMeth2, "Extract method") +//@codeactionedit(B_LongList, "refactor.extract", contextFunc2, "Extract function") + +type B struct { + x int + y int +} + +func (b *B) AddP(ctx context.Context) (int, error) { + sum := b.x + b.y + return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) +} + +func (b *B) LongList(ctx context.Context) (int, error) { + p1 := 1 + p2 := 1 + p3 := 1 + return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) +} +-- @contextMeth1/context.go -- +--- before ++++ after +@@ -17 +17,5 @@ +- return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) ++ return b.newMethod(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) ++} ++ ++func (*B) newMethod(ctx context.Context, sum int) (int, error) { ++ return sum, ctx.Err() +-- @contextMeth2/context.go -- +--- before ++++ after +@@ -24 +24 @@ +- return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) ++ return b.newMethod(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) +@@ -26 +26,4 @@ ++ ++func (*B) newMethod(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { ++ return p1 + p2 + p3, ctx.Err() ++} +-- @contextFunc2/context.go -- +--- before ++++ after +@@ -24 +24 @@ +- return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) ++ return newFunction(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) +@@ -26 +26,4 @@ ++ ++func newFunction(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { ++ return p1 + p2 + p3, ctx.Err() ++} +-- @contextFunc1/context.go -- +--- before ++++ after +@@ -17 +17,5 @@ +- return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) ++ return newFunction(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) ++} ++ ++func newFunction(ctx context.Context, sum int) (int, error) { ++ return sum, ctx.Err() diff --git a/gopls/internal/regtest/marker/testdata/codeaction/extract_variable.txt b/gopls/internal/regtest/marker/testdata/codeaction/extract_variable.txt new file mode 100644 index 00000000000..81226aed157 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/extract_variable.txt @@ -0,0 +1,83 @@ +This test checks the behavior of the 'extract variable' code action. + +-- flags -- +-ignore_extra_diags + +-- basic_lit.go -- +package extract + +func _() { + var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) + var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +} + +-- @basic_lit1/basic_lit.go -- +--- before ++++ after +@@ -3,2 +3,3 @@ +-func _() { ++func _() { ++ x := 1 +- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) ++ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +-- @basic_lit2/basic_lit.go -- +--- before ++++ after +@@ -5 +5,2 @@ +- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) ++ x := 3 + 4 ++ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +-- func_call.go -- +package extract + +import "strconv" + +func _() { + x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) + str := "1" + b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +} + +-- @func_call1/func_call.go -- +--- before ++++ after +@@ -6 +6,2 @@ +- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) ++ x := append([]int{}, 1) ++ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +-- @func_call2/func_call.go -- +--- before ++++ after +@@ -8 +8,2 @@ +- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) ++ x, x1 := strconv.Atoi(str) ++ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +-- scope.go -- +package extract + +import "go/ast" + +func _() { + x0 := 0 + if true { + y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) + } + if true { + x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) + } +} + +-- @scope1/scope.go -- +--- before ++++ after +@@ -8 +8,2 @@ +- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) ++ x := ast.CompositeLit{} ++ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +-- @scope2/scope.go -- +--- before ++++ after +@@ -11 +11,2 @@ +- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) ++ x := !false ++ x1 := x //@codeactionedit("!false", "refactor.extract", scope2) From 729e159c03f809dea9d75605900b19a66793d955 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Wed, 8 Nov 2023 20:08:45 +0000 Subject: [PATCH 100/100] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I0bd2a5d784d250187c82ded58476565d852adb56 Reviewed-on: https://go-review.googlesource.com/c/tools/+/540818 Reviewed-by: Heschi Kreinick Auto-Submit: Gopher Robot Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- gopls/go.mod | 8 ++++---- gopls/go.sum | 24 +++++++++++------------- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/go.mod b/go.mod index 9688b9dae25..50c32a948a8 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,9 @@ go 1.18 require ( github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.13.0 - golang.org/x/net v0.16.0 - golang.org/x/sys v0.13.0 + golang.org/x/mod v0.14.0 + golang.org/x/net v0.18.0 + golang.org/x/sys v0.14.0 ) -require golang.org/x/sync v0.4.0 +require golang.org/x/sync v0.5.0 diff --git a/go.sum b/go.sum index 78a350fe890..4e1df992575 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,10 @@ 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.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/gopls/go.mod b/gopls/go.mod index 051e7f81582..093fc01be7c 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -7,11 +7,11 @@ require ( github.com/jba/printsrc v0.2.2 github.com/jba/templatecheck v0.6.0 github.com/sergi/go-diff v1.1.0 - golang.org/x/mod v0.13.0 - golang.org/x/sync v0.4.0 - golang.org/x/sys v0.13.0 + golang.org/x/mod v0.14.0 + golang.org/x/sync v0.5.0 + golang.org/x/sys v0.14.0 golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052 - golang.org/x/text v0.13.0 + golang.org/x/text v0.14.0 golang.org/x/tools v0.13.1-0.20230920233436-f9b8da7b22be golang.org/x/vuln v1.0.1 gopkg.in/yaml.v3 v3.0.1 diff --git a/gopls/go.sum b/gopls/go.sum index e22f5e522a8..cdae287f16f 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -29,31 +29,29 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= 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.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3 h1:vxxQvncMbcRAtqHV5HsHGJkbya+BIOYIY+y6cdPZhzk= -golang.org/x/telemetry v0.0.0-20231003223302-0168ef4ebbd3/go.mod h1:ppZ76JTkRgJC2GQEgtVY3fiuJR+N8FU2MAlp+gfN1E4= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052 h1:1baVNneD/IRxmu8JQdBuki78zUqBtZxq8smZXQj0X2Y= golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052/go.mod h1:6p4ScoNeC2dhpQ1nSSMmkZ7mEj5JQUSCyc0uExBp5T4= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= 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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU= golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=