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 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() 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/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: 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/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/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 +} 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/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/bools/bools.go b/go/analysis/passes/bools/bools.go index 4219f087b98..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" ) @@ -83,7 +84,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 +163,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 = 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)...) @@ -213,14 +181,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 - } -} diff --git a/go/analysis/passes/buildssa/buildssa.go b/go/analysis/passes/buildssa/buildssa.go index 881b8fd67d7..f077ea28247 100644 --- a/go/analysis/passes/buildssa/buildssa.go +++ b/go/analysis/passes/buildssa/buildssa.go @@ -26,15 +26,13 @@ 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 } 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) @@ -76,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/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 ec7727de769..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 } @@ -319,9 +320,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/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..7f62ad4c825 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) } @@ -69,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 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) } 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) {} 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..c0060753f9f 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) { @@ -118,3 +107,46 @@ 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 +} + +// 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() == 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 +} 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/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/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..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") } } @@ -68,11 +65,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/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/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/timeformat/timeformat.go b/go/analysis/passes/timeformat/timeformat.go index c45b9fa54bc..eb84502bd99 100644 --- a/go/analysis/passes/timeformat/timeformat.go +++ b/go/analysis/passes/timeformat/timeformat.go @@ -88,29 +88,16 @@ 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 { - 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/unsafeptr/unsafeptr.go b/go/analysis/passes/unsafeptr/unsafeptr.go index e43ac20782e..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." @@ -104,8 +105,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 } } @@ -118,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 && @@ -153,13 +153,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") } diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index cb487a21775..7f79b4a7543 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 } @@ -92,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) { 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/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/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/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.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/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) { 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/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, "/") -} diff --git a/go/packages/packages.go b/go/packages/packages.go index ece0e7c603e..6cbd3de83ec 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -258,31 +258,52 @@ 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, external, err := defaultDriver(&ld.Config, patterns...) if err != nil { return nil, err } - l.sizes = types.SizesFor(response.Compiler, response.Arch) - return l.refine(response) + + ld.sizes = types.SizesFor(response.Compiler, response.Arch) + if ld.sizes == nil && ld.Config.Mode&(NeedTypes|NeedTypesSizes|NeedTypesInfo) != 0 { + // 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) } // defaultDriver is a driver that implements go/packages' fallback behavior. // 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. @@ -553,7 +574,7 @@ type loaderPackage struct { type loader struct { pkgs map[string]*loaderPackage Config - sizes types.Sizes + 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 @@ -678,39 +699,38 @@ func (ld *loader) refine(response *driverResponse) ([]*Package, error) { } } - // 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 - var srcPkgs []*loaderPackage - 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 - // If NeedImports isn't set, the imports fields will all be zeroed out. - if ld.Mode&NeedImports != 0 { + 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") + } + 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 @@ -734,40 +754,39 @@ func (ld *loader) refine(response *driverResponse) ([]*Package, error) { } lpkg.Imports[importPath] = imp.Package } - } - if lpkg.needsrc { - srcPkgs = append(srcPkgs, lpkg) - } - if ld.Mode&NeedTypesSizes != 0 { - lpkg.TypesSizes = ld.sizes - } - stack = stack[:len(stack)-1] // pop - lpkg.color = black - return lpkg.needsrc - } + // 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 + } + } - 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 + // 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 } - } else { + // For each initial package, create its import DAG. for _, lpkg := range initial { visit(lpkg) } - } - if ld.Mode&NeedImports != 0 && ld.Mode&NeedTypes != 0 { - for _, lpkg := range srcPkgs { - // 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 - } + + } 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 } } + // 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 { @@ -1042,7 +1061,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) diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 60fdf9fbadd..6e461c8acad 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -1217,6 +1217,35 @@ func testSizes(t *testing.T, exporter packagestest.Exporter) { } } +// 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. +// (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/go/ssa/builder.go b/go/ssa/builder.go index 0e49537d00a..b64c50b4349 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.) // -// Locks: +// Runtime types // -// 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. +// 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. // -// Synthetics: +// 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.) // -// 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. +// Function literals // -// Runtime types: +// 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.) // -// 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. -// -// Happens-before: -// -// The above discussion leads to the following happens-before relation for -// the BUILD and CREATE phases. -// The happens-before relation (with X 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 } // 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) @@ -821,7 +786,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { case types.MethodExpr: // (*T).f or T.f, the method f from the method-set of type T. // The result is a "thunk". - thunk := makeThunk(fn.Prog, sel, b.created) + thunk := createThunk(fn.Prog, sel, b.created) return emitConv(fn, thunk, fn.typ(tv.Type)) case types.MethodVal: @@ -856,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 { @@ -864,7 +829,7 @@ func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value { obj = fn.Prog.canon.instantiateMethod(obj, fn.subst.types(targs), fn.Prog.ctxt) } c := &MakeClosure{ - Fn: makeBound(fn.Prog, obj, b.created), + Fn: createBound(fn.Prog, obj, b.created), Bindings: []Value{v}, } c.setPos(e.Sel.Pos()) @@ -994,11 +959,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 = fn.Prog.needsInstance(callee, receiverTypeArgs(obj), b.created) - } - c.Value = callee + c.Value = fn.Prog.objectMethod(obj, b.created) c.Args = append(c.Args, v) } return @@ -1090,9 +1051,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, @@ -1139,7 +1099,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) @@ -1150,7 +1110,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) } @@ -1162,7 +1122,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)) } @@ -1182,8 +1142,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 } } @@ -1292,9 +1252,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 @@ -1582,13 +1540,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, @@ -1737,7 +1695,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) @@ -1748,7 +1706,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)) @@ -1783,20 +1741,33 @@ 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 + // 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: - // 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) } + body := fn.newBasicBlock("for.body") done := fn.newBasicBlock("for.done") // target of 'break' loop := body // target of back-edge @@ -1834,23 +1805,142 @@ 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. // 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 @@ -1872,7 +1962,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") @@ -1935,16 +2025,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) // @@ -1997,13 +2087,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") @@ -2030,6 +2120,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 := emitLocal(fn, T, token.NoPos, "rangeint.iter") + // 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) { @@ -2041,21 +2182,28 @@ 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)) + 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))) } } + 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 @@ -2067,13 +2215,30 @@ 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()) } + 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 { @@ -2297,73 +2462,71 @@ 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. As +// this may create new methods, the process is iterated until it +// converges. +func (b *builder) iterate() { + for ; b.finished < b.created.Len(); b.finished++ { + fn := b.created.At(b.finished) + b.buildFunction(fn) + } +} + // buildFunction builds SSA code for the body of function fn. Idempotent. func (b *builder) buildFunction(fn *Function) { - if !fn.built { + if fn.build != nil { assert(fn.parent == nil, "anonymous functions should not be built by buildFunction()") - b.buildFunctionBody(fn) + + if fn.Prog.mode&LogSource != 0 { + defer logStack("build %s @ %s", fn, fn.Prog.Fset.Position(fn.pos))() + } + fn.build(b, fn) fn.done() } } -// buildFunctionBody builds SSA code for the body of function fn. -// -// fn is not done building until fn.done() is called. -func (b *builder) buildFunctionBody(fn *Function) { - // TODO(taking): see if this check is reachable. - if fn.Blocks != nil { - return // building already started +// buildParamsOnly builds fn.Params from fn.Signature, but does not build fn.Body. +func (b *builder) buildParamsOnly(fn *Function) { + // For external (C, asm) functions or functions loaded from + // export data, we must set fn.Params even though there is no + // body code to reference them. + if recv := fn.Signature.Recv(); recv != nil { + fn.addParamVar(recv) } + params := fn.Signature.Params() + for i, n := 0, params.Len(); i < n; i++ { + fn.addParamVar(params.At(i)) + } +} - var recvField *ast.FieldList - var body *ast.BlockStmt - var functype *ast.FuncType - switch n := fn.syntax.(type) { - case nil: - if fn.Params != nil { - return // not a Go source function. (Synthetic, or from object file.) - } +// buildFromSyntax builds fn.Body from fn.syntax, which must be non-nil. +func (b *builder) buildFromSyntax(fn *Function) { + var ( + recvField *ast.FieldList + body *ast.BlockStmt + functype *ast.FuncType + ) + switch syntax := fn.syntax.(type) { case *ast.FuncDecl: - functype = n.Type - recvField = n.Recv - body = n.Body + functype = syntax.Type + recvField = syntax.Recv + body = syntax.Body + if body == nil { + b.buildParamsOnly(fn) // no body (non-Go function) + return + } case *ast.FuncLit: - functype = n.Type - body = n.Body + functype = syntax.Type + body = syntax.Body + case nil: + panic("no syntax") default: - panic(n) - } - - if body == nil { - // External function. - if fn.Params == nil { - // This condition ensures we add a non-empty - // params list once only, but we may attempt - // the degenerate empty case repeatedly. - // TODO(adonovan): opt: don't do that. - - // 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) - } - params := fn.Signature.Params() - for i, n := 0, params.Len(); i < n; i++ { - fn.addParamObj(params.At(i)) - } - } - return + panic(syntax) // unexpected syntax } - // Build instantiation wrapper around generic body? - if fn.topLevelOrigin != nil && fn.subst == nil { - buildInstantiationWrapper(fn) - return - } - - if fn.Prog.mode&LogSource != 0 { - defer logStack("build function %s @ %s", fn, fn.Prog.Fset.Position(fn.pos))() - } fn.startBody() fn.createSyntacticParams(recvField, functype) b.stmt(fn, body) @@ -2381,45 +2544,17 @@ func (b *builder) buildFunctionBody(fn *Function) { fn.finishBody() } -// buildCreated does the BUILD phase for each function created by builder that is not yet BUILT. -// Functions are built using buildFunction. -// -// May add types that require runtime type information to builder. -func (b *builder) buildCreated() { - for ; b.finished < b.created.Len(); b.finished++ { - fn := b.created.At(b.finished) - b.buildFunction(fn) - } -} - -// Adds any needed runtime type information for the created functions. -// -// May add newly CREATEd functions that may need to be built or runtime type information. +// addRuntimeType records t as a runtime type, +// along with all types derivable from it using reflection. // -// 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) - } -} - -func (b *builder) done() bool { - return b.rtypes >= b.created.Len() +// 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? + }) } // Build calls Package.Build for each package in prog. @@ -2447,9 +2582,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) } @@ -2458,45 +2595,39 @@ func (p *Package) build() { if p.info == nil { 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. - // - // TODO(adonovan): ideally belongs in memberFromObject, but - // that would require package creation in topological order. - 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.needMethodsOf(mem.Type(), &p.created) - } - } if p.Prog.mode&LogSource != 0 { defer logStack("build %s", p)() } 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() { @@ -2506,9 +2637,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) } } @@ -2516,11 +2647,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 { + 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 + // fn.goversion for each one. (Since init is a synthetic + // function it has no syntax of its own that needs a version.) + fn.goversion = p.initVersion[varinit.Rhs] if len(varinit.Lhs) == 1 { // 1:1 initialization: var x, y = a(), b() var lval lvalue @@ -2529,28 +2667,33 @@ 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()) } } } + // 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 { for _, decl := range file.Decls { 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) } @@ -2560,35 +2703,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. - } - - p.info = nil // We no longer need ASTs or go/types deductions. - p.created = nil // We no longer need created functions. - - 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/builder_generic_test.go b/go/ssa/builder_generic_test.go index 8ddf898efd9..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 @@ -515,48 +511,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,11 +542,50 @@ 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 { - 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_go122_test.go b/go/ssa/builder_go122_test.go new file mode 100644 index 00000000000..d98431296a7 --- /dev/null +++ b/go/ssa/builder_go122_test.go @@ -0,0 +1,192 @@ +// 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 ( + "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" +) + +// 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) { + for i := range l { + print(&i) + } + } + `, + "pre.go": ` + package p + + var same = func(l []int) { + for i := range l { + print(&i) + } + } + `, + } + + 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.Fatal(err) + } + + // 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. + } { + // 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("Failed to find *ssa.Function for initial value of global %s", test.global) + } + + 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 := 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) + } + } +} + +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/builder_test.go b/go/ssa/builder_test.go index 25b72fc1ec6..2186d2578a9 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 } @@ -59,7 +61,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 +71,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 } @@ -165,16 +167,129 @@ 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() + // 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 +297,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 +321,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 +340,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 +366,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) @@ -422,6 +532,7 @@ var ( "P.init": "package initializer", } + var seen []string // may contain dups for fn := range ssautil.AllFunctions(prog) { if fn.Synthetic == "" { continue @@ -432,12 +543,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) } @@ -529,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 @@ -586,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 @@ -842,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){} `}, @@ -920,7 +1025,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} @@ -973,7 +1078,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} @@ -1004,13 +1109,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 @@ -1031,7 +1131,8 @@ func TestSyntax(t *testing.T) { } return (*T)(f3()) } - var _ = F[int] + var g = F[int] + var _ = F[P] // unreferenced => not instantiated ` // Parse @@ -1049,7 +1150,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. @@ -1059,6 +1160,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) } @@ -1072,8 +1176,41 @@ 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) } } + +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/create.go b/go/ssa/create.go index 1bf88c83e76..eaaf4695e85 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -15,41 +15,43 @@ import ( "os" "sync" - "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" ) // 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 { - prog := &Program{ + return &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(), - 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 // 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: @@ -58,9 +60,11 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { } case *types.TypeName: - pkg.Members[name] = &Type{ - object: obj, - pkg: pkg, + if name != "_" { + pkg.Members[name] = &Type{ + object: obj, + pkg: pkg, + } } case *types.Const: @@ -70,7 +74,9 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { pkg: pkg, } pkg.objects[obj] = c - pkg.Members[name] = c + if name != "_" { + pkg.Members[name] = c + } case *types.Var: g := &Global{ @@ -81,7 +87,9 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { 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) @@ -89,36 +97,10 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { 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 - } - - fn := &Function{ - name: name, - object: obj, - Signature: sig, - syntax: syntax, - pos: obj.Pos(), - Pkg: pkg, - Prog: pkg.Prog, - typeparams: tparams, - info: pkg.info, - } - pkg.created.Add(fn) - if syntax == nil { - fn.Synthetic = "loaded from gc object file" - } - 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 { + if name != "_" && sig.Recv() == nil { pkg.Members[name] = fn // package-level function } @@ -127,45 +109,79 @@ func memberFromObject(pkg *Package, obj types.Object, syntax ast.Node) { } } +// 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) + + // 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, + info: info, + goversion: goversion, + pos: obj.Pos(), + Pkg: nil, // may be set by caller + Prog: prog, + typeparams: tparams, + } + if fn.syntax == nil { + fn.Synthetic = "from type information" + fn.build = (*builder).buildParamsOnly + } + if tparams.Len() > 0 { + fn.generic = new(generic) + } + 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. -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 { 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, "") } } 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) } } 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) - } + memberFromObject(pkg, pkg.info.Defs[id], decl, goversion) } } @@ -182,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. // @@ -190,36 +206,48 @@ 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. +// +// 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! + } p := &Package{ Prog: prog, 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) + syntax: info != nil, + // transient values (cleared after Package.Build) + info: info, + files: files, + 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. } 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. for _, file := range files { + goversion := goversionOf(p, file) for _, decl := range file.Decls { - membersFromDecl(p, decl) + membersFromDecl(p, decl, goversion) } } } else { @@ -229,11 +257,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, "") } } } @@ -271,8 +299,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/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/emit.go b/go/ssa/emit.go index abb617e6d40..d77b4407a80 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.vars[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 { @@ -148,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 } @@ -206,6 +243,13 @@ 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. + t := val.Type() + if f.typeparams.Len() == 0 || !f.Prog.parameterized.isParameterized(t) { + addRuntimeType(f.Prog, t) + } + mi := &MakeInterface{X: val} mi.setType(typ) return f.emit(mi) @@ -537,48 +581,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 := f.addLocal(typ, token.NoPos) - 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/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 38c3e31baff..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" @@ -108,52 +107,40 @@ 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.Object) *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 } // 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) { - 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) +func (f *Function) addSpilledParam(obj *types.Var) { + param := f.addParamVar(obj) + spill := emitLocalVar(f, obj) f.emit(&Store{Addr: spill, Val: param}) } @@ -161,7 +148,7 @@ func (f *Function) addSpilledParam(obj types.Object) { // 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 @@ -177,11 +164,11 @@ 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 { - f.addParamObj(f.Signature.Recv()) + f.addParamVar(f.Signature.Recv()) } } } @@ -191,11 +178,11 @@ 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 { - f.addParamObj(f.Signature.Params().At(len(f.Params) - n)) + f.addParamVar(f.Signature.Params().At(len(f.Params) - n)) } } } @@ -205,7 +192,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) } } } @@ -250,49 +238,14 @@ 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. func (f *Function) finishBody() { - f.objects = nil + f.vars = nil 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 { @@ -320,15 +273,15 @@ 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 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") @@ -338,7 +291,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() @@ -376,7 +329,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 } @@ -387,40 +339,25 @@ 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 // 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 { - if alloc, ok := v.(*Alloc); ok && escaping { - alloc.Heap = true +// 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 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) } @@ -438,7 +375,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 } @@ -536,7 +473,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: @@ -689,17 +626,11 @@ 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. +func identVar(fn *Function, id *ast.Ident) *types.Var { + return fn.info.Defs[id].(*types.Var) +} 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.go b/go/ssa/instantiate.go index 38249dea26d..370284ab72a 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go @@ -6,129 +6,59 @@ package ssa 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. -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. - - // 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. -} - -func (insts *instanceSet) list() []*Function { - if insts == nil { - return nil - } - - fns := make([]*Function, 0, len(insts.instances)) - for _, fn := range insts.instances { - fns = append(fns, fn) - } - return fns +// A generic records information about a generic origin function, +// including a cache of existing instantiations. +type generic struct { + instancesMu sync.Mutex + instances map[*typeList]*Function // canonical type arguments to an instance. } -// 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. // -// EXCLUSIVE_LOCKS_ACQUIRED(prog.methodMu) -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, +// 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(fn, 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. -// -// EXCLUSIVE_LOCKS_ACQUIRED(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. -// -// EXCLUSIVE_LOCKS_REQUIRED(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 generic function fn using targs. // If the instantiation is created, this is added to cr. -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 fn.generic.instancesMu. +func createInstance(fn *Function, targs []types.Type, cr *creator) *Function { 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/instantiation wrapper - var syntax ast.Node - if insts.syntax != nil { - syntax = insts.syntax - } - + // Compute signature. var sig *types.Signature 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 { + // function instSig, err := typeparams.Instantiate(prog.ctxt, fn.Signature, targs, false) if err != nil { panic(err) @@ -137,41 +67,48 @@ 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) } - var synthetic string - var subst *subster - - concrete := !parameterized.anyParameterized(targs) - - if prog.mode&InstantiateGenerics != 0 && concrete { + // Choose strategy (instance or wrapper). + var ( + synthetic string + subst *subster + build buildFunc + ) + if prog.mode&InstantiateGenerics != 0 && !prog.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 fn.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, + syntax: fn.syntax, // \ + info: fn.info, // } empty for non-created packages + goversion: fn.goversion, // / + build: build, topLevelOrigin: fn, pos: obj.Pos(), Pkg: nil, Prog: fn.Prog, typeparams: fn.typeparams, // share with origin typeargs: targs, - info: insts.info, // on synthetic packages info is nil. subst: subst, } - cr.Add(instance) - insts.instances[key] = instance return instance } diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index cd33e7e659e..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 @@ -102,7 +98,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 +108,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 { @@ -135,6 +131,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) @@ -148,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 @@ -256,7 +251,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 } @@ -292,9 +287,6 @@ func changeTypeInstrs(b *BasicBlock) int { } func TestInstanceUniqueness(t *testing.T) { - if !typeparams.Enabled { - return - } const input = ` package p @@ -324,7 +316,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 @@ -339,7 +330,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) { @@ -349,13 +340,22 @@ 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)) +// 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 } - sort.Strings(is) - return is + + fn.generic.instancesMu.Lock() + defer fn.generic.instancesMu.Unlock() + return mapValues(fn.generic.instances) } diff --git a/go/ssa/interp/interp_go122_test.go b/go/ssa/interp/interp_go122_test.go new file mode 100644 index 00000000000..dbaeb67bae0 --- /dev/null +++ b/go/ssa/interp/interp_go122_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. + +//go:build go1.22 +// +build go1.22 + +package interp_test + +import ( + "log" + "os" + "path/filepath" + "testing" + + "golang.org/x/tools/internal/testenv" +) + +func init() { + testdataTests = append(testdataTests, + "rangevarlifetime_go122.go", + "forvarlifetime_go122.go", + ) +} + +// 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/interp_test.go b/go/ssa/interp/interp_test.go index 9728d6ec523..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", @@ -132,6 +133,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/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/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/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/go/ssa/interp/testdata/rangevarlifetime_go122.go b/go/ssa/interp/testdata/rangevarlifetime_go122.go new file mode 100644 index 00000000000..950f63e7aa2 --- /dev/null +++ b/go/ssa/interp/testdata/rangevarlifetime_go122.go @@ -0,0 +1,167 @@ +// 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 + for i := range a { + r = append(r, &i) + } + return r +}([3]int{}) + +func test_init() { + if len(distinct) != 3 { + panic(distinct) + } + for i := 0; i < 3; i++ { + if i != *(distinct[i]) { + panic(distinct) + } + } +} + +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 new file mode 100644 index 00000000000..345d2a9b205 --- /dev/null +++ b/go/ssa/interp/testdata/rangevarlifetime_old.go @@ -0,0 +1,176 @@ +// 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 + for i := range a { + r = append(r, &i) + } + return r +}([3]int{}) + +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]) != 2 { + panic(same) + } + } +} + +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") + } +} diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 29449837138..03ef62521d9 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go @@ -10,54 +10,124 @@ import ( "fmt" "go/types" + "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" ) // 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. // // 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)) } 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 // generic method + } + 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 := 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() + + 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()) } - prog.methodsMu.Unlock() - if m == nil { - return nil // abstract method (generic) + // Belongs to a created package? + if fn := prog.FuncValue(obj); fn != nil { + return fn } - for !b.done() { - b.buildCreated() - b.needsRuntimeTypes() + + // 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) } - return m + + // 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. +// 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 { @@ -68,208 +138,136 @@ 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 -} - -// Precondition: T is a concrete type, e.g. !isInterface(T) and not parameterized. -// EXCLUSIVE_LOCKS_REQUIRED(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. -// EXCLUSIVE_LOCKS_REQUIRED(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 = makeWrapper(prog, sel, cr) - } else { - fn = prog.originFunc(obj) - if fn.typeparams.Len() > 0 { // instantiate - targs := receiverTypeArgs(obj) - fn = prog.lookupOrCreateInstance(fn, targs, cr) - } - } - if fn.Signature.Recv() == nil { - panic(fn) // missing receiver - } - mset.mapping[id] = fn - } - return fn + mapping map[string]*Function // populated lazily } -// 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. // -// 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) - } - }) - return res -} - -// 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()) + prog.runtimeTypesMu.Lock() + defer prog.runtimeTypesMu.Unlock() + return prog.runtimeTypes.Keys() } -// 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. -// -// Thread-safe. (Called via Package.build from multiple builder goroutines.) +// forEachReachable calls f for type T and each type reachable from +// its type through reflection. // -// TODO(adonovan): make this faster. It accounts for 20% of SSA build time. +// The function f must use memoization to break cycles and +// return false when the type has already been visited. // -// 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. -// -// 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) - } - switch t := T.(type) { - case *types.Basic: - // nop + // 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 + } - case *types.Interface: - // nop---handled by recursion over method set. + switch T := T.(type) { + case *types.Basic: + // nop - case *types.Pointer: - prog.needMethods(t.Elem(), false, cr) + case *types.Interface: + // nop---handled by recursion over method set. - case *types.Slice: - prog.needMethods(t.Elem(), false, cr) + case *types.Pointer: + visit(T.Elem(), false) - case *types.Chan: - prog.needMethods(t.Elem(), false, cr) + case *types.Slice: + visit(T.Elem(), false) - case *types.Map: - prog.needMethods(t.Key(), false, cr) - prog.needMethods(t.Elem(), false, cr) + case *types.Chan: + 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.Map: + visit(T.Key(), false) + visit(T.Elem(), false) - 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: + // forEachReachable must not be called on parameterized types. + panic(T) - default: - panic(T) + default: + panic(T) + } } + visit(T, false) } 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.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/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/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 9c900e3aab1..7b1eb8527f9 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. // @@ -172,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] @@ -189,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. @@ -223,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/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/ssa.go b/go/ssa/ssa.go index bd42f2e0a90..58a641a1fdb 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go @@ -23,20 +23,25 @@ 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 - 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. + methodsMu sync.Mutex + methodSets typeutil.Map // maps type to its concrete *methodSet + + parameterized tpWalker // memoization of whether a type refers to type parameters + + 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 @@ -51,17 +56,19 @@ 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 // 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, @@ -296,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 @@ -305,39 +312,45 @@ 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 + // 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: + 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 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 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. + // The following fields are cleared after building. 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 - 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 + lblocks map[*types.Label]*lblock // labelled blocks + subst *subster // type parameter substitutions (if non-nil) } // BasicBlock represents an SSA basic block. @@ -402,9 +415,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 } @@ -482,15 +494,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 @@ -1068,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: // @@ -1390,7 +1400,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. // @@ -1408,7 +1418,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 } @@ -1507,14 +1517,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 @@ -1562,7 +1577,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/ssautil/load.go b/go/ssa/ssautil/load.go index 96d69a20a17..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) } @@ -147,6 +165,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 } @@ -168,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/ssautil/visit.go b/go/ssa/ssautil/visit.go index 5f27050b022..3cdd3462271 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 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/stdlib_test.go b/go/ssa/stdlib_test.go index 11782f778fe..d294fe6b085 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,38 @@ 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 { + obj := pkg.TypesInfo.Defs[decl.Name].(*types.Func) + if obj == nil { + panic("nil *Func") + } + addSrcFunc(prog.FuncValue(obj)) + } + } + } + } + return res +} 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 diff --git a/go/ssa/util.go b/go/ssa/util.go index 68cc971b3ee..63fbbc1282a 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 { @@ -384,3 +366,16 @@ 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 } + +// 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/gopls/internal/lsp/tests/util_go122.go b/go/ssa/versions_go122.go similarity index 52% rename from gopls/internal/lsp/tests/util_go122.go rename to go/ssa/versions_go122.go index 90ae029766a..b74165a8e32 100644 --- a/gopls/internal/lsp/tests/util_go122.go +++ b/go/ssa/versions_go122.go @@ -5,8 +5,17 @@ //go:build go1.22 // +build go1.22 -package tests +package ssa + +import ( + "go/ast" +) func init() { - builtins["zero"] = true + 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..7c7ee4099e3 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, @@ -72,35 +69,53 @@ 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 + build: (*builder).buildWrapper, + syntax: nil, + info: nil, + goversion: "", } cr.Add(fn) + return fn +} + +// buildWrapper builds fn.Body for a method wrapper. +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) @@ -122,18 +137,14 @@ 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) - if callee.typeparams.Len() > 0 { - callee = prog.lookupOrCreateInstance(callee, receiverTypeArgs(obj), cr) - } - 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 = obj + c.Call.Method = fn.object c.Call.Value = emitLoad(fn, v) // interface (possibly a typeparam) } for _, arg := range fn.Params[1:] { @@ -141,8 +152,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 @@ -151,13 +160,13 @@ 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)) } } // -- 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 @@ -176,66 +185,57 @@ 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 { - 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(), - info: nil, // info is not set on wrappers. - } - 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 +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, + Signature: changeRecv(obj.Type().(*types.Signature), nil), // drop receiver + Synthetic: description, + Prog: prog, + pos: obj.Pos(), + // wrappers have no syntax + build: (*builder).buildBound, + syntax: nil, + info: nil, + goversion: "", } + fn.FreeVars = []*FreeVar{{name: "recv", typ: recvType(obj), parent: fn}} // (cyclic) + cr.Add(fn) return fn } +// buildBound builds fn.Body for a bound method closure. +func (b *builder) buildBound(fn *Function) { + fn.startBody() + createParams(fn, 0) + var c Call + + recv := fn.FreeVars[0] + if !types.IsInterface(recvType(fn.object)) { // concrete + c.Call.Value = fn.Prog.objectMethod(fn.object, b.created) + c.Call.Args = []Value{recv} + } else { + 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() +} + // -- 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. @@ -251,38 +251,16 @@ 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() } -// -// 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 { +func createThunk(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, + fn := createWrapper(prog, sel, cr) + if fn.Signature.Recv() != nil { + panic(fn) // unexpected receiver } - 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 - } return fn } @@ -290,21 +268,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 { @@ -329,16 +292,16 @@ 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 fn.startBody() if sig.Recv() != nil { - fn.addParamObj(sig.Recv()) + fn.addParamVar(sig.Recv()) } createParams(fn, 0) 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 + } +} 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/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/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/doc/inline-after.png b/gopls/doc/inline-after.png new file mode 100644 index 00000000000..843a8454136 Binary files /dev/null and b/gopls/doc/inline-after.png differ diff --git a/gopls/doc/inline-before.png b/gopls/doc/inline-before.png new file mode 100644 index 00000000000..e3adbd4dcbf Binary files /dev/null and b/gopls/doc/inline-before.png differ 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. 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/go.mod b/gopls/go.mod index 866fb259581..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/telemetry v0.0.0-20231003223302-0168ef4ebbd3 - golang.org/x/text 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.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 365fd0c1a4f..cdae287f16f 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -29,29 +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= 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 ( 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/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/analysis.go b/gopls/internal/lsp/cache/analysis.go index dfb09951494..ae666ba9111 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 @@ -192,7 +190,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) } @@ -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 @@ -309,7 +320,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 @@ -431,10 +442,10 @@ 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[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) } } @@ -1201,7 +1220,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 } @@ -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) @@ -1343,7 +1362,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, @@ -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) +} diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go index e0be99b64f5..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 } @@ -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/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 { 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 331e0e7ebc7..7621f8198f4 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 { @@ -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 @@ -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), @@ -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, @@ -616,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. @@ -627,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 @@ -656,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{}{} @@ -671,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 7346e24f82a..e565f19e1a6 100644 --- a/gopls/internal/lsp/cache/session.go +++ b/gopls/internal/lsp/cache/session.go @@ -76,15 +76,19 @@ 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) + 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, 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, info *workspaceInformation, folder *Folder, seqID uint64) (*View, *snapshot, func(), error) { index := atomic.AddInt64(&viewIndex, 1) - // Get immutable workspace information. - info, err := s.getWorkspaceInformation(ctx, folder, options) - if err != nil { - return nil, nil, nil, err - } - gowork, _ := info.GOWORK() wsModFiles, wsModFilesErr := computeWorkspaceModFiles(ctx, info.gomod, gowork, info.effectiveGO111MODULE(), s) @@ -117,11 +115,9 @@ 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, @@ -129,16 +125,16 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI, workspaceInformation: info, } v.importsState = &importsState{ - ctx: backgroundCtx, + ctx: baseCtx, 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 +163,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() @@ -256,15 +251,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 @@ -297,28 +286,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, options *source.Options) 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, view.name, view.folder, options, 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() @@ -328,13 +323,13 @@ func (s *Session) updateViewLocked(ctx context.Context, view *View, options *sou // 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. @@ -372,6 +367,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. @@ -411,7 +414,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 { @@ -427,7 +429,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 := 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 @@ -438,10 +440,8 @@ 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 { - if err := s.updateViewLocked(ctx, view, view.lastOptions); err != nil { + } else if *info != *view.workspaceInformation { + 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) @@ -453,13 +453,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 { @@ -499,7 +493,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) releases = append(releases, release) viewToSnapshot[view] = snapshot } @@ -586,11 +580,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 6fc69bdda3d..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 { @@ -175,10 +176,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 +285,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 +296,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 +370,7 @@ func (s *snapshot) workspaceMode() workspaceMode { return mode } mode |= moduleMode - if s.options.TempModfile { + if s.Options().TempModfile { mode |= tempModfile } return mode @@ -408,7 +405,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 +487,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 +656,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 +914,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 +923,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 +980,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 +1003,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 +1016,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 +1681,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,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, newOptions *source.Options, forceReloadMetadata bool) (*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, @@ -1850,11 +1847,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. @@ -1984,7 +1976,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. @@ -2439,7 +2431,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..246669c3290 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,28 +33,31 @@ 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 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 - - // 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 // View. - workspaceInformation // Go environment information + *workspaceInformation // Go environment information importsState *importsState @@ -116,9 +118,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 @@ -407,52 +406,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 +424,14 @@ 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 + 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 } } @@ -474,23 +439,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 +454,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 +493,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 { + 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 } @@ -579,33 +533,34 @@ 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.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.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 { - filterer := buildFilterer(s.view.folder.Filename(), 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(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 } @@ -632,12 +587,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) { @@ -917,7 +867,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) (*snapshot, func()) { // Detach the context so that content invalidation cannot be canceled. ctx = xcontext.Detach(ctx) @@ -939,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, newOptions, forceReloadMetadata) + v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, changes) prevReleaseSnapshot() v.destroy(prevSnapshot, "View.invalidateContent") @@ -948,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. @@ -980,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 } @@ -1002,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 } 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/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) + } } } diff --git a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go index 555131ea796..8978ab5bfed 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,47 @@ 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 info.Decl.Body == nil { + return false // external function + } + + 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 @@ -618,7 +678,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/command.go b/gopls/internal/lsp/command.go index f4d4a9e4ba2..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 } - return c.s.didModifyFiles(ctx, []source.FileModification{mod}, FromRegenerateCgo) + + snapshot, release, err := v.Snapshot() + if err != nil { + return err + } + wg.Add(1) + go func() { + c.s.diagnoseSnapshot(snapshot, nil, true, 0) + release() + wg.Done() + }() + return nil }) } @@ -1210,3 +1236,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/completion_test.go b/gopls/internal/lsp/completion_test.go deleted file mode 100644 index 06a6a09aa1a..00000000000 --- a/gopls/internal/lsp/completion_test.go +++ /dev/null @@ -1,173 +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" - "strings" - "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) 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 - 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) 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 - 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 - 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/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/diagnostics.go b/gopls/internal/lsp/diagnostics.go index 4fbfd0acec3..4ea02cff9bf 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() @@ -175,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)...) @@ -200,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/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< 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() @@ -501,51 +454,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 { @@ -572,58 +480,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 { - 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/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/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index f6c1d4c6077..45ecc74e41a 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" @@ -114,6 +115,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) @@ -148,12 +153,22 @@ 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 -// 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. // -// - codeactionerr(kind, start, end, wantError): specifies a codeaction that +// 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. // // - codelens(location, title): specifies that a codelens is expected at the @@ -181,6 +196,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 @@ -215,6 +233,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. @@ -222,6 +245,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,35 +381,22 @@ 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 // (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 -// - Diagnostics -// - CompletionItems -// - Completions -// - CompletionSnippets -// - DeepCompletions -// - FuzzyCompletions -// - CaseSensitiveCompletions -// - RankCompletions -// - Formats -// - Imports // - SemanticTokens -// - FunctionExtractions -// - MethodExtractions -// - Renames -// - PrepareRenames +// - SuggestedFixes // - InlayHints -// - WorkspaceSymbols -// - Signatures -// - Links -// - AddImport +// - Renames // - SelectionRanges func RunMarkerTests(t *testing.T, dir string) { // The marker tests must be able to run go/packages.Load. @@ -394,7 +408,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 { @@ -704,16 +717,19 @@ 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), "def": actionMarkerFunc(defMarker), "diag": actionMarkerFunc(diagMarker), + "documentlink": actionMarkerFunc(documentLinkMarker), "foldingrange": actionMarkerFunc(foldingRangeMarker), "format": actionMarkerFunc(formatMarker), "highlight": actionMarkerFunc(highlightMarker), "hover": actionMarkerFunc(hoverMarker), "implementation": actionMarkerFunc(implementationMarker), + "preparerename": actionMarkerFunc(prepareRenameMarker), "rank": actionMarkerFunc(rankMarker), "rankl": actionMarkerFunc(ranklMarker), "refs": actionMarkerFunc(refsMarker), @@ -753,6 +769,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 @@ -764,6 +782,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 } @@ -850,7 +870,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 +884,7 @@ func loadMarkerTests(dir string) ([]*markerTest, error) { } tests = append(tests, test) } - return nil + return err }) return tests, err } @@ -895,9 +915,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 { @@ -962,6 +979,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 } @@ -1001,6 +1024,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. @@ -1219,7 +1245,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 } @@ -1233,7 +1259,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) } } @@ -1404,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 @@ -1478,7 +1539,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 { @@ -1503,7 +1564,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{ @@ -1546,13 +1607,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] } @@ -1679,15 +1744,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) { @@ -1790,14 +1847,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) } } @@ -1866,26 +1932,36 @@ 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, 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 } // 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, 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 + } + + 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) + _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, nil) wantErr.check(mark, err) } @@ -1909,7 +1985,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}) })) @@ -1928,6 +2004,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)) { @@ -1944,7 +2035,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) @@ -1954,14 +2045,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, nil) 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 @@ -1971,7 +2062,23 @@ func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, a // 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) { +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 + } + 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. +// 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.) @@ -1995,14 +2102,23 @@ func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKi 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] @@ -2011,20 +2127,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 } } @@ -2047,15 +2162,12 @@ 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) - } - - if err := applyDocumentChanges(env, env.Awaiter.takeDocumentChanges(), fileChanges); err != nil { - return nil, fmt.Errorf("applying document changes from command: %v", err) + return nil, err } + return env.Awaiter.takeDocumentChanges(), nil } - return fileChanges, nil + return nil, nil } // TODO(adonovan): suggestedfixerr @@ -2105,6 +2217,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. @@ -2215,13 +2347,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/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() 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/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/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" diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go index 0fd6b07c54b..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\"", @@ -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", @@ -733,6 +751,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", @@ -1069,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/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/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/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/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/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/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/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/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..848b5f7cc08 --- /dev/null +++ b/gopls/internal/lsp/source/inline_all.go @@ -0,0 +1,275 @@ +// 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" + "go/types" + + "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 + ) + + // 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{ + 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/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) { diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go index ec544fc31b1..74f9bed8d00 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, @@ -595,6 +599,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 @@ -807,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 { @@ -1181,6 +1199,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 +1226,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": @@ -1239,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) @@ -1356,6 +1384,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) @@ -1599,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}, @@ -1688,7 +1721,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)) @@ -1698,13 +1731,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), 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/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/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..46c2eeeb609 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 } @@ -352,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 @@ -711,7 +738,6 @@ const ( Save Create Delete - InvalidateMetadata ) func (a FileAction) String() string { @@ -728,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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 c43004833c2..00000000000 --- a/gopls/internal/lsp/testdata/rank/convert_rank.go.in +++ /dev/null @@ -1,54 +0,0 @@ -package rank - -import "time" - -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/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/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/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/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 7059a381d19..b48a44d4dff 100644 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ b/gopls/internal/lsp/testdata/summary.txt.golden @@ -1,18 +1,8 @@ -- summary -- CallHierarchyCount = 2 -CompletionsCount = 194 -CompletionSnippetCount = 74 -DeepCompletionsCount = 5 -FuzzyCompletionsCount = 8 -RankedCompletionsCount = 166 -CaseSensitiveCompletionsCount = 4 SemanticTokenCount = 3 -SuggestedFixCount = 80 -MethodExtractionCount = 8 +SuggestedFixCount = 39 InlayHintsCount = 5 -RenamesCount = 48 -PrepareRenamesCount = 7 -SignaturesCount = 32 -LinksCount = 7 +RenamesCount = 45 SelectionRangesCount = 3 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/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 5b7074fe6fa..d310a2331cc 100644 --- a/gopls/internal/lsp/tests/tests.go +++ b/gopls/internal/lsp/tests/tests.go @@ -15,24 +15,18 @@ import ( "io" "os" "path/filepath" - "regexp" "sort" - "strconv" "strings" "sync" "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" "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/txtar" ) @@ -55,45 +49,23 @@ 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 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 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 Signatures = map[span.Span]*protocol.SignatureHelp -type Links = map[span.URI][]Link 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 - DeepCompletions DeepCompletions - FuzzyCompletions FuzzyCompletions - CaseSensitiveCompletions CaseSensitiveCompletions - RankCompletions RankCompletions - SemanticTokens SemanticTokens - SuggestedFixes SuggestedFixes - MethodExtractions MethodExtractions - Renames Renames - InlayHints InlayHints - PrepareRenames PrepareRenames - Signatures Signatures - Links Links - 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 @@ -114,43 +86,14 @@ 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) - 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) 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) - 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) } -type CompletionTestType int - -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 -) - type Completion struct { CompletionItems []token.Pos } @@ -234,21 +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), - CompletionItems: make(CompletionItems), - Completions: make(Completions), - CompletionSnippets: make(CompletionSnippets), - DeepCompletions: make(DeepCompletions), - FuzzyCompletions: make(FuzzyCompletions), - RankCompletions: make(RankCompletions), - CaseSensitiveCompletions: make(CaseSensitiveCompletions), - Renames: make(Renames), - PrepareRenames: make(PrepareRenames), - SuggestedFixes: make(SuggestedFixes), - MethodExtractions: make(MethodExtractions), - Signatures: make(Signatures), - Links: make(Links), - AddImport: make(AddImport), + CallHierarchy: make(CallHierarchy), + Renames: make(Renames), + SuggestedFixes: make(SuggestedFixes), + AddImport: make(AddImport), dir: dir, fragments: map[string]string{}, @@ -381,21 +313,10 @@ 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), - "deep": datum.collectCompletions(CompletionDeep), - "fuzzy": datum.collectCompletions(CompletionFuzzy), - "casesensitive": datum.collectCompletions(CompletionCaseSensitive), - "rank": datum.collectCompletions(CompletionRank), - "snippet": datum.collectCompletionSnippets, "semantic": datum.collectSemanticTokens, "inlayHint": datum.collectInlayHints, "rename": datum.collectRenames, - "prepare": datum.collectPrepareRenames, - "signature": datum.collectSignatures, - "link": datum.collectLinks, "suggestedfix": datum.collectSuggestedFixes, - "extractmethod": datum.collectMethodExtractions, "incomingcalls": datum.collectIncomingCalls, "outgoingcalls": datum.collectOutgoingCalls, "addimport": datum.collectAddImports, @@ -454,23 +375,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 { @@ -481,50 +385,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} { - 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("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) - }) - - 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 { @@ -549,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 { @@ -583,48 +429,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("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 { - // 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 { @@ -660,38 +464,12 @@ 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 { - 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, "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)) 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, "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)) want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { @@ -773,49 +551,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 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) - } - 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) - } - } -} - -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 } @@ -828,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) } @@ -880,13 +609,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()) @@ -897,39 +619,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, - PlainSnippet: plain, - PlaceholderSnippet: placeholder, - }) -} - -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 b9a21fe9627..ea0920d2e62 100644 --- a/gopls/internal/lsp/tests/util.go +++ b/gopls/internal/lsp/tests/util.go @@ -5,139 +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/lsp/tests/compare" - "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, -} - -// 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() -} - -// 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' -// 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) @@ -159,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/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/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 } 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")), + ) }) } 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()) + }) +} 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) 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 new file mode 100644 index 00000000000..ad2289284d8 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt @@ -0,0 +1,246 @@ +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("unused", "unused", "refactor.rewrite", a) + return x +} + +-- @a/a/a.go -- +package a + +func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", 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("int", "int", "refactor.rewrite", field) +} + +func _() { + Field(1, 2) +} +-- @field/field/field.go -- +package field + +func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field) +} + +func _() { + Field(2) +} +-- ellipsis/ellipsis.go -- +package ellipsis + +func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", 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("any", "any", "refactor.rewrite", 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 { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") + return 0 +} + +func _() { + x := Overlapping(Overlapping(0)) + _ = x +} + +-- effects/effects.go -- +package effects + +func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite", 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("y", "y", "refactor.rewrite", 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("x", "x", "refactor.rewrite", recursive) + return Recursive(1) +} + +-- @recursive/recursive/recursive.go -- +package 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 new file mode 100644 index 00000000000..17abb98d5c9 --- /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("unused", "unused", "refactor.rewrite", 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("unused", "unused", "refactor.rewrite", 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..e67e378fde3 --- /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("unused", "unused", "refactor.rewrite", 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..d183cc44135 --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt @@ -0,0 +1,160 @@ +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("x", "x", "refactor.rewrite", 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("x", "x", "refactor.rewrite", d) +} + +func _() { + D(1) +} + +-- @b/a/a1.go -- +package a + +import ( + "mod.test/b" + "mod.test/c" +) + +func _() { + var _ c.C = <-b.Chan + b.B(<-b.Chan) +} +-- @b/a/a2.go -- +package a + +import ( + "mod.test/b" + "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" + "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" + . "mod.test/b" + "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("x", "x", "refactor.rewrite", b) +} +-- @d/d/d.go -- +package d + +// Removing the parameter should remove this import. + +func D() { //@codeaction("x", "x", "refactor.rewrite", d) +} + +func _() { + D() +} 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/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/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/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/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/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(" //") +} 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/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/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/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/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/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/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/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/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/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/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) } 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/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 { 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/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) 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/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/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/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) + } +} + 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/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\\}") +} diff --git a/gopls/internal/regtest/marker/testdata/completion/testy.txt b/gopls/internal/regtest/marker/testdata/completion/testy.txt index f26b3ae1b1f..983fc09160b 100644 --- a/gopls/internal/regtest/marker/testdata/completion/testy.txt +++ b/gopls/internal/regtest/marker/testdata/completion/testy.txt @@ -52,6 +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()", 0) +} 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/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 75% rename from gopls/internal/lsp/testdata/typeparams/type_params.go rename to gopls/internal/regtest/marker/testdata/completion/type_params.txt index 21fc7049f5b..185d77f9911 100644 --- a/gopls/internal/lsp/testdata/typeparams/type_params.go +++ b/gopls/internal/regtest/marker/testdata/completion/type_params.txt @@ -1,8 +1,18 @@ -//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. +/* 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]() {} @@ -26,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") @@ -43,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") @@ -51,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\\}") } 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) +} 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/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/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 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") } 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 +} 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) +} 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") 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 new file mode 100644 index 00000000000..1003ef21ffa --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt @@ -0,0 +1,21 @@ +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", fix) + log.Print(s) +} + +-- @fix/a.go -- +--- 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/quickfix/undeclared.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt similarity index 51% rename from gopls/internal/regtest/marker/testdata/quickfix/undeclared.txt rename to gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt index 6dc27eefd85..e2c15675b98 100644 --- a/gopls/internal/regtest/marker/testdata/quickfix/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/quickfix/unusedrequire.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt similarity index 74% rename from gopls/internal/regtest/marker/testdata/quickfix/unusedrequire.txt rename to gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt index 6317b73f067..c9f6eee5c3a 100644 --- a/gopls/internal/regtest/marker/testdata/quickfix/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/quickfix/unusedrequire_gowork.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt similarity index 74% rename from gopls/internal/regtest/marker/testdata/quickfix/unusedrequire_gowork.txt rename to gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt index 8a090d7fa48..35ed16c8d9d 100644 --- a/gopls/internal/regtest/marker/testdata/quickfix/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() {} 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 ) }) } 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)) + } + }) +} diff --git a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go index a96024b9ca2..5f243a158af 100644 --- a/gopls/internal/regtest/misc/semantictokens_test.go +++ b/gopls/internal/regtest/misc/semantictokens_test.go @@ -5,11 +5,10 @@ package misc import ( - "strings" "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/gopls/internal/lsp" + "golang.org/x/tools/gopls/internal/lsp/fake" "golang.org/x/tools/gopls/internal/lsp/protocol" . "golang.org/x/tools/gopls/internal/lsp/regtest" "golang.org/x/tools/internal/typeparams" @@ -48,31 +47,30 @@ func main() {} // fix bug involving type parameters and regular parameters // (golang/vscode-go#2527) func TestSemantic_2527(t *testing.T) { - if !typeparams.Enabled { - t.Skip("type parameters are needed for this test") - } // these are the expected types of identifiers in text order - want := []result{ - {"package", "keyword", ""}, - {"foo", "namespace", ""}, - {"func", "keyword", ""}, - {"Add", "function", "definition deprecated"}, - {"T", "typeParameter", "definition"}, - {"int", "type", "defaultLibrary"}, - {"target", "parameter", "definition"}, - {"T", "typeParameter", ""}, - {"l", "parameter", "definition"}, - {"T", "typeParameter", ""}, - {"T", "typeParameter", ""}, - {"return", "keyword", ""}, - {"append", "function", "defaultLibrary"}, - {"l", "parameter", ""}, - {"target", "parameter", ""}, - {"for", "keyword", ""}, - {"range", "keyword", ""}, - {"l", "parameter", ""}, - {"return", "keyword", ""}, - {"nil", "variable", "readonly defaultLibrary"}, + want := []fake.SemanticToken{ + {Token: "package", TokenType: "keyword"}, + {Token: "foo", TokenType: "namespace"}, + {Token: "// Deprecated (for testing)", TokenType: "comment"}, + {Token: "func", TokenType: "keyword"}, + {Token: "Add", TokenType: "function", Mod: "definition deprecated"}, + {Token: "T", TokenType: "typeParameter", Mod: "definition"}, + {Token: "int", TokenType: "type", Mod: "defaultLibrary"}, + {Token: "target", TokenType: "parameter", Mod: "definition"}, + {Token: "T", TokenType: "typeParameter"}, + {Token: "l", TokenType: "parameter", Mod: "definition"}, + {Token: "T", TokenType: "typeParameter"}, + {Token: "T", TokenType: "typeParameter"}, + {Token: "return", TokenType: "keyword"}, + {Token: "append", TokenType: "function", Mod: "defaultLibrary"}, + {Token: "l", TokenType: "parameter"}, + {Token: "target", TokenType: "parameter"}, + {Token: "for", TokenType: "keyword"}, + {Token: "range", TokenType: "keyword"}, + {Token: "l", TokenType: "parameter"}, + {Token: "// test coverage", TokenType: "comment"}, + {Token: "return", TokenType: "keyword"}, + {Token: "nil", TokenType: "variable", Mod: "readonly defaultLibrary"}, } src := ` -- go.mod -- @@ -96,16 +94,10 @@ func Add[T int](target T, l []T) []T { env.AfterChange( Diagnostics(env.AtRegexp("main.go", "for range")), ) - p := &protocol.SemanticTokensParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: env.Sandbox.Workdir.URI("main.go"), - }, - } - v, err := env.Editor.Server.SemanticTokensFull(env.Ctx, p) + seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go") if err != nil { t.Fatal(err) } - seen := interpret(v.Data, env.BufferText("main.go")) if x := cmp.Diff(want, seen); x != "" { t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) } @@ -142,16 +134,10 @@ func New[K int, V any]() Smap[K, V] { Settings{"semanticTokens": true}, ).Run(t, src, func(t *testing.T, env *Env) { env.OpenFile("main.go") - p := &protocol.SemanticTokensParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: env.Sandbox.Workdir.URI("main.go"), - }, - } - v, err := env.Editor.Server.SemanticTokensFull(env.Ctx, p) + seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go") if err != nil { t.Fatal(err) } - seen := interpret(v.Data, env.BufferText("main.go")) for i, s := range seen { if (s.Token == "K" || s.Token == "V") && s.TokenType != "typeParameter" { t.Errorf("%d: expected K and V to be type parameters, but got %v", i, s) @@ -160,45 +146,59 @@ func New[K int, V any]() Smap[K, V] { }) } -type result struct { - Token string - TokenType string - Mod string -} +func TestSemanticGoDirectives(t *testing.T) { + src := ` +-- go.mod -- +module example.com -// human-readable version of the semantic tokens -// comment, string, number are elided -// (and in the future, maybe elide other things, like operators) -func interpret(x []uint32, contents string) []result { - lines := strings.Split(contents, "\n") - ans := []result{} - 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 := semanticTypes[x[i+3]] - if t == "comment" || t == "string" || t == "number" { - continue +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) } - l := x[i+4] - var mods []string - for i, mod := range semanticModifiers { - if l&(1<", "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") + whyLiveFlag = flag.String("whylive", "", "show a path from main to the named function") + 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") ) @@ -91,6 +98,16 @@ func main() { }() } + // Reject bad output options early. + if *formatFlag != "" { + if *jsonFlag { + log.Fatalf("you cannot specify both -f=template and -json") + } + if _, err := template.New("deadcode").Parse(*formatFlag); err != nil { + log.Fatalf("invalid -f: %v", err) + } + } + // Load, parse, and type-check the complete program(s). cfg := &packages.Config{ BuildFlags: []string{"-tags=" + *tagsFlag}, @@ -108,17 +125,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 == "" { @@ -148,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". @@ -164,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 -f=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) { @@ -193,12 +274,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,12 +287,9 @@ func main() { } } - // 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) { @@ -230,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()) @@ -243,19 +312,62 @@ func main() { return xposn.Line < yposn.Line }) - if *lineFlag { - // line-oriented output - for _, fn := range fns { - fmt.Println(fn) - } - } 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)) + 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 } - fmt.Println() + + functions = append(functions, jsonFunction{ + Name: fn.String(), + RelName: fn.RelString(fn.Pkg.Pkg), + Posn: toJSONPosition(posn), + Generated: gen, + }) + } + packages = append(packages, jsonPackage{ + Path: pkgpath, + Funcs: functions, + }) + } + + // 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 + } + + // -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 + if err := tmpl.Execute(&buf, object); err != nil { + log.Fatal(err) + } + if n := buf.Len(); n == 0 || buf.Bytes()[n-1] != '\n' { + buf.WriteByte('\n') + } + os.Stdout.Write(buf.Bytes()) } } @@ -297,3 +409,172 @@ 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 jsonPosition // file/line/column of declaration + 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 } + +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 ab8c81c86f0..f17a1227362 100644 --- a/internal/cmd/deadcode/deadcode_test.go +++ b/internal/cmd/deadcode/deadcode_test.go @@ -45,72 +45,101 @@ 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 "quoted" expected/unwanted string in output + // [!]deadcode args... command-line arguments + // [!]want arg expected/unwanted string in output (or stderr) // - var args []string - want := make(map[string]bool) // string -> sense - for _, line := range strings.Split(string(ar.Comment), "\n") { + // Args may be Go-quoted strings. + 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 } - fields := strings.Fields(line) - switch kind := fields[0]; kind { - case "deadcode": - args = fields[1:] // lossy wrt spaces + 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", "!deadcode": + current = &testcase{ + linenum: i + 1, + want: make(map[string]bool), + args: words[1:], + wantErr: kind[0] == '!', + } + cases = append(cases, current) 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 current == nil { + t.Fatalf("'want' directive must be after 'deadcode'") } - want[str] = kind[0] != '!' + if len(words) != 2 { + t.Fatalf("'want' directive needs argument <<%s>>", line) + } + 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) + } + } + }) } }) } @@ -129,3 +158,27 @@ 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 != "" { + s = strings.TrimSpace(s) + 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..8d28eb31288 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,80 @@ 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 Package +objects, as defined by the JSON schema (see below). + +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: + + -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: + + -f='{{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 -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: + + $ 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/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/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..f0f3ab21bd0 --- /dev/null +++ b/internal/cmd/deadcode/testdata/jsonflag.txtar @@ -0,0 +1,21 @@ +# 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` + want `"Line": 5,` + want `"Col": 6` + +-- go.mod -- +module example.com +go 1.18 + +-- p/p.go -- +package main + +func main() {} + +func Dead() {} diff --git a/internal/cmd/deadcode/testdata/lineflag.txtar b/internal/cmd/deadcode/testdata/lineflag.txtar index b817e4cde90..51940ad3274 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 "-f={{range .Funcs}}{{println .Name}}{{end}}" -filter= example.com want "(example.com.T).Goodbye" !want "(example.com.T).Hello" 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 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 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 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/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/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 { diff --git a/internal/gopathwalk/walk.go b/internal/gopathwalk/walk.go index 452e342c559..f79dd8cc3f5 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 @@ -105,7 +115,9 @@ 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 } // init initializes the walker based on its Options @@ -121,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) } } } @@ -162,9 +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 { - if os.SameFile(fi, ignoredDir) { + if dir == ignoredDir { return true } } @@ -176,20 +184,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 +212,66 @@ func (w *walker) walk(path string, typ os.FileMode) error { (!w.opts.ModulesEnabled && base == "node_modules") { return filepath.SkipDir } - fi, err := os.Lstat(path) - if err == nil && w.shouldSkipDir(fi, path) { + if w.shouldSkipDir(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 @@ -222,6 +281,10 @@ func (w *walker) walk(path string, typ os.FileMode) 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 @@ -234,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) } 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/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/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/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 9615ab43c59..06f64013c79 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" ) @@ -182,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 @@ -217,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) } } @@ -295,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 } @@ -382,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 @@ -399,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 } } @@ -414,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 } @@ -466,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. @@ -487,8 +499,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) @@ -506,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), @@ -751,6 +767,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: @@ -767,8 +793,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) @@ -830,11 +855,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 @@ -844,28 +882,35 @@ 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) } + return res, nil } - return res, nil } } @@ -890,15 +935,12 @@ 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. 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) && @@ -930,8 +972,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 && @@ -944,9 +984,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 @@ -982,15 +1019,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, } @@ -1086,7 +1140,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{ @@ -1098,8 +1152,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} @@ -1233,11 +1287,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 } @@ -1299,7 +1353,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 @@ -1307,6 +1361,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.) // @@ -1529,11 +1611,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: @@ -1675,14 +1758,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. @@ -1903,27 +1991,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 @@ -1971,7 +2055,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) { @@ -1995,17 +2079,35 @@ 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: - 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 @@ -2296,60 +2398,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. // @@ -2601,12 +2649,13 @@ func declares(stmts []ast.Stmt) map[string]bool { } } } + delete(names, "_") 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 8362e445b66..525be74ea60 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -388,6 +388,77 @@ 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 +}`, + }, + { + // (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 { + } +}`, + }, + }) +} + +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) +}`, + }, }) } @@ -533,6 +604,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) } }`, @@ -582,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{}) }`, }, }) } @@ -617,6 +718,18 @@ 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 }`, + }, + { + "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 }() }`, + }, }) } @@ -626,7 +739,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).", @@ -715,6 +828,39 @@ 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()) }`, + }, + { + "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/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/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/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 diff --git a/internal/refactor/inline/testdata/issue63298.txtar b/internal/refactor/inline/testdata/issue63298.txtar index e355e8e64d9..cc556c90ecd 100644 --- a/internal/refactor/inline/testdata/issue63298.txtar +++ b/internal/refactor/inline/testdata/issue63298.txtar @@ -38,15 +38,13 @@ func B() {} package a import ( - b "testdata/b" + "testdata/b" b0 "testdata/another/b" //@ inline(re"a2", result) ) 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/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 _() { 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 { 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 +} 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) + } +} 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)