diff --git a/cmd/goimports/goimports_gc.go b/cmd/goimports/goimports_gc.go index 190a56535ca..3326646d035 100644 --- a/cmd/goimports/goimports_gc.go +++ b/cmd/goimports/goimports_gc.go @@ -19,8 +19,8 @@ func doTrace() func() { bw, flush := bufferedFileWriter(*traceProfile) trace.Start(bw) return func() { - flush() trace.Stop() + flush() } } return func() {} diff --git a/cmd/guru/unit_test.go b/cmd/guru/unit_test.go index 7c24d714f19..0e4cd43b181 100644 --- a/cmd/guru/unit_test.go +++ b/cmd/guru/unit_test.go @@ -49,18 +49,22 @@ func TestIssue17515(t *testing.T) { {home + "/go", home + "/go/src/test/test.go", filepath.FromSlash(home + "/go/src")}, } - // Add symlink cases if not on Windows, Plan 9 - if runtime.GOOS != "windows" && runtime.GOOS != "plan9" { - // symlink between /tmp/home/go/src and /tmp/home/src - if err := os.Symlink(home+"/go/src", home+"/src"); err != nil { - t.Fatal(err) - } - + // symlink between /tmp/home/go/src and /tmp/home/src + symlinkErr := os.Symlink(filepath.Join("go", "src"), home+"/src") + if symlinkErr == nil { successTests = append(successTests, []SuccessTest{ {home + "/go", home + "/src/test/test.go", filepath.FromSlash(home + "/go/src")}, {home, home + "/go/src/test/test.go", filepath.FromSlash(home + "/src")}, {home, home + "/src/test/test.go", filepath.FromSlash(home + "/src")}, }...) + } else { + switch runtime.GOOS { + case "aix", "darwin", "dragonfly", "freebsd", "illumos", "linux", "netbsd", "openbsd", "solaris": + // Non-mobile OS known to always support symlinks. + t.Fatal(err) + default: + t.Logf("omitting symlink cases: %v", err) + } } for _, test := range successTests { @@ -85,7 +89,7 @@ func TestIssue17515(t *testing.T) { {home + "/go", home + "/go/src/fake/test.go", errFormat(filepath.FromSlash(home + "/go/src/fake"))}, } - if runtime.GOOS != "windows" && runtime.GOOS != "plan9" { + if symlinkErr == nil { failTests = append(failTests, []FailTest{ {home + "/go", home + "/src/fake/test.go", errFormat(filepath.FromSlash(home + "/src/fake"))}, {home, home + "/src/fake/test.go", errFormat(filepath.FromSlash(home + "/src/fake"))}, diff --git a/go.mod b/go.mod index 0f57d6751c5..8cf0ccc7da7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/yuin/goldmark v1.4.13 golang.org/x/mod v0.14.0 - golang.org/x/net v0.19.0 + golang.org/x/net v0.20.0 ) -require golang.org/x/sync v0.5.0 +require golang.org/x/sync v0.6.0 diff --git a/go.sum b/go.sum index cb917ea2726..cc5534add2c 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,7 @@ 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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -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/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index 7f79b4a7543..76f42b052e4 100644 --- a/go/analysis/passes/unusedresult/unusedresult.go +++ b/go/analysis/passes/unusedresult/unusedresult.go @@ -59,7 +59,25 @@ func init() { // List standard library functions here. // The context.With{Cancel,Deadline,Timeout} entries are // effectively redundant wrt the lostcancel analyzer. - funcs.Set("errors.New,fmt.Errorf,fmt.Sprintf,fmt.Sprint,sort.Reverse,context.WithValue,context.WithCancel,context.WithDeadline,context.WithTimeout") + funcs = stringSetFlag{ + "context.WithCancel": true, + "context.WithDeadline": true, + "context.WithTimeout": true, + "context.WithValue": true, + "errors.New": true, + "fmt.Errorf": true, + "fmt.Sprint": true, + "fmt.Sprintf": true, + "slices.Clip": true, + "slices.Compact": true, + "slices.CompactFunc": true, + "slices.Delete": true, + "slices.DeleteFunc": true, + "slices.Grow": true, + "slices.Insert": true, + "slices.Replace": true, + "sort.Reverse": true, + } Analyzer.Flags.Var(&funcs, "funcs", "comma-separated list of functions whose results must be used") diff --git a/go/callgraph/vta/graph.go b/go/callgraph/vta/graph.go index f68f4536e32..4b5a65b3336 100644 --- a/go/callgraph/vta/graph.go +++ b/go/callgraph/vta/graph.go @@ -497,7 +497,13 @@ func (b *builder) lookup(l *ssa.Lookup) { // No interesting flows for string lookups. return } - b.addInFlowAliasEdges(b.nodeFromVal(l), mapValue{typ: t.Elem()}) + + if !l.CommaOk { + b.addInFlowAliasEdges(b.nodeFromVal(l), mapValue{typ: t.Elem()}) + } else { + i := indexedLocal{val: l, typ: t.Elem(), index: 0} + b.addInFlowAliasEdges(i, mapValue{typ: t.Elem()}) + } } // mapUpdate handles map update commands m[b] = a where m is of type diff --git a/go/callgraph/vta/testdata/src/callgraph_comma_maps.go b/go/callgraph/vta/testdata/src/callgraph_comma_maps.go new file mode 100644 index 00000000000..47546d8de3e --- /dev/null +++ b/go/callgraph/vta/testdata/src/callgraph_comma_maps.go @@ -0,0 +1,84 @@ +// 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 ignore + +package testdata + +type I interface { + Name() string + Foo() +} + +var is = make(map[string]I) + +func init() { + register(A{}) + register(B{}) +} + +func register(i I) { + is[i.Name()] = i +} + +type A struct{} + +func (a A) Foo() {} +func (a A) Name() string { return "a" } + +type B struct{} + +func (b B) Foo() {} +func (b B) Name() string { return "b" } + +func Do(n string) { + i, ok := is[n] + if !ok { + return + } + i.Foo() +} + +func Go(n string) { + if i, ok := is[n]; !ok { + return + } else { + i.Foo() + } +} + +func To(n string) { + var i I + var ok bool + + if i, ok = is[n]; !ok { + return + } + i.Foo() +} + +func Ro(n string) { + i := is[n] + i.Foo() +} + +// Relevant SSA: +// func Do(n string): +// t0 = *is +// t1 = t0[n],ok +// t2 = extract t1 #0 +// t3 = extract t1 #1 +// if t3 goto 2 else 1 +// 1: +// return +// 2: +// t4 = invoke t2.Foo() +// return + +// WANT: +// register: invoke i.Name() -> A.Name, B.Name +// Do: invoke t2.Foo() -> A.Foo, B.Foo +// Go: invoke t2.Foo() -> A.Foo, B.Foo +// To: invoke t2.Foo() -> A.Foo, B.Foo +// Ro: invoke t1.Foo() -> A.Foo, B.Foo diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go index 2c6538c853a..76c6611d2dd 100644 --- a/go/callgraph/vta/vta_test.go +++ b/go/callgraph/vta/vta_test.go @@ -26,6 +26,7 @@ func TestVTACallGraph(t *testing.T) { "testdata/src/callgraph_field_funcs.go", "testdata/src/callgraph_recursive_types.go", "testdata/src/callgraph_issue_57756.go", + "testdata/src/callgraph_comma_maps.go", } { t.Run(file, func(t *testing.T) { prog, want, err := testProg(file, ssa.BuilderMode(0)) diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 0d6716c4296..8622dfc53a8 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go @@ -932,7 +932,10 @@ func (b *builder) receiver(fn *Function, e ast.Expr, wantAddr, escaping bool, se last := len(sel.index) - 1 // The position of implicit selection is the position of the inducing receiver expression. v = emitImplicitSelections(fn, v, sel.index[:last], e.Pos()) - if _, vptr := deref(v.Type()); !wantAddr && vptr { + if types.IsInterface(v.Type()) { + // When v is an interface, sel.Kind()==MethodValue and v.f is invoked. + // So v is not loaded, even if v has a pointer core type. + } else if _, vptr := deref(v.Type()); !wantAddr && vptr { v = emitLoad(fn, v) } return v diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go index 7c43b24c6c9..85c599443b7 100644 --- a/go/ssa/builder_generic_test.go +++ b/go/ssa/builder_generic_test.go @@ -483,6 +483,38 @@ func TestGenericBodies(t *testing.T) { } } `, + ` + package issue64324 + + type bar[T any] interface { + Bar(int) T + } + type foo[T any] interface { + bar[[]T] + *T + } + func Foo[T any, F foo[T]](d int) { + m := new(T) + f := F(m) + print(f.Bar(d)) /*@ types("[]T")*/ + } + `, ` + package issue64324b + + type bar[T any] interface { + Bar(int) T + } + type baz[T any] interface { + bar[*int] + *int + } + + func Baz[I baz[string]](d int) { + m := new(int) + f := I(m) + print(f.Bar(d)) /*@ types("*int")*/ + } + `, } { contents := contents pkgname := packageName(t, contents) diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 28ec131f8c4..22a3c6bc3dc 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go @@ -132,6 +132,11 @@ func (s *sanity) checkInstr(idx int, instr Instruction) { case *BinOp: case *Call: + if common := instr.Call; common.IsInvoke() { + if !types.IsInterface(common.Value.Type()) { + s.errorf("invoke on %s (%s) which is not an interface type (or type param)", common.Value, common.Value.Type()) + } + } case *ChangeInterface: case *ChangeType: case *SliceToArrayPointer: diff --git a/gopls/doc/commands.md b/gopls/doc/commands.md index 3244a77694e..a838c73df6b 100644 --- a/gopls/doc/commands.md +++ b/gopls/doc/commands.md @@ -581,6 +581,22 @@ Args: } ``` +### **List current Views on the server.** +Identifier: `gopls.views` + +This command is intended for use by gopls tests only. + +Result: + +``` +[]{ + "Type": string, + "Root": string, + "Folder": string, + "EnvOverlay": []string, +} +``` + ### **Fetch workspace statistics** Identifier: `gopls.workspace_stats` diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 95ca4988adb..d6ec1df356b 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -97,12 +97,14 @@ Default: `""`. **This setting is experimental and may be deleted.** -expandWorkspaceToModule instructs `gopls` to adjust the scope of the -workspace to find the best available module root. `gopls` first looks for -a go.mod file in any parent directory of the workspace folder, expanding -the scope to that directory if it exists. If no viable parent directory is -found, gopls will check if there is exactly one child directory containing -a go.mod file, narrowing the scope to that directory if it exists. +expandWorkspaceToModule determines which packages are considered +"workspace packages" when the workspace is using modules. + +Workspace packages affect the scope of workspace-wide operations. Notably, +gopls diagnoses all packages considered to be part of the workspace after +every keystroke, so by setting "ExpandWorkspaceToModule" to false, and +opening a nested workspace directory, you can reduce the amount of work +gopls has to do to keep your workspace up to date. Default: `true`. diff --git a/gopls/go.mod b/gopls/go.mod index 3ffb19163ae..903a7ada8a9 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -6,9 +6,8 @@ require ( github.com/google/go-cmp v0.5.9 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.14.0 - golang.org/x/sync v0.5.0 + golang.org/x/sync v0.6.0 golang.org/x/telemetry v0.0.0-20231114163143-69313e640400 golang.org/x/text v0.14.0 golang.org/x/tools v0.13.1-0.20230920233436-f9b8da7b22be @@ -23,7 +22,8 @@ require ( github.com/BurntSushi/toml v1.2.1 // indirect github.com/google/safehtml v0.1.0 // indirect golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.16.0 // indirect + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/gopls/go.sum b/gopls/go.sum index 4a310e92b4d..a4a914744ae 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -1,8 +1,5 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -19,35 +16,28 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -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.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y= golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 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.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20231114163143-69313e640400 h1:brbkEFfGwNGAEkykUOcryE/JiHUMMJouzE0fWWmz/QU= golang.org/x/telemetry v0.0.0-20231114163143-69313e640400/go.mod h1:P6hMdmAcoG7FyATwqSr6R/U0n7yeXNP/QXeRlxb1szE= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -59,9 +49,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo= diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go index b7bb17b0665..e2337a111c8 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/analysis/fillstruct/fillstruct.go @@ -144,9 +144,9 @@ func DiagnoseFillableStructs(inspect *inspector.Inspector, start, end token.Pos, // SuggestedFix computes the suggested fix for the kinds of // diagnostics produced by the Analyzer above. -func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { if info == nil { - return nil, fmt.Errorf("nil types.Info") + return nil, nil, fmt.Errorf("nil types.Info") } pos := start // don't use the end @@ -155,7 +155,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil // calling PathEnclosingInterval. Switch this approach. path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) == 0 { - return nil, fmt.Errorf("no enclosing ast.Node") + return nil, nil, fmt.Errorf("no enclosing ast.Node") } var expr *ast.CompositeLit for _, n := range path { @@ -167,14 +167,14 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil typ := info.TypeOf(expr) if typ == nil { - return nil, fmt.Errorf("no composite literal") + return nil, nil, fmt.Errorf("no composite literal") } // Find reference to the type declaration of the struct being initialized. typ = deref(typ) tStruct, ok := typ.Underlying().(*types.Struct) if !ok { - return nil, fmt.Errorf("%s is not a (pointer to) struct type", + return nil, nil, fmt.Errorf("%s is not a (pointer to) struct type", types.TypeString(typ, types.RelativeTo(pkg))) } // Inv: typ is the possibly-named struct type. @@ -240,7 +240,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil } else { names, ok := matches[fieldTyp] if !ok { - return nil, fmt.Errorf("invalid struct field type: %v", fieldTyp) + return nil, nil, fmt.Errorf("invalid struct field type: %v", fieldTyp) } // Find the name most similar to the field name. @@ -251,7 +251,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil } else if v := populateValue(file, pkg, fieldTyp); v != nil { kv.Value = v } else { - return nil, nil + return nil, nil, nil // no fix to suggest } } elts = append(elts, kv) @@ -260,7 +260,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil // If all of the struct's fields are unexported, we have nothing to do. if len(elts) == 0 { - return nil, fmt.Errorf("no elements to fill") + return nil, nil, fmt.Errorf("no elements to fill") } // Add the final line for the right brace. Offset is the number of @@ -292,7 +292,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil // First pass through the formatter: turn the expr into a string. var formatBuf bytes.Buffer if err := format.Node(&formatBuf, fakeFset, cl); err != nil { - return nil, fmt.Errorf("failed to run first format on:\n%s\ngot err: %v", cl.Type, err) + return nil, nil, fmt.Errorf("failed to run first format on:\n%s\ngot err: %v", cl.Type, err) } sug := indent(formatBuf.Bytes(), whitespace) @@ -304,7 +304,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil } } - return &analysis.SuggestedFix{ + return fset, &analysis.SuggestedFix{ TextEdits: []analysis.TextEdit{ { Pos: expr.Pos(), diff --git a/gopls/internal/analysis/stubmethods/stubmethods.go b/gopls/internal/analysis/stubmethods/stubmethods.go index 8f9f8c7900b..02eef5c29c1 100644 --- a/gopls/internal/analysis/stubmethods/stubmethods.go +++ b/gopls/internal/analysis/stubmethods/stubmethods.go @@ -66,7 +66,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // MatchesMessage reports whether msg matches the error message sought after by // the stubmethods fix. func MatchesMessage(msg string) bool { - return strings.Contains(msg, "missing method") || strings.HasPrefix(msg, "cannot convert") + return strings.Contains(msg, "missing method") || strings.HasPrefix(msg, "cannot convert") || strings.Contains(msg, "not implement") } // DiagnosticForError computes a diagnostic suggesting to implement an diff --git a/gopls/internal/analysis/stubmethods/stubmethods_test.go b/gopls/internal/analysis/stubmethods/stubmethods_test.go new file mode 100644 index 00000000000..86328ae4606 --- /dev/null +++ b/gopls/internal/analysis/stubmethods/stubmethods_test.go @@ -0,0 +1,17 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package stubmethods_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/gopls/internal/analysis/stubmethods" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.Run(t, testdata, stubmethods.Analyzer, "a") +} diff --git a/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go b/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go new file mode 100644 index 00000000000..be20e1d9904 --- /dev/null +++ b/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go @@ -0,0 +1,15 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package stubmethods + +var _ I = Y{} // want "Implement I" + +type I interface{ F() } + +type X struct{} + +func (X) F(string) {} + +type Y struct{ X } diff --git a/gopls/internal/analysis/undeclaredname/undeclared.go b/gopls/internal/analysis/undeclaredname/undeclared.go index 147831c07aa..377c635a5b7 100644 --- a/gopls/internal/analysis/undeclaredname/undeclared.go +++ b/gopls/internal/analysis/undeclaredname/undeclared.go @@ -109,15 +109,15 @@ func runForError(pass *analysis.Pass, err types.Error) { }) } -func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { +func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { pos := start // don't use the end path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) < 2 { - return nil, fmt.Errorf("no expression found") + return nil, nil, fmt.Errorf("no expression found") } ident, ok := path[0].(*ast.Ident) if !ok { - return nil, fmt.Errorf("no identifier found") + return nil, nil, fmt.Errorf("no identifier found") } // Check for a possible call expression, in which case we should add a @@ -131,7 +131,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil // Get the place to insert the new statement. insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path) if insertBeforeStmt == nil { - return nil, fmt.Errorf("could not locate insertion point") + return nil, nil, fmt.Errorf("could not locate insertion point") } insertBefore := safetoken.StartPosition(fset, insertBeforeStmt.Pos()).Offset @@ -145,7 +145,7 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil // Create the new local variable statement. newStmt := fmt.Sprintf("%s := %s", ident.Name, indent) - return &analysis.SuggestedFix{ + return fset, &analysis.SuggestedFix{ Message: fmt.Sprintf("Create variable \"%s\"", ident.Name), TextEdits: []analysis.TextEdit{{ Pos: insertBeforeStmt.Pos(), @@ -155,17 +155,17 @@ func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, fil }, nil } -func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, info *types.Info, fset *token.FileSet) (*analysis.SuggestedFix, error) { +func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, info *types.Info, fset *token.FileSet) (*token.FileSet, *analysis.SuggestedFix, error) { if len(path) < 3 { - return nil, fmt.Errorf("unexpected set of enclosing nodes: %v", path) + return nil, nil, fmt.Errorf("unexpected set of enclosing nodes: %v", path) } ident, ok := path[0].(*ast.Ident) if !ok { - return nil, fmt.Errorf("no name for function declaration %v (%T)", path[0], path[0]) + return nil, nil, fmt.Errorf("no name for function declaration %v (%T)", path[0], path[0]) } call, ok := path[1].(*ast.CallExpr) if !ok { - return nil, fmt.Errorf("no call expression found %v (%T)", path[1], path[1]) + return nil, nil, fmt.Errorf("no call expression found %v (%T)", path[1], path[1]) } // Find the enclosing function, so that we can add the new declaration @@ -180,7 +180,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, // TODO(rstambler): Support the situation when there is no enclosing // function. if enclosing == nil { - return nil, fmt.Errorf("no enclosing function found: %v", path) + return nil, nil, fmt.Errorf("no enclosing function found: %v", path) } pos := enclosing.End() @@ -192,7 +192,7 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, for _, arg := range call.Args { typ := info.TypeOf(arg) if typ == nil { - return nil, fmt.Errorf("unable to determine type for %s", arg) + return nil, nil, fmt.Errorf("unable to determine type for %s", arg) } switch t := typ.(type) { @@ -291,9 +291,9 @@ func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, b := bytes.NewBufferString("\n\n") if err := format.Node(b, fset, decl); err != nil { - return nil, err + return nil, nil, err } - return &analysis.SuggestedFix{ + return fset, &analysis.SuggestedFix{ Message: fmt.Sprintf("Create function \"%s\"", ident.Name), TextEdits: []analysis.TextEdit{{ Pos: pos, diff --git a/gopls/internal/cmd/execute.go b/gopls/internal/cmd/execute.go index 22d50e7e766..22e7820b36b 100644 --- a/gopls/internal/cmd/execute.go +++ b/gopls/internal/cmd/execute.go @@ -44,7 +44,7 @@ This interface is experimental and commands may change or disappear without noti Examples: - $ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI", "file:///hello.go"}' + $ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI": "file:///hello.go"}' $ gopls execute gopls.run_tests '{"URI": "file:///a_test.go", "Tests": ["Test"]}' $ gopls execute gopls.list_known_packages '{"URI": "file:///hello.go"}' diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index db7609d161d..4da649f5b4c 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go @@ -978,7 +978,7 @@ var _ io.Reader = C{} type C struct{} // Read implements io.Reader. -func (C) Read(p []byte) (n int, err error) { +func (c C) Read(p []byte) (n int, err error) { panic("unimplemented") } `[1:] @@ -1042,7 +1042,7 @@ func writeTree(t *testing.T, archive string) string { root := t.TempDir() // This unfortunate step is required because gopls output - // expands symbolic links it its input file names (arguably it + // expands symbolic links in its input file names (arguably it // should not), and on macOS the temp dir is in /var -> private/var. root, err := filepath.EvalSymlinks(root) if err != nil { diff --git a/gopls/internal/cmd/suggested_fix.go b/gopls/internal/cmd/suggested_fix.go index 7ba9c7fb840..9fe64977e7d 100644 --- a/gopls/internal/cmd/suggested_fix.go +++ b/gopls/internal/cmd/suggested_fix.go @@ -10,6 +10,7 @@ import ( "fmt" "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/gopls/internal/util/slices" "golang.org/x/tools/internal/tool" ) @@ -148,42 +149,22 @@ func (s *suggestedFix) Run(ctx context.Context, args ...string) error { continue } - // Partially apply CodeAction.Edit, a WorkspaceEdit. - // (See also conn.Client.applyWorkspaceEdit(a.Edit)). - if !from.HasPosition() { - for _, c := range a.Edit.DocumentChanges { - if c.TextDocumentEdit != nil { - if c.TextDocumentEdit.TextDocument.URI == uri { - edits = append(edits, protocol.AsTextEdits(c.TextDocumentEdit.Edits)...) - } - } - } + // If the provided span has a position (not just offsets), + // and the action has diagnostics, the action must have a + // diagnostic with the same range as it. + if from.HasPosition() && len(a.Diagnostics) > 0 && + !slices.ContainsFunc(a.Diagnostics, func(diag protocol.Diagnostic) bool { + return diag.Range.Start == rng.Start + }) { continue } - // The provided span has a position (not just offsets). - // Find the code action that has the same range as it. - for _, diag := range a.Diagnostics { - if diag.Range.Start == rng.Start { - for _, c := range a.Edit.DocumentChanges { - if c.TextDocumentEdit != nil { - if c.TextDocumentEdit.TextDocument.URI == uri { - edits = append(edits, protocol.AsTextEdits(c.TextDocumentEdit.Edits)...) - } - } - } - break - } - } - - // If suggested fix is not a diagnostic, still must collect edits. - if len(a.Diagnostics) == 0 { - for _, c := range a.Edit.DocumentChanges { - if c.TextDocumentEdit != nil { - if c.TextDocumentEdit.TextDocument.URI == uri { - edits = append(edits, protocol.AsTextEdits(c.TextDocumentEdit.Edits)...) - } - } + // Partially apply CodeAction.Edit, a WorkspaceEdit. + // (See also conn.Client.applyWorkspaceEdit(a.Edit)). + for _, c := range a.Edit.DocumentChanges { + tde := c.TextDocumentEdit + if tde != nil && tde.TextDocument.URI == uri { + edits = append(edits, protocol.AsTextEdits(tde.Edits)...) } } } diff --git a/gopls/internal/cmd/usage/execute.hlp b/gopls/internal/cmd/usage/execute.hlp index c5fb557d8e8..9fb9ece2988 100644 --- a/gopls/internal/cmd/usage/execute.hlp +++ b/gopls/internal/cmd/usage/execute.hlp @@ -15,7 +15,7 @@ This interface is experimental and commands may change or disappear without noti Examples: - $ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI", "file:///hello.go"}' + $ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI": "file:///hello.go"}' $ gopls execute gopls.run_tests '{"URI": "file:///a_test.go", "Tests": ["Test"]}' $ gopls execute gopls.list_known_packages '{"URI": "file:///hello.go"}' diff --git a/gopls/internal/coverage/coverage.go b/gopls/internal/coverage/coverage.go deleted file mode 100644 index 9b630dee833..00000000000 --- a/gopls/internal/coverage/coverage.go +++ /dev/null @@ -1,266 +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 go.1.16 -// +build go.1.16 - -// Running this program in the tools directory will produce a coverage file /tmp/cover.out -// and a coverage report for all the packages under internal/lsp, accumulated by all the tests -// under gopls. -// -// -o controls where the coverage file is written, defaulting to /tmp/cover.out -// -i coverage-file will generate the report from an existing coverage file -// -v controls verbosity (0: only report coverage, 1: report as each directory is finished, -// -// 2: report on each test, 3: more details, 4: too much) -// -// -t tests only tests packages in the given comma-separated list of directories in gopls. -// -// The names should start with ., as in ./internal/test/integrationo/bench -// -// -run tests. If set, -run tests is passed on to the go test command. -// -// Despite gopls' use of goroutines, the counts are almost deterministic. -package main - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "sort" - "strings" - "time" - - "golang.org/x/tools/cover" -) - -var ( - proFile = flag.String("i", "", "existing profile file") - outFile = flag.String("o", "/tmp/cover.out", "where to write the coverage file") - verbose = flag.Int("v", 0, "how much detail to print as tests are running") - tests = flag.String("t", "", "list of tests to run") - run = flag.String("run", "", "value of -run to pass to go test") -) - -func main() { - log.SetFlags(log.Lshortfile) - flag.Parse() - - if *proFile != "" { - report(*proFile) - return - } - - checkCwd() - // find the packages under gopls containing tests - tests := listDirs("gopls") - tests = onlyTests(tests) - tests = realTestName(tests) - - // report coverage for packages under internal/lsp - parg := "golang.org/x/tools/gopls/internal/lsp/..." - - accum := []string{} - seen := make(map[string]bool) - now := time.Now() - for _, toRun := range tests { - if excluded(toRun) { - continue - } - x := runTest(toRun, parg) - if *verbose > 0 { - fmt.Printf("finished %s %.1fs\n", toRun, time.Since(now).Seconds()) - } - lines := bytes.Split(x, []byte{'\n'}) - for _, l := range lines { - if len(l) == 0 { - continue - } - if !seen[string(l)] { - // not accumulating counts, so only works for mode:set - seen[string(l)] = true - accum = append(accum, string(l)) - } - } - } - sort.Strings(accum[1:]) - if err := os.WriteFile(*outFile, []byte(strings.Join(accum, "\n")), 0644); err != nil { - log.Print(err) - } - report(*outFile) -} - -type result struct { - Time time.Time - Test string - Action string - Package string - Output string - Elapsed float64 -} - -func runTest(tName, parg string) []byte { - args := []string{"test", "-short", "-coverpkg", parg, "-coverprofile", *outFile, - "-json"} - if *run != "" { - args = append(args, fmt.Sprintf("-run=%s", *run)) - } - args = append(args, tName) - cmd := exec.Command("go", args...) - cmd.Dir = "./gopls" - ans, err := cmd.Output() - if *verbose > 1 { - got := strings.Split(string(ans), "\n") - for _, g := range got { - if g == "" { - continue - } - var m result - if err := json.Unmarshal([]byte(g), &m); err != nil { - log.Printf("%T/%v", err, err) // shouldn't happen - continue - } - maybePrint(m) - } - } - if err != nil { - log.Printf("%s: %q, cmd=%s", tName, ans, cmd.String()) - } - buf, err := os.ReadFile(*outFile) - if err != nil { - log.Fatal(err) - } - return buf -} - -func report(fn string) { - profs, err := cover.ParseProfiles(fn) - if err != nil { - log.Fatal(err) - } - for _, p := range profs { - statements, counts := 0, 0 - for _, x := range p.Blocks { - statements += x.NumStmt - if x.Count != 0 { - counts += x.NumStmt // sic: if any were executed, all were - } - } - pc := 100 * float64(counts) / float64(statements) - fmt.Printf("%3.0f%% %3d/%3d %s\n", pc, counts, statements, p.FileName) - } -} - -var todo []string // tests to run - -func excluded(tname string) bool { - if *tests == "" { // run all tests - return false - } - if todo == nil { - todo = strings.Split(*tests, ",") - } - for _, nm := range todo { - if tname == nm { // run this test - return false - } - } - // not in list, skip it - return true -} - -// should m.Package be printed sometime? -func maybePrint(m result) { - switch m.Action { - case "pass", "fail", "skip": - fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) - case "run": - if *verbose > 2 { - fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) - } - case "output": - if *verbose > 3 { - fmt.Printf("%s %s %q %.3f\n", m.Action, m.Test, m.Output, m.Elapsed) - } - case "pause", "cont": - if *verbose > 2 { - fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) - } - default: - fmt.Printf("%#v\n", m) - log.Fatalf("unknown action %s\n", m.Action) - } -} - -// return only the directories that contain tests -func onlyTests(s []string) []string { - ans := []string{} -outer: - for _, d := range s { - files, err := os.ReadDir(d) - if err != nil { - log.Fatalf("%s: %v", d, err) - } - for _, de := range files { - if strings.Contains(de.Name(), "_test.go") { - ans = append(ans, d) - continue outer - } - } - } - return ans -} - -// replace the prefix gopls/ with ./ as the tests are run in the gopls directory -func realTestName(p []string) []string { - ans := []string{} - for _, x := range p { - x = x[len("gopls/"):] - ans = append(ans, "./"+x) - } - return ans -} - -// make sure we start in a tools directory -func checkCwd() { - dir, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - // we expect to be at the root of golang.org/x/tools - cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "golang.org/x/tools") - buf, err := cmd.Output() - buf = bytes.Trim(buf, "\n \t") // remove \n at end - if err != nil { - log.Fatal(err) - } - if string(buf) != dir { - log.Fatalf("wrong directory: in %q, should be in %q", dir, string(buf)) - } - // and we expect gopls and internal/lsp as subdirectories - _, err = os.Stat("gopls") - if err != nil { - log.Fatalf("expected a gopls directory, %v", err) - } -} - -func listDirs(dir string) []string { - ans := []string{} - f := func(path string, dirEntry os.DirEntry, err error) error { - if strings.HasSuffix(path, "/testdata") || strings.HasSuffix(path, "/typescript") { - return filepath.SkipDir - } - if dirEntry.IsDir() { - ans = append(ans, path) - } - return nil - } - filepath.WalkDir(dir, f) - return ans -} diff --git a/gopls/internal/debug/info.go b/gopls/internal/debug/info.go index 579e54978b7..84027ec43e1 100644 --- a/gopls/internal/debug/info.go +++ b/gopls/internal/debug/info.go @@ -144,16 +144,3 @@ func printModuleInfo(w io.Writer, m debug.Module, _ PrintMode) { } fmt.Fprintf(w, "\n") } - -type field struct { - index []int -} - -var fields []field - -type sessionOption struct { - Name string - Type string - Current string - Default string -} diff --git a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go index e337f006fdd..d7ba381d3d5 100644 --- a/gopls/internal/debug/serve.go +++ b/gopls/internal/debug/serve.go @@ -791,29 +791,6 @@ Using session: {{template "sessionlink" .Session.ID}}
{{if .DebugAddress}}Debug this client at: {{localAddress .DebugAddress}}
{{end}} Logfile: {{.Logfile}}
Gopls Path: {{.GoplsPath}}
-

Diagnostics

-{{/*Service: []protocol.Server; each server has map[uri]fileReports; - each fileReport: map[diagnosticSoure]diagnosticReport - diagnosticSource is one of 5 source - diagnosticReport: snapshotID and map[hash]*source.Diagnostic - sourceDiagnostic: struct { - Range protocol.Range - Message string - Source string - Code string - CodeHref string - Severity protocol.DiagnosticSeverity - Tags []protocol.DiagnosticTag - - Related []RelatedInformation - } - RelatedInformation: struct { - URI protocol.DocumentURI - Range protocol.Range - Message string - } - */}} - {{end}} `)) @@ -831,7 +808,7 @@ var SessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` {{define "body"}} From: {{template "cachelink" .Cache.ID}}

Views

- +

Overlays

{{$session := .}}