diff --git a/cmd/present/doc.go b/cmd/present/doc.go index a5065f0..bc34117 100644 --- a/cmd/present/doc.go +++ b/cmd/present/doc.go
@@ -6,6 +6,8 @@ Present displays slide presentations and articles. It runs a web server that presents slide and article files from the current directory. +Present assumes presentation authors are trusted, and should not be used with untrusted inputs. + It may be run as a stand-alone command or an App Engine app. To use with App Engine, copy the files in the tools/cmd/present directory to the
diff --git a/cmd/present/main.go b/cmd/present/main.go index 6c7a40c..36786ce 100644 --- a/cmd/present/main.go +++ b/cmd/present/main.go
@@ -117,7 +117,7 @@ and port will have access to this machine as the user running present. To avoid this message, listen on localhost, run with -play=false, or run with --play_socket=false. +-use_playground=true. If you don't understand this message, hit Control-C to terminate this process.
diff --git a/cmd/present2md/main.go b/cmd/present2md/main.go index d775344..2caf569 100644 --- a/cmd/present2md/main.go +++ b/cmd/present2md/main.go
@@ -4,6 +4,8 @@ // Present2md converts legacy-syntax present files to Markdown-syntax present files. // +// It assumes presentation authors are trusted, and should not be used with untrusted inputs. +// // Usage: // // present2md [-w] [file ...]
diff --git a/go.mod b/go.mod index afba373..a2b6ae7 100644 --- a/go.mod +++ b/go.mod
@@ -5,10 +5,10 @@ require ( github.com/google/go-cmp v0.6.0 github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.37.0 - golang.org/x/net v0.56.0 - golang.org/x/sync v0.21.0 - golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 + golang.org/x/mod v0.39.0 + golang.org/x/net v0.58.0 + golang.org/x/sync v0.22.0 + golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 ) -require golang.org/x/sys v0.46.0 // indirect +require golang.org/x/sys v0.47.0 // indirect
diff --git a/go.sum b/go.sum index 8c315fa..475f513 100644 --- a/go.sum +++ b/go.sum
@@ -2,13 +2,13 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= -golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 h1:ZUSxONxc981v7AW7QUg+I9WwZzSTTJ019ENBYr5pV/Q= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5/go.mod h1:LVehoXe41cL5SCVQilsV7Gg6BNG+Js6P9PhSbYTIUkQ=
diff --git a/go/analysis/analysistest/analysistest.go b/go/analysis/analysistest/analysistest.go index ef339a4..b448fb4 100644 --- a/go/analysis/analysistest/analysistest.go +++ b/go/analysis/analysistest/analysistest.go
@@ -419,7 +419,7 @@ }) } - res, err := checker.Analyze([]*analysis.Analyzer{a}, pkgs, nil) + res, err := checker.Analyze([]*analysis.Analyzer{a}, pkgs, &checker.Options{SanityCheck: true}) if err != nil { t.Errorf("Analyze: %v", err) return nil
diff --git a/go/analysis/checker/checker.go b/go/analysis/checker/checker.go index bb69dac..bc844ca 100644 --- a/go/analysis/checker/checker.go +++ b/go/analysis/checker/checker.go
@@ -401,7 +401,7 @@ if act.opts.SanityCheck { encodedFact, err := codeFact(fact) if err != nil { - log.Panicf("internal error: encoding of %T fact failed in %v", fact, act) + log.Panicf("internal error: encoding of %T fact failed in %v: %v", fact, act, err) } fact = encodedFact } @@ -435,7 +435,7 @@ if act.opts.SanityCheck { encodedFact, err := codeFact(fact) if err != nil { - log.Panicf("internal error: encoding of %T fact failed in %v", fact, act) + log.Panicf("internal error: encoding of %T fact failed in %v: %v", fact, act, err) } fact = encodedFact }
diff --git a/go/analysis/passes/composite/testdata/src/a/a.go b/go/analysis/passes/composite/testdata/src/a/a.go index cd69d39..b401134 100644 --- a/go/analysis/passes/composite/testdata/src/a/a.go +++ b/go/analysis/passes/composite/testdata/src/a/a.go
@@ -7,12 +7,13 @@ package a import ( - "flag" "go/scanner" "go/token" "image" "sync" "unicode" + + "./flag" ) var Okay1 = []string{
diff --git a/go/analysis/passes/composite/testdata/src/a/a.go.golden b/go/analysis/passes/composite/testdata/src/a/a.go.golden index fe73a2e..8498814 100644 --- a/go/analysis/passes/composite/testdata/src/a/a.go.golden +++ b/go/analysis/passes/composite/testdata/src/a/a.go.golden
@@ -7,12 +7,13 @@ package a import ( - "flag" "go/scanner" "go/token" "image" "sync" "unicode" + + "./flag" ) var Okay1 = []string{
diff --git a/go/analysis/passes/composite/testdata/src/a/flag/flag.go b/go/analysis/passes/composite/testdata/src/a/flag/flag.go new file mode 100644 index 0000000..8159fd8 --- /dev/null +++ b/go/analysis/passes/composite/testdata/src/a/flag/flag.go
@@ -0,0 +1,12 @@ +// Copyright 2026 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 flag + +type Flag struct { + Name string // name as it appears on command line + Usage string // help message + Value any // value as set + DefValue string // default value (as text); for usage message +}
diff --git a/go/analysis/passes/fieldalignment/fieldalignment.go b/go/analysis/passes/fieldalignment/fieldalignment.go index 53c7463..02791bc 100644 --- a/go/analysis/passes/fieldalignment/fieldalignment.go +++ b/go/analysis/passes/fieldalignment/fieldalignment.go
@@ -14,6 +14,7 @@ "go/token" "go/types" "sort" + "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -68,37 +69,66 @@ func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - nodeFilter := []ast.Node{ - (*ast.StructType)(nil), + for curStruct := range inspect.Root().Preorder((*ast.StructType)(nil)) { + s := curStruct.Node().(*ast.StructType) + // For every named struct defined as "type Name struct { ... }", + // the *ast.StructType node has a parent *ast.TypeSpec, + // which contains the struct's name in its Name field. + name := "struct" // (anonymous) + if spec, ok := curStruct.Parent().Node().(*ast.TypeSpec); ok { + name = spec.Name.Name + } + fieldalignment(pass, s, name) } - inspect.Preorder(nodeFilter, func(node ast.Node) { - var s *ast.StructType - var ok bool - if s, ok = node.(*ast.StructType); !ok { - return - } - if tv, ok := pass.TypesInfo.Types[s]; ok { - fieldalignment(pass, s, tv.Type.(*types.Struct)) - } - }) + return nil, nil } -var unsafePointerTyp = types.Unsafe.Scope().Lookup("Pointer").(*types.TypeName).Type() +func fieldalignment(pass *analysis.Pass, node *ast.StructType, name string) { + var ( + sizes = &gcSizes{ + wordSize: pass.TypesSizes.Sizeof(types.Typ[types.UnsafePointer]), + maxAlign: pass.TypesSizes.Alignof(types.Typ[types.UnsafePointer]), + } -func fieldalignment(pass *analysis.Pass, node *ast.StructType, typ *types.Struct) { - wordSize := pass.TypesSizes.Sizeof(unsafePointerTyp) - maxAlign := pass.TypesSizes.Alignof(unsafePointerTyp) + typ = pass.TypesInfo.TypeOf(node).(*types.Struct) + optimal, indexes = optimalOrder(typ, sizes) - s := gcSizes{wordSize, maxAlign} - optimal, indexes := optimalOrder(typ, &s) - optsz, optptrs := s.Sizeof(optimal), s.ptrdata(optimal) + actualSize = sizes.sizeof(typ) + actualPtrs = sizes.ptrdata(typ) - var message string - if sz := s.Sizeof(typ); sz != optsz { - message = fmt.Sprintf("struct of size %d could be %d", sz, optsz) - } else if ptrs := s.ptrdata(typ); ptrs != optptrs { - message = fmt.Sprintf("struct with %d pointer bytes could be %d", ptrs, optptrs) + optimalSize = sizes.sizeof(optimal) + optimalPtrs = sizes.ptrdata(optimal) + ) + + var message strings.Builder + if actualSize != optimalSize { + // Struct could be smaller. + // TODO(adonovan): IMHO the criterion should be "significantly smaller". + fmt.Fprintf(&message, "%s has size %d", name, actualSize) + actualClass := classSize(actualSize) + if actualClass == -1 { + actualClass = actualSize + fmt.Fprint(&message, " (uses global allocator)") + } else if actualClass != actualSize { + fmt.Fprintf(&message, " (allocator size class %d)", actualClass) + } + + fmt.Fprintf(&message, " but the optimal size is %d", optimalSize) + optimalClass := classSize(optimalSize) + if optimalClass == -1 { + optimalClass = optimalSize + } else if optimalClass != optimalSize { + fmt.Fprintf(&message, " (allocator size class %d)", optimalClass) + } + + wastage := actualClass - optimalClass + if percentage := wastage * 100 / actualClass; percentage > 25 { + fmt.Fprintf(&message, " leading to a waste of %d bytes (%d%%)", wastage, percentage) + } + } else if actualPtrs != optimalPtrs { + // Struct could place pointers more efficiently for GC marking. + fmt.Fprintf(&message, "%s has %d leading bytes of pointer data but optimal value is %d", name, actualPtrs, optimalPtrs) } else { // Already optimal order. return @@ -151,7 +181,7 @@ pass.Report(analysis.Diagnostic{ Pos: node.Pos(), End: node.Pos() + token.Pos(len("struct")), - Message: message, + Message: message.String(), SuggestedFixes: []analysis.SuggestedFix{{ Message: "Rearrange fields", TextEdits: []analysis.TextEdit{{ @@ -179,8 +209,8 @@ ft := field.Type() elems[i] = elem{ i, - sizes.Alignof(ft), - sizes.Sizeof(ft), + sizes.alignof(ft), + sizes.sizeof(ft), sizes.ptrdata(ft), } } @@ -240,40 +270,42 @@ return types.NewStruct(fields, nil), indexes } -// Code below based on go/types.StdSizes. +// gcSizes implements cmd/compile layout rules, providing ptrdata (GC +// scanning limits) and trailing zero-size field padding not available +// in [types.Sizes]. type gcSizes struct { - WordSize int64 - MaxAlign int64 + wordSize int64 + maxAlign int64 } -func (s *gcSizes) Alignof(T types.Type) int64 { +func (s *gcSizes) alignof(T types.Type) int64 { // For arrays and structs, alignment is defined in terms // of alignment of the elements and fields, respectively. switch t := T.Underlying().(type) { case *types.Array: // spec: "For a variable x of array type: unsafe.Alignof(x) // is the same as unsafe.Alignof(x[0]), but at least 1." - return s.Alignof(t.Elem()) + return s.alignof(t.Elem()) case *types.Struct: // spec: "For a variable x of struct type: unsafe.Alignof(x) // is the largest of the values unsafe.Alignof(x.f) for each // field f of x, but at least 1." max := int64(1) for i, nf := 0, t.NumFields(); i < nf; i++ { - if a := s.Alignof(t.Field(i).Type()); a > max { + if a := s.alignof(t.Field(i).Type()); a > max { max = a } } return max } - a := s.Sizeof(T) // may be 0 + a := s.sizeof(T) // may be 0 // spec: "For a variable x of any type: unsafe.Alignof(x) is at least 1." if a < 1 { return 1 } - if a > s.MaxAlign { - return s.MaxAlign + if a > s.maxAlign { + return s.maxAlign } return a } @@ -294,7 +326,7 @@ types.Complex128: 16, } -func (s *gcSizes) Sizeof(T types.Type) int64 { +func (s *gcSizes) sizeof(T types.Type) int64 { switch t := T.Underlying().(type) { case *types.Basic: k := t.Kind() @@ -304,12 +336,12 @@ } } if k == types.String { - return s.WordSize * 2 + return s.wordSize * 2 } case *types.Array: - return t.Len() * s.Sizeof(t.Elem()) + return t.Len() * s.sizeof(t.Elem()) case *types.Slice: - return s.WordSize * 3 + return s.wordSize * 3 case *types.Struct: nf := t.NumFields() if nf == 0 { @@ -320,7 +352,7 @@ max := int64(1) for i := range nf { ft := t.Field(i).Type() - a, sz := s.Alignof(ft), s.Sizeof(ft) + a, sz := s.alignof(ft), s.sizeof(ft) if a > max { max = a } @@ -331,9 +363,9 @@ } return align(o, max) case *types.Interface: - return s.WordSize * 2 + return s.wordSize * 2 } - return s.WordSize // catch-all + return s.wordSize // catch-all } // align returns the smallest y >= x such that y % a == 0. @@ -347,13 +379,13 @@ case *types.Basic: switch t.Kind() { case types.String, types.UnsafePointer: - return s.WordSize + return s.wordSize } return 0 case *types.Chan, *types.Map, *types.Pointer, *types.Signature, *types.Slice: - return s.WordSize + return s.wordSize case *types.Interface: - return 2 * s.WordSize + return 2 * s.wordSize case *types.Array: n := t.Len() if n == 0 { @@ -363,7 +395,7 @@ if a == 0 { return 0 } - z := s.Sizeof(t.Elem()) + z := s.sizeof(t.Elem()) return (n-1)*z + a case *types.Struct: nf := t.NumFields() @@ -374,7 +406,7 @@ var o, p int64 for i := range nf { ft := t.Field(i).Type() - a, sz := s.Alignof(ft), s.Sizeof(ft) + a, sz := s.alignof(ft), s.sizeof(ft) fp := s.ptrdata(ft) o = align(o, a) if fp != 0 { @@ -387,3 +419,16 @@ panic("impossible") } + +// Code below based on tools/gopls/internal/golang/hover.go + +// classSize reports the size class for a struct of the specified size, or -1 if unknown. +// See GOROOT/src/runtime/msize.go for details. +func classSize(size int64) int64 { + if size > 1<<15 { + return -1 // avoid allocation + } + // We assume that bytes.Clone doesn't trim, + // and reports the underlying size class + return int64(cap(bytes.Clone(make([]byte, size)))) +}
diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a.go b/go/analysis/passes/fieldalignment/testdata/src/a/a.go index 463b4cb..8e888e7 100644 --- a/go/analysis/passes/fieldalignment/testdata/src/a/a.go +++ b/go/analysis/passes/fieldalignment/testdata/src/a/a.go
@@ -6,7 +6,7 @@ z byte } -type Bad struct { // want "struct of size 12 could be 8" +type Bad struct { // want "Bad has size 12 \\(allocator size class 16\\) but the optimal size is 8 leading to a waste of 8 bytes \\(50%\\)" x byte y int32 z byte @@ -17,7 +17,7 @@ b uint32 } -type ZeroBad struct { // want "struct of size 8 could be 4" +type ZeroBad struct { // want "ZeroBad has size 8 but the optimal size is 4 \\(allocator size class 8\\)" a uint32 b [0]byte } @@ -29,14 +29,14 @@ z byte } -type NoNameBad struct { // want "struct of size 20 could be 16" +type NoNameBad struct { // want "NoNameBad has size 20 \\(allocator size class 24\\) but the optimal size is 16 leading to a waste of 8 bytes \\(33%\\)" Good x byte y int32 z byte } -type WithComments struct { // want "struct of size 8 could be 4" +type WithComments struct { // want "WithComments has size 8 but the optimal size is 4 \\(allocator size class 8\\)" // doc style comment a uint32 // field a comment b [0]byte // field b comment
diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a_386.go b/go/analysis/passes/fieldalignment/testdata/src/a/a_386.go index 13f9b9f..0f37578 100644 --- a/go/analysis/passes/fieldalignment/testdata/src/a/a_386.go +++ b/go/analysis/passes/fieldalignment/testdata/src/a/a_386.go
@@ -5,7 +5,7 @@ buf [1000]uintptr } -type PointerBad struct { // want "struct with 4004 pointer bytes could be 4" +type PointerBad struct { // want "PointerBad has 4004 leading bytes of pointer data but optimal value is 4" buf [1000]uintptr P *int } @@ -21,7 +21,7 @@ } } -type PointerSortaBad struct { // want "struct with 16 pointer bytes could be 12" +type PointerSortaBad struct { // want "PointerSortaBad has 16 leading bytes of pointer data but optimal value is 12" a struct { p *int q [2]uintptr @@ -32,7 +32,7 @@ } } -type MultiField struct { // want "struct of size 20 could be 12" +type MultiField struct { // want "MultiField has size 20 \\(allocator size class 24\\) but the optimal size is 12 \\(allocator size class 16\\) leading to a waste of 8 bytes \\(33%\\)" b bool i1, i2 int a3 [3]bool
diff --git a/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go b/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go index d6383b7..9a8ab9e 100644 --- a/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go +++ b/go/analysis/passes/fieldalignment/testdata/src/a/a_amd64.go
@@ -5,7 +5,7 @@ buf [1000]uintptr } -type PointerBad struct { // want "struct with 8008 pointer bytes could be 8" +type PointerBad struct { // want "PointerBad has 8008 leading bytes of pointer data but optimal value is 8" buf [1000]uintptr P *int } @@ -21,7 +21,7 @@ } } -type PointerSortaBad struct { // want "struct with 32 pointer bytes could be 24" +type PointerSortaBad struct { // want "PointerSortaBad has 32 leading bytes of pointer data but optimal value is 24" a struct { p *int q [2]uintptr @@ -32,14 +32,14 @@ } } -type MultiField struct { // want "struct of size 40 could be 24" +type MultiField struct { // want "MultiField has size 40 \\(allocator size class 48\\) but the optimal size is 24 leading to a waste of 24 bytes \\(50%\\)" b bool i1, i2 int a3 [3]bool _ [0]func() } -type Issue43233 struct { // want "struct with 88 pointer bytes could be 80" +type Issue43233 struct { // want "Issue43233 has 88 leading bytes of pointer data but optimal value is 80" AllowedEvents []*string // allowed events BlockedEvents []*string // blocked events APIVersion string `mapstructure:"api_version"`
diff --git a/go/analysis/passes/modernize/any.go b/go/analysis/passes/modernize/any.go index 579ab86..79e2802 100644 --- a/go/analysis/passes/modernize/any.go +++ b/go/analysis/passes/modernize/any.go
@@ -18,7 +18,7 @@ Doc: analyzerutil.MustExtractDoc(doc, "any"), Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: runAny, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_any", } // The any pass replaces interface{} with go1.18's 'any'.
diff --git a/go/analysis/passes/modernize/atomictypes.go b/go/analysis/passes/modernize/atomictypes.go index 9df39fb..6fd618f 100644 --- a/go/analysis/passes/modernize/atomictypes.go +++ b/go/analysis/passes/modernize/atomictypes.go
@@ -33,7 +33,7 @@ typeindexanalyzer.Analyzer, }, Run: runAtomic, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#atomictypes", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_atomictypes", } // TODO(mkalil): support the Pointer variants.
diff --git a/go/analysis/passes/modernize/bloop.go b/go/analysis/passes/modernize/bloop.go index ad45d74..d44c856 100644 --- a/go/analysis/passes/modernize/bloop.go +++ b/go/analysis/passes/modernize/bloop.go
@@ -32,7 +32,7 @@ typeindexanalyzer.Analyzer, }, Run: bloop, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#bloop", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_bloop", } // bloop updates benchmarks that use "for range b.N", replacing it
diff --git a/go/analysis/passes/modernize/doc.go b/go/analysis/passes/modernize/doc.go index c5545c0..caf3df9 100644 --- a/go/analysis/passes/modernize/doc.go +++ b/go/analysis/passes/modernize/doc.go
@@ -15,13 +15,16 @@ trivial to fix. We regard any modernizer whose fix changes program behavior to have a serious bug and will endeavor to fix it. -To apply all modernization fixes en masse, you can use the +Since Go 1.26, the 'go fix' command has included the modernize suite, +so to apply all modernization fixes en masse, you can use the following command: - $ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./... + $ go fix ./... -(Do not use "go get -tool" to add gopls as a dependency of your -module; gopls commands must be built from their release branch.) +If you need to run a modernizer added or modified since the Go +release, you can use this standalone command: + + $ go run golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize@latest -fix ./... If the tool warns of conflicting fixes, you may need to run it more than once until it has applied all fixes cleanly. This command is @@ -344,6 +347,21 @@ or when the operand has potential side effects. +# Analyzer reflecttypeassert + +reflecttypeassert: replace v.Interface().(T) with reflect.TypeAssert[T](v) + +This analyzer suggests fixes to replace two-valued type assertions on +the result of (reflect.Value).Interface with reflect.TypeAssert, +introduced in go1.25, which avoids the intermediate allocation of an +interface value, for example: + + x, ok := v.Interface().(string) -> x, ok := reflect.TypeAssert[string](v) + +No fix is offered for single-valued assertions, since they panic when +the assertion fails whereas reflect.TypeAssert does not. Nor is a fix +offered for a type switch. + # Analyzer slicesbackward slicesbackward: replace backward loops over slices with slices.Backward @@ -366,6 +384,22 @@ for i, v := range slices.Backward(s) { ... } +# Analyzer slicesclip + +slicesclip: replace three-index slice expressions with slices.Clip + +The slicesclip analyzer suggests replacing a full slice expression of +the form + + x[:len(x):len(x)] + +which clips the capacity of a slice to its length, with the simpler +and more readable + + slices.Clip(x) + +added in Go 1.21. + # Analyzer slicescontains slicescontains: replace loops with slices.Contains or slices.ContainsFunc @@ -422,7 +456,7 @@ iterator offered by the same data type: for elem := range x.All() { - use(x.At(i) + use(elem) } where x is one of various well-known types in the standard library.
diff --git a/go/analysis/passes/modernize/embedlit.go b/go/analysis/passes/modernize/embedlit.go index d2c71b5..a30276a 100644 --- a/go/analysis/passes/modernize/embedlit.go +++ b/go/analysis/passes/modernize/embedlit.go
@@ -33,7 +33,7 @@ typeindexanalyzer.Analyzer, }, Run: runEmbedLit, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#embedlit", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_embedlit", } // Go1.27 introduced the ability to directly access embedded struct fields. @@ -209,15 +209,25 @@ case edge.AssignStmt_Rhs: assign := curLit.Parent().Node().(*ast.AssignStmt) // TODO(mkalil): Handle lhs forms that aren't idents, i.e. x.y[i] = T{...}. - if id, ok := assign.Lhs[curLit.ParentEdgeIndex()].(*ast.Ident); ok { + // TODO(mkalil): Handle multi-assignments like t1, t2 := A{}, B{} + if len(assign.Lhs) != 1 { + return nil + } + if id, ok := assign.Lhs[0].(*ast.Ident); ok { lhs = id curStmt = curLit.Parent() } case edge.ValueSpec_Values: spec := curLit.Parent().Node().(*ast.ValueSpec) - lhs = spec.Names[curLit.ParentEdgeIndex()] + // TODO(mkalil): Handle multi-declarations like var (x = A{}; y = B{}) or var x, y = ... + if len(spec.Names) != 1 { + return nil + } + lhs = spec.Names[0] if decl, ok := moreiters.First(curLit.Enclosing((*ast.DeclStmt)(nil))); ok { - curStmt = decl + if gdecl, ok := decl.Node().(*ast.DeclStmt).Decl.(*ast.GenDecl); ok && len(gdecl.Specs) == 1 { + curStmt = decl + } } default: return nil @@ -231,7 +241,8 @@ tObj = info.ObjectOf(lhs) // Marks the contiguous block of embedded field assign statements that will // be moved into the struct initialization. - firstStmt, lastStmt inspector.Cursor + firstStmt, lastStmt inspector.Cursor + hasEmbeddedSelection bool ) stmtloop: for { @@ -262,6 +273,15 @@ if obj != tObj { break } + // The selection is from an embedded field if it directly + // assigns an embedded struct field (t.B = B{...}) or if + // the length of the index path is greater than one. + seln := info.Selections[sel] + if v, ok := seln.Obj().(*types.Var); ok && v.Embedded() || + len(seln.Index()) > 1 { + hasEmbeddedSelection = true + } + rhsCur := curStmt.ChildAt(edge.AssignStmt_Rhs, 0) if uses(index, rhsCur, tObj) { break @@ -284,7 +304,8 @@ lastStmt = curStmt } - if !firstStmt.Valid() { + if !firstStmt.Valid() || !hasEmbeddedSelection { + // We should not suggest a fix if none of the selections are from embedded fields. return nil }
diff --git a/go/analysis/passes/modernize/errorsastype.go b/go/analysis/passes/modernize/errorsastype.go index 0e3f17f..8a3f1c1 100644 --- a/go/analysis/passes/modernize/errorsastype.go +++ b/go/analysis/passes/modernize/errorsastype.go
@@ -27,7 +27,7 @@ var ErrorsAsTypeAnalyzer = &analysis.Analyzer{ Name: "errorsastype", Doc: analyzerutil.MustExtractDoc(doc, "errorsastype"), - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_errorsastype", Requires: []*analysis.Analyzer{typeindexanalyzer.Analyzer}, Run: errorsastype, } @@ -228,6 +228,9 @@ len(curDecl.Node().(*ast.GenDecl).Specs) != 1 { return // not a simple "var v T" decl } + if curDecl.ParentEdgeKind() != edge.DeclStmt_Decl { + return // package-level var, not a local declaration statement + } // AsType requires that its type argument implements error. // Reject if v does not implement error. if !types.AssignableTo(v.Type(), errorType) { @@ -239,5 +242,5 @@ // ... // if errors.As(err, &v) { ... } // with no uses of v outside the IfStmt. - return v, curDecl.Parent(), curIfStmt // curDecl.Parent() is a DeclStmt + return v, curDecl.Parent(), curIfStmt }
diff --git a/go/analysis/passes/modernize/export_test.go b/go/analysis/passes/modernize/export_test.go index 0401cf0..b873454 100644 --- a/go/analysis/passes/modernize/export_test.go +++ b/go/analysis/passes/modernize/export_test.go
@@ -7,7 +7,9 @@ package modernize var ( - ImportCommentAnalyzer = importCommentAnalyzer - SlicesBackwardAnalyzer = slicesBackwardAnalyzer - UnsafeFuncsAnalyzer = unsafeFuncsAnalyzer + ImportCommentAnalyzer = importCommentAnalyzer + ReflectTypeAssertAnalyzer = reflectTypeAssertAnalyzer + SlicesBackwardAnalyzer = slicesBackwardAnalyzer + SlicesClipAnalyzer = slicesClipAnalyzer + UnsafeFuncsAnalyzer = unsafeFuncsAnalyzer )
diff --git a/go/analysis/passes/modernize/fmtappendf.go b/go/analysis/passes/modernize/fmtappendf.go index 8210654..d67b6ad 100644 --- a/go/analysis/passes/modernize/fmtappendf.go +++ b/go/analysis/passes/modernize/fmtappendf.go
@@ -30,7 +30,7 @@ typeindexanalyzer.Analyzer, }, Run: fmtappendf, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#fmtappendf", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_fmtappendf", } // The fmtappend function replaces []byte(fmt.Sprintf(...)) by
diff --git a/go/analysis/passes/modernize/forvar.go b/go/analysis/passes/modernize/forvar.go index ba54dae..d79f1f9 100644 --- a/go/analysis/passes/modernize/forvar.go +++ b/go/analysis/passes/modernize/forvar.go
@@ -21,7 +21,7 @@ Doc: analyzerutil.MustExtractDoc(doc, "forvar"), Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: forvar, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#forvar", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_forvar", } // forvar offers to fix unnecessary copying of a for variable
diff --git a/go/analysis/passes/modernize/importcomment.go b/go/analysis/passes/modernize/importcomment.go index 1538783..de0f687 100644 --- a/go/analysis/passes/modernize/importcomment.go +++ b/go/analysis/passes/modernize/importcomment.go
@@ -14,7 +14,7 @@ var importCommentAnalyzer = &analysis.Analyzer{ Name: "importcomment", Doc: analyzerutil.MustExtractDoc(doc, "importcomment"), - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#importcomment", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_importcomment", Run: importcomment, }
diff --git a/go/analysis/passes/modernize/maps.go b/go/analysis/passes/modernize/maps.go index 7f3fd4e..38d0f23 100644 --- a/go/analysis/passes/modernize/maps.go +++ b/go/analysis/passes/modernize/maps.go
@@ -28,7 +28,7 @@ Doc: analyzerutil.MustExtractDoc(doc, "mapsloop"), Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: mapsloop, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_mapsloop", } // The mapsloop pass offers to simplify a loop of map insertions:
diff --git a/go/analysis/passes/modernize/minmax.go b/go/analysis/passes/modernize/minmax.go index 9fd8657..928fcf5 100644 --- a/go/analysis/passes/modernize/minmax.go +++ b/go/analysis/passes/modernize/minmax.go
@@ -32,7 +32,7 @@ typeindexanalyzer.Analyzer, }, Run: minmax, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_minmax", } // The minmax pass replaces if/else statements with calls to min or max, @@ -451,18 +451,3 @@ // Check if the sign matches the function name return cond(sign < 0, "min", "max") == funcName } - -// -- utils -- - -func is[T any](x any) bool { - _, ok := x.(T) - return ok -} - -func cond[T any](cond bool, t, f T) T { - if cond { - return t - } else { - return f - } -}
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go index 23dd8ab..9e950d7 100644 --- a/go/analysis/passes/modernize/modernize.go +++ b/go/analysis/passes/modernize/modernize.go
@@ -45,8 +45,10 @@ OmitZeroAnalyzer, PlusBuildAnalyzer, RangeIntAnalyzer, + reflectTypeAssertAnalyzer, // awaiting public symbol ReflectTypeForAnalyzer, slicesBackwardAnalyzer, // awaiting public symbol + slicesClipAnalyzer, // awaiting public symbol SlicesContainsAnalyzer, SlicesSortAnalyzer, StdIteratorsAnalyzer, @@ -122,7 +124,7 @@ // specified standard packages or their dependencies. func within(pass *analysis.Pass, pkgs ...string) bool { path := pass.Pkg.Path() - return packagepath.IsStdPackage(path) && + return packagepath.MaybeStdPackage(path) && moreiters.Contains(stdlib.Dependencies(pkgs...), path) } @@ -136,6 +138,7 @@ builtinMake = types.Universe.Lookup("make") builtinNew = types.Universe.Lookup("new") builtinNil = types.Universe.Lookup("nil") + builtinRecover = types.Universe.Lookup("recover") builtinString = types.Universe.Lookup("string") builtinTrue = types.Universe.Lookup("true") byteSliceType = types.NewSlice(types.Typ[types.Byte]) @@ -185,3 +188,16 @@ } return depth >= 4 } + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +} + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +}
diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go index bb2f7ac..37060dc 100644 --- a/go/analysis/passes/modernize/modernize_test.go +++ b/go/analysis/passes/modernize/modernize_test.go
@@ -99,11 +99,20 @@ RunWithSuggestedFixes(t, TestData(), modernize.ReflectTypeForAnalyzer, "reflecttypefor") } +func TestReflectTypeAssert(t *testing.T) { + testenv.NeedsGo1Point(t, 25) // reflect.TypeAssert requires go1.25 + RunWithSuggestedFixes(t, TestData(), modernize.ReflectTypeAssertAnalyzer, "reflecttypeassert") +} + func TestSlicesBackward(t *testing.T) { testenv.NeedsGo1Point(t, 23) RunWithSuggestedFixes(t, TestData(), modernize.SlicesBackwardAnalyzer, "slicesbackward") } +func TestSlicesClip(t *testing.T) { + RunWithSuggestedFixes(t, TestData(), modernize.SlicesClipAnalyzer, "slicesclip") +} + func TestSlicesContains(t *testing.T) { RunWithSuggestedFixes(t, TestData(), modernize.SlicesContainsAnalyzer, "slicescontains") } @@ -131,7 +140,7 @@ } func TestStringsSeq(t *testing.T) { - RunWithSuggestedFixes(t, TestData(), modernize.StringsSeqAnalyzer, "splitseq", "fieldsseq") + RunWithSuggestedFixes(t, TestData(), modernize.StringsSeqAnalyzer, "splitseq/...", "fieldsseq") } func TestTestingContext(t *testing.T) {
diff --git a/go/analysis/passes/modernize/newexpr.go b/go/analysis/passes/modernize/newexpr.go index 15d52d1..168c09f 100644 --- a/go/analysis/passes/modernize/newexpr.go +++ b/go/analysis/passes/modernize/newexpr.go
@@ -24,7 +24,7 @@ var NewExprAnalyzer = &analysis.Analyzer{ Name: "newexpr", Doc: analyzerutil.MustExtractDoc(doc, "newexpr"), - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#newexpr", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_newexpr", Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: run, FactTypes: []analysis.Fact{&newLike{}},
diff --git a/go/analysis/passes/modernize/omitzero.go b/go/analysis/passes/modernize/omitzero.go index 59ba950..4d93f7f 100644 --- a/go/analysis/passes/modernize/omitzero.go +++ b/go/analysis/passes/modernize/omitzero.go
@@ -24,7 +24,7 @@ Doc: analyzerutil.MustExtractDoc(doc, "omitzero"), Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: omitzero, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_omitzero", } // The omitzero pass searches for instances of "omitempty" in a json field tag on a
diff --git a/go/analysis/passes/modernize/plusbuild.go b/go/analysis/passes/modernize/plusbuild.go index 574ce0a..09877f8 100644 --- a/go/analysis/passes/modernize/plusbuild.go +++ b/go/analysis/passes/modernize/plusbuild.go
@@ -17,7 +17,7 @@ var PlusBuildAnalyzer = &analysis.Analyzer{ Name: "plusbuild", Doc: analyzerutil.MustExtractDoc(doc, "plusbuild"), - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#plusbuild", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_plusbuild", Run: plusbuild, }
diff --git a/go/analysis/passes/modernize/rangeint.go b/go/analysis/passes/modernize/rangeint.go index f7cb965..9cd7663 100644 --- a/go/analysis/passes/modernize/rangeint.go +++ b/go/analysis/passes/modernize/rangeint.go
@@ -13,8 +13,6 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/edge" - "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/analysis/analyzerutil" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" @@ -33,7 +31,7 @@ typeindexanalyzer.Analyzer, }, Run: rangeint, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_rangeint", } // rangeint offers a fix to replace a 3-clause 'for' loop: @@ -112,7 +110,7 @@ // limit is a local or unexported global var. // (An exported global may have uses we can't see.) for cur := range typeindex.Uses(v) { - if isScalarLvalue(info, cur) { + if typesinternal.IsAssignedOrAddressTaken(info, cur) { // Limit var is assigned or address-taken. continue nextLoop } @@ -161,7 +159,7 @@ // Reject if any is an l-value (assigned or address-taken): // a "for range int" loop does not respect assignments to // the loop variable. - if isScalarLvalue(info, curId) { + if typesinternal.IsAssignedOrAddressTaken(info, curId) { continue nextLoop } } @@ -345,40 +343,3 @@ } return nil, nil } - -// isScalarLvalue reports whether the specified identifier is -// address-taken or appears on the left side of an assignment. -// -// This function is valid only for scalars (x = ...), -// not for aggregates (x.a[i] = ...) -func isScalarLvalue(info *types.Info, curId inspector.Cursor) bool { - // Unfortunately we can't simply use info.Types[e].Assignable() - // as it is always true for a variable even when that variable is - // used only as an r-value. So we must inspect enclosing syntax. - - cur := astutil.UnparenEnclosingCursor(curId) - - switch cur.ParentEdgeKind() { - case edge.AssignStmt_Lhs: - assign := cur.Parent().Node().(*ast.AssignStmt) - if assign.Tok != token.DEFINE { - return true // i = j or i += j - } - id := curId.Node().(*ast.Ident) - if v, ok := info.Defs[id]; ok && v.Pos() != id.Pos() { - return true // reassignment of i (i, j := 1, 2) - } - case edge.RangeStmt_Key: - rng := cur.Parent().Node().(*ast.RangeStmt) - if rng.Tok == token.ASSIGN { - return true // "for k, v = range x" is like an AssignStmt to k, v - } - case edge.IncDecStmt_X: - return true // i++, i-- - case edge.UnaryExpr_X: - if cur.Parent().Node().(*ast.UnaryExpr).Op == token.AND { - return true // &i - } - } - return false -}
diff --git a/go/analysis/passes/modernize/reflect.go b/go/analysis/passes/modernize/reflect.go index 10fbdf8..14446a1 100644 --- a/go/analysis/passes/modernize/reflect.go +++ b/go/analysis/passes/modernize/reflect.go
@@ -30,7 +30,7 @@ typeindexanalyzer.Analyzer, }, Run: reflecttypefor, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypefor", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_reflecttypefor", } func reflecttypefor(pass *analysis.Pass) (any, error) {
diff --git a/go/analysis/passes/modernize/reflecttypeassert.go b/go/analysis/passes/modernize/reflecttypeassert.go new file mode 100644 index 0000000..ff41e43 --- /dev/null +++ b/go/analysis/passes/modernize/reflecttypeassert.go
@@ -0,0 +1,118 @@ +// Copyright 2026 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 modernize + +import ( + "go/ast" + "go/token" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/internal/analysis/analyzerutil" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" + "golang.org/x/tools/internal/versions" +) + +var reflectTypeAssertAnalyzer = &analysis.Analyzer{ + Name: "reflecttypeassert", + Doc: analyzerutil.MustExtractDoc(doc, "reflecttypeassert"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, + Run: reflecttypeassert, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_reflecttypeassert", +} + +func reflecttypeassert(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + + valueInterface = index.Selection("reflect", "Value", "Interface") + ) + + for curCall := range index.Calls(valueInterface) { + call := curCall.Node().(*ast.CallExpr) + // Have: v.Interface() + + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + continue // method expression reflect.Value.Interface(v) + } + + // TypeAssert's argument must be a reflect.Value; a pointer + // receiver would need an explicit dereference in the rewrite. + if !typesinternal.IsTypeNamed(info.TypeOf(sel.X), "reflect", "Value") { + continue + } + + // The call must be the operand of a type assertion + // (not a type switch, whose Type field is nil). + curOperand := astutil.UnparenEnclosingCursor(curCall) + if curOperand.ParentEdgeKind() != edge.TypeAssertExpr_X { + continue + } + curAssert := curOperand.Parent() + assert := curAssert.Node().(*ast.TypeAssertExpr) + if assert.Type == nil { + continue // type switch + } + + // The assertion must be the sole RHS of a two-valued + // assignment, x, ok := v.Interface().(T), so that the + // rewrite preserves the "commaOK" semantics; a single-valued + // assertion panics on failure whereas TypeAssert does not. + curRhs := astutil.UnparenEnclosingCursor(curAssert) + if curRhs.ParentEdgeKind() != edge.AssignStmt_Rhs { + continue + } + assign := curRhs.Parent().Node().(*ast.AssignStmt) + if len(assign.Lhs) != 2 || len(assign.Rhs) != 1 || + (assign.Tok != token.ASSIGN && assign.Tok != token.DEFINE) { + continue + } + + file := astutil.EnclosingFile(curCall) + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_25) { + continue // TypeAssert requires go1.25 + } + + prefix, importEdits := refactor.AddImport(info, file, "reflect", "reflect", "TypeAssert", assert.Pos()) + + tstr := astutil.Format(pass.Fset, assert.Type) + pass.Report(analysis.Diagnostic{ + Pos: assert.Pos(), + End: assert.End(), + Message: "Interface().(" + tstr + ") can be simplified using reflect.TypeAssert", + SuggestedFixes: []analysis.SuggestedFix{{ + // v.Interface().(T) -> reflect.TypeAssert[T](v) + Message: "Replace Interface().(" + tstr + ") by reflect.TypeAssert[" + tstr + "]", + // Edit around sel.X instead of reformatting it, so its + // comments and spacing are preserved; only the type, + // which must move, is reformatted. + TextEdits: append(importEdits, + analysis.TextEdit{ + Pos: assert.Pos(), + End: sel.X.Pos(), + NewText: []byte(prefix + "TypeAssert[" + tstr + "]("), + }, + analysis.TextEdit{ + Pos: sel.X.End(), + End: assert.End(), + NewText: []byte(")"), + }, + ), + }}, + }) + } + + return nil, nil +}
diff --git a/go/analysis/passes/modernize/slices.go b/go/analysis/passes/modernize/slices.go index 6c8ea22..4fb40a8 100644 --- a/go/analysis/passes/modernize/slices.go +++ b/go/analysis/passes/modernize/slices.go
@@ -27,7 +27,7 @@ Doc: analyzerutil.MustExtractDoc(doc, "appendclipped"), Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: appendclipped, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#appendclipped", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_appendclipped", } // The appendclipped pass offers to simplify a tower of append calls:
diff --git a/go/analysis/passes/modernize/slicesbackward.go b/go/analysis/passes/modernize/slicesbackward.go index 02cd30a..c65a6c7 100644 --- a/go/analysis/passes/modernize/slicesbackward.go +++ b/go/analysis/passes/modernize/slicesbackward.go
@@ -19,6 +19,7 @@ typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/versions" ) @@ -32,7 +33,7 @@ typeindexanalyzer.Analyzer, }, Run: slicesbackward, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesbackward", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesbackward", } // slicesbackward offers a fix to replace a manually-written backward loop: @@ -128,7 +129,7 @@ // (e.g. &i before the loop). bodyCur := curLoop.Child(loop.Body) for curUse := range index.Uses(indexObj) { - if !isScalarLvalue(info, curUse) { + if !typesinternal.IsAssignedOrAddressTaken(info, curUse) { continue } if bodyCur.Contains(curUse) { @@ -160,20 +161,25 @@ // If so, we also need to check whether s[i] is an lvalue. If we're // mutating the slice or taking an element's address, a fix will not // be offered. + // Modernization to "for _, v := range slices.Backward(s)" is unsafe if + // s[i] is mutated or address-taken (since v would be a local copy of + // the element so s[i] wouldn't get mutated). + // We don't need to worry about indirect selections (e.g. s[i].n++ where + // s is []*item) or indirect references like indexing a slice of slices. if curUse.ParentEdgeKind() == edge.IndexExpr_Index { - if isScalarLvalue(pass.TypesInfo, curUse.Parent()) { + curIdx := curUse.Parent() + if typesinternal.IsAssignedOrAddressTaken(info, curIdx) { continue nextLoop } - idxCur := curUse.Parent() - idxExpr := idxCur.Node().(*ast.IndexExpr) + idxExpr := curIdx.Node().(*ast.IndexExpr) if astutil.EqualSyntax(idxExpr.X, sliceExpr) { sliceIdxs++ // If the current statement is the first in the body of the form // "name := s[i]", save it so we can use "name" as the value // variable in slices.Backward. We can also remove the entire assign // statement. - if firstSliceIdxAssign == nil && idxCur.ParentEdgeKind() == edge.AssignStmt_Rhs { - assignStmt := idxCur.Parent().Node().(*ast.AssignStmt) + if firstSliceIdxAssign == nil && curIdx.ParentEdgeKind() == edge.AssignStmt_Rhs { + assignStmt := curIdx.Parent().Node().(*ast.AssignStmt) if len(assignStmt.Lhs) == 1 && assignStmt.Tok == token.DEFINE { // The condition above implies that assignStmt.Lhs[0] is a valid // identifier.
diff --git a/go/analysis/passes/modernize/slicesclip.go b/go/analysis/passes/modernize/slicesclip.go new file mode 100644 index 0000000..b08cbc6 --- /dev/null +++ b/go/analysis/passes/modernize/slicesclip.go
@@ -0,0 +1,81 @@ +// Copyright 2026 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 modernize + +import ( + "fmt" + "go/ast" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" +) + +var slicesClipAnalyzer = &analysis.Analyzer{ + Name: "slicesclip", + Doc: analyzerutil.MustExtractDoc(doc, "slicesclip"), + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + }, + Run: slicesclip, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesclip", +} + +func slicesclip(pass *analysis.Pass) (any, error) { + if within(pass, "slices", "runtime") { + return nil, nil + } + info := pass.TypesInfo + + // isLenX reports whether e is a call len(x) where x is + // syntactically identical to the operand x of the slice expr. + isLenX := func(e, x ast.Expr) bool { + call, ok := e.(*ast.CallExpr) + if !ok || len(call.Args) != 1 { + return false + } + return typeutil.Callee(info, call) == builtinLen && + astutil.EqualSyntax(call.Args[0], x) + } + + for curFile := range filesUsingGoVersion(pass, versions.Go1_21) { + file := curFile.Node().(*ast.File) + + for curSlice := range curFile.Preorder((*ast.SliceExpr)(nil)) { + slice := curSlice.Node().(*ast.SliceExpr) + _, ok := info.TypeOf(slice.X).Underlying().(*types.Slice) // in case x is an array/pointer to array + if !slice.Slice3 || slice.Low != nil || !ok { + continue + } + + if isLenX(slice.High, slice.X) && isLenX(slice.Max, slice.X) && typesinternal.NoEffects(info, slice.X) { + // Have x[:len(x):len(x)] -> slices.Clip(x) + prefix, edits := refactor.AddImport(info, file, "slices", "slices", "Clip", slice.Pos()) + sx := astutil.Format(pass.Fset, slice.X) + pass.Report(analysis.Diagnostic{ + Pos: slice.Pos(), + End: slice.End(), + Message: "x[:len(x):len(x)] can be simplified using slices.Clip", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Replace with slices.Clip(%s)", sx), + TextEdits: append(edits, analysis.TextEdit{ + Pos: slice.Pos(), + End: slice.End(), + NewText: fmt.Appendf(nil, "%sClip(%s)", prefix, sx), + }), + }}, + }) + } + } + } + + return nil, nil +}
diff --git a/go/analysis/passes/modernize/slicescontains.go b/go/analysis/passes/modernize/slicescontains.go index ed75e05..b27be6f 100644 --- a/go/analysis/passes/modernize/slicescontains.go +++ b/go/analysis/passes/modernize/slicescontains.go
@@ -32,7 +32,7 @@ typeindexanalyzer.Analyzer, }, Run: slicescontains, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicescontains", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicescontains", } // The slicescontains pass identifies loops that can be replaced by a
diff --git a/go/analysis/passes/modernize/slicesdelete.go b/go/analysis/passes/modernize/slicesdelete.go index 7b3aa87..c623aed 100644 --- a/go/analysis/passes/modernize/slicesdelete.go +++ b/go/analysis/passes/modernize/slicesdelete.go
@@ -25,7 +25,7 @@ Doc: analyzerutil.MustExtractDoc(doc, "slicesdelete"), Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: slicesdelete, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesdelete", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesdelete", } // The slicesdelete pass attempts to replace instances of append(s[:i], s[i+k:]...)
diff --git a/go/analysis/passes/modernize/sortslice.go b/go/analysis/passes/modernize/sortslice.go index e22b8c5..08d8667 100644 --- a/go/analysis/passes/modernize/sortslice.go +++ b/go/analysis/passes/modernize/sortslice.go
@@ -28,7 +28,7 @@ typeindexanalyzer.Analyzer, }, Run: slicessort, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicessort", } // The slicessort pass replaces sort.Slice(slice, less) with
diff --git a/go/analysis/passes/modernize/stditerators.go b/go/analysis/passes/modernize/stditerators.go index 1953268..2520886 100644 --- a/go/analysis/passes/modernize/stditerators.go +++ b/go/analysis/passes/modernize/stditerators.go
@@ -28,7 +28,7 @@ typeindexanalyzer.Analyzer, }, Run: stditerators, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stditerators", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stditerators", } // stditeratorsTable records std types that have legacy T.{Len,At}
diff --git a/go/analysis/passes/modernize/stringsbuilder.go b/go/analysis/passes/modernize/stringsbuilder.go index 6aa9c88..ca1cc14 100644 --- a/go/analysis/passes/modernize/stringsbuilder.go +++ b/go/analysis/passes/modernize/stringsbuilder.go
@@ -34,7 +34,7 @@ typeindexanalyzer.Analyzer, }, Run: stringsbuilder, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringbuilder", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringsbuilder", } // stringsbuilder replaces string += string in a loop by strings.Builder.
diff --git a/go/analysis/passes/modernize/stringscut.go b/go/analysis/passes/modernize/stringscut.go index ae93d4d..daa6a67 100644 --- a/go/analysis/passes/modernize/stringscut.go +++ b/go/analysis/passes/modernize/stringscut.go
@@ -35,7 +35,7 @@ typeindexanalyzer.Analyzer, }, Run: stringscut, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscut", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringscut", } // stringscut offers a fix to replace an occurrence of strings.Index{,Byte} with @@ -513,9 +513,12 @@ info.Types[expr.Fun].IsType() && // make sure this isn't a function that returns a byte slice indexArgValid(info, index, expr.Args[0], afterPos) // check s in []byte(s) case *ast.Ident: - sObj := info.Uses[expr] - sUses := index.Uses(sObj) - return !hasModifyingUses(sUses, afterPos) + for use := range index.Uses(info.Uses[expr]) { + if typesinternal.IsAssignedOrAddressTaken(info, use) { + return false + } + } + return true default: // For now, skip instances where s or substr are not // identifiers, basic lits, or call expressions of the form @@ -612,30 +615,6 @@ return negative, nonnegative, beforeSlice, afterSlice } -// hasModifyingUses reports whether any of the uses involve potential -// modifications. Uses involving assignments before the "afterPos" won't be -// considered. -func hasModifyingUses(uses iter.Seq[inspector.Cursor], afterPos token.Pos) bool { - for curUse := range uses { - ek := curUse.ParentEdgeKind() - if ek == edge.AssignStmt_Lhs { - if curUse.Node().Pos() <= afterPos { - continue - } - // Any use on the LHS is a modifying use. - return true - } else if ek == edge.UnaryExpr_X && - curUse.Parent().Node().(*ast.UnaryExpr).Op == token.AND { - // Modifying use because we might be passing the object by reference (an explicit &). - // We can ignore the case where we have a method call on the expression (which - // has an implicit &) because we know the type of s and substr are strings - // which cannot have methods on them. - return true - } - } - return false -} - // checkIdxComparison reports whether the check is equivalent to i < 0 or its negation, or neither. // For equivalent to i >= 0, we only accept this exact BinaryExpr since // expressions like i > 0 or i >= 1 make a stronger statement about the value of i.
diff --git a/go/analysis/passes/modernize/stringscutprefix.go b/go/analysis/passes/modernize/stringscutprefix.go index 11d3359..6e28545 100644 --- a/go/analysis/passes/modernize/stringscutprefix.go +++ b/go/analysis/passes/modernize/stringscutprefix.go
@@ -31,7 +31,7 @@ typeindexanalyzer.Analyzer, }, Run: stringscutprefix, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscutprefix", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringscutprefix", } // stringscutprefix offers a fix to replace an if statement which
diff --git a/go/analysis/passes/modernize/stringsseq.go b/go/analysis/passes/modernize/stringsseq.go index d02a532..064444d 100644 --- a/go/analysis/passes/modernize/stringsseq.go +++ b/go/analysis/passes/modernize/stringsseq.go
@@ -28,7 +28,7 @@ typeindexanalyzer.Analyzer, }, Run: stringsseq, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringsseq", } // stringsseq offers a fix to replace a call to strings.Split with @@ -117,6 +117,8 @@ } switch obj := typeutil.Callee(info, call); obj { + case nil: + // a conversion, not a call case stringsSplit, stringsFields, bytesSplit, bytesFields: oldFnName := obj.Name() seqFnName := fmt.Sprintf("%sSeq", oldFnName)
diff --git a/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go index 39300a1..6c9ac33 100644 --- a/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go +++ b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go
@@ -63,6 +63,11 @@ type U struct { x int + V +} + +type V struct { + x, y int } var ( @@ -207,6 +212,32 @@ a: 1, // comment, with a comma } t16.b = 2 + + assgn1, assgn2 := A{}, A{} // nope, multi-assign + assgn1.e = 1 + assgn2.e = 1 + + var ( + v = A{} // nope, multi-declaration + othervar = 2 + ) + v.e = 1 + _ = othervar + + var v1, v2 = A{}, A{} // nope, multi-declaration + v2.e = 1 + _ = v1 + + t17 := U{} + t17.x = 1 // nope: not embedded + + t18 := V{} + t18.x = 1 + t18.y = 2 // nope: not embedded + + t19 := U{} // want "embedded field assignment can be moved to struct literal" + t19.y = 2 + t19.x = 1 } func foo() int {
diff --git a/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go.golden b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go.golden index 69fe4ab..48f0593 100644 --- a/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go.golden +++ b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go.golden
@@ -63,6 +63,11 @@ type U struct { x int + V +} + +type V struct { + x, y int } var ( @@ -185,7 +190,7 @@ a: 1, b: foo() + 1} - + t15 := A{a: 1} // nope: += in field assignment t15.b += 2 @@ -193,6 +198,32 @@ a: 1, // comment, with a comma b: 2} + + assgn1, assgn2 := A{}, A{} // nope, multi-assign + assgn1.e = 1 + assgn2.e = 1 + + var ( + v = A{} // nope, multi-declaration + othervar = 2 + ) + v.e = 1 + _ = othervar + + var v1, v2 = A{}, A{} // nope, multi-declaration + v2.e = 1 + _ = v1 + + t17 := U{} + t17.x = 1 // nope: not embedded + + t18 := V{} + t18.x = 1 + t18.y = 2 // nope: not embedded + + t19 := U{ // want "embedded field assignment can be moved to struct literal" + y: 2, + x: 1} } func foo() int {
diff --git a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go index 2e840b7..a7aa2a5 100644 --- a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go +++ b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go
@@ -5,7 +5,13 @@ "os" ) +var packagePathErr *os.PathError + func _(err error) { + if errors.As(err, &packagePathErr) { // nope: packagePathErr is not declared by a local statement + print(packagePathErr) + } + { var patherr *os.PathError if errors.As(err, &patherr) { // want `errors.As can be simplified using AsType\[\*os.PathError\]`
diff --git a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden index 35a1728..9628fe8 100644 --- a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden +++ b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden
@@ -5,7 +5,13 @@ "os" ) +var packagePathErr *os.PathError + func _(err error) { + if errors.As(err, &packagePathErr) { // nope: packagePathErr is not declared by a local statement + print(packagePathErr) + } + { if patherr, ok := errors.AsType[*os.PathError](err); ok { // want `errors.As can be simplified using AsType\[\*os.PathError\]` print(patherr)
diff --git a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go index d3ea1e9..4f41813 100644 --- a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go +++ b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go
@@ -317,6 +317,14 @@ } } +func issue77034_value(slice []int) { + for i := 0; i < 5; i++ { // nope: inner loop modifies i + for _, i = range slice { + } + } +} + + func issue77034_define_inner() { for i := 0; i < 5; i++ { // want "for loop can be modernized using range over int" for i := range 10 { // inner "i" doesn't modify outer "i"
diff --git a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden index f85e986..7cc05a9 100644 --- a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden +++ b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden
@@ -316,6 +316,14 @@ } } +func issue77034_value(slice []int) { + for i := 0; i < 5; i++ { // nope: inner loop modifies i + for _, i = range slice { + } + } +} + + func issue77034_define_inner() { for range 5 { // want "for loop can be modernized using range over int" for i := range 10 { // inner "i" doesn't modify outer "i"
diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go b/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go new file mode 100644 index 0000000..b7ad8a3 --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go
@@ -0,0 +1,59 @@ +package reflecttypeassert + +import ( + "io" + "reflect" +) + +type payload struct{ n int } + +func twoValued(v reflect.Value) { + x, ok := v.Interface().(string) // want "Interface\\(\\)\\.\\(string\\) can be simplified using reflect.TypeAssert" + _, _ = x, ok + + p, ok := v.Interface().(payload) // want "Interface\\(\\)\\.\\(payload\\) can be simplified using reflect.TypeAssert" + _, _ = p, ok + + r, ok := v.Interface().(io.Reader) // want "Interface\\(\\)\\.\\(io.Reader\\) can be simplified using reflect.TypeAssert" + _, _ = r, ok +} + +func assignment(v reflect.Value) { + var y int + var ok bool + y, ok = v.Interface().(int) // want "Interface\\(\\)\\.\\(int\\) can be simplified using reflect.TypeAssert" + _, _ = y, ok +} + +func inIfInit(v reflect.Value) { + if s, ok := v.Interface().(string); ok { // want "Interface\\(\\)\\.\\(string\\) can be simplified using reflect.TypeAssert" + _ = s + } +} + +func receiverExpr(vs []reflect.Value) { + e, ok := vs[0].Interface().(error) // want "Interface\\(\\)\\.\\(error\\) can be simplified using reflect.TypeAssert" + _, _ = e, ok +} + +func nomatch(v reflect.Value, pv *reflect.Value, any1 any) { + // Single-valued assertion panics on failure; TypeAssert doesn't. + s := v.Interface().(string) + _ = s + + // Not a type assertion on Value.Interface. + i, ok := any1.(int) + _, _ = i, ok + + // Type switches have no TypeAssert equivalent. + switch v.Interface().(type) { + case string: + } + + // Pointer receiver would need an explicit dereference; leave it alone. + ps, ok := pv.Interface().(string) + _, _ = ps, ok + + // Interface method value invocation, not part of an assignment. + _ = v.Interface() +}
diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go.golden b/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go.golden new file mode 100644 index 0000000..6d53437 --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/reflecttypeassert/reflecttypeassert.go.golden
@@ -0,0 +1,59 @@ +package reflecttypeassert + +import ( + "io" + "reflect" +) + +type payload struct{ n int } + +func twoValued(v reflect.Value) { + x, ok := reflect.TypeAssert[string](v) // want "Interface\\(\\)\\.\\(string\\) can be simplified using reflect.TypeAssert" + _, _ = x, ok + + p, ok := reflect.TypeAssert[payload](v) // want "Interface\\(\\)\\.\\(payload\\) can be simplified using reflect.TypeAssert" + _, _ = p, ok + + r, ok := reflect.TypeAssert[io.Reader](v) // want "Interface\\(\\)\\.\\(io.Reader\\) can be simplified using reflect.TypeAssert" + _, _ = r, ok +} + +func assignment(v reflect.Value) { + var y int + var ok bool + y, ok = reflect.TypeAssert[int](v) // want "Interface\\(\\)\\.\\(int\\) can be simplified using reflect.TypeAssert" + _, _ = y, ok +} + +func inIfInit(v reflect.Value) { + if s, ok := reflect.TypeAssert[string](v); ok { // want "Interface\\(\\)\\.\\(string\\) can be simplified using reflect.TypeAssert" + _ = s + } +} + +func receiverExpr(vs []reflect.Value) { + e, ok := reflect.TypeAssert[error](vs[0]) // want "Interface\\(\\)\\.\\(error\\) can be simplified using reflect.TypeAssert" + _, _ = e, ok +} + +func nomatch(v reflect.Value, pv *reflect.Value, any1 any) { + // Single-valued assertion panics on failure; TypeAssert doesn't. + s := v.Interface().(string) + _ = s + + // Not a type assertion on Value.Interface. + i, ok := any1.(int) + _, _ = i, ok + + // Type switches have no TypeAssert equivalent. + switch v.Interface().(type) { + case string: + } + + // Pointer receiver would need an explicit dereference; leave it alone. + ps, ok := pv.Interface().(string) + _, _ = ps, ok + + // Interface method value invocation, not part of an assignment. + _ = v.Interface() +}
diff --git a/go/analysis/passes/modernize/testdata/src/slicesbackward/slicesbackward.go b/go/analysis/passes/modernize/testdata/src/slicesbackward/slicesbackward.go index fe3fc25..9e026fe 100644 --- a/go/analysis/passes/modernize/testdata/src/slicesbackward/slicesbackward.go +++ b/go/analysis/passes/modernize/testdata/src/slicesbackward/slicesbackward.go
@@ -4,7 +4,10 @@ package slicesbackward -import "slices" +import ( + "slices" + "sync" +) var _ = slices.Backward[[]int] // force import of "slices" to avoid duplicate import edits @@ -157,3 +160,117 @@ _ = &s[i] } } + +type item struct { + n int +} + +// Should NOT fire: field mutation +func indexExprMutated(s []item) { + for i := len(s) - 1; i >= 0; i-- { + s[i].n++ + } +} + +type outer struct { + inner item +} + +// Should NOT fire: nested field mutation +func indexExprNestedField(s []outer) { + for i := len(s) - 1; i >= 0; i-- { + s[i].inner.n++ + } +} + +// Should NOT fire: field assignment +func indexExprFieldAssign(s []item) { + for i := len(s) - 1; i >= 0; i-- { + s[i].n = 5 + } +} + +// Should NOT fire: multi-field assignment +func indexExprMultiAssign(s []int) { + for i := len(s) - 2; i >= 0; i-- { + s[i], s[i+1] = 1, 2 + } +} + +// Should NOT fire: parenthesized assignment +func indexExprParenthesizedAssign(s []int) { + for i := len(s) - 1; i >= 0; i-- { + (s[i]) = 5 + } +} + +// Should NOT fire: address-taken +func indexExprFieldAddr(s []item) { + for i := len(s) - 1; i >= 0; i-- { + _ = &s[i].n + } +} + +// Should NOT fire: method call with pointer receiver may mutate element +func indexExprMethodCall(s []sync.Mutex) { + for i := len(s) - 1; i >= 0; i-- { + s[i].Lock() + s[i].Unlock() + } +} + +type stringer struct{} + +func (stringer) String() string { return "" } + +func (*stringer) StringPtr() string { return "" } + +// SHOULD fire: method call with value receiver does not mutate element +func indexExprValueReceiver(s []stringer) { + for i := len(s) - 1; i >= 0; i-- { // want "backward loop over slice can be modernized using slices.Backward" + _ = s[i].String() + } +} + +// SHOULD fire: method call with pointer receiver type and pointer receiver value does not mutate +func indexExprPtrValueReceiver(s []*stringer) { + for i := len(s) - 1; i >= 0; i-- { // want "backward loop over slice can be modernized using slices.Backward" + _ = s[i].StringPtr() + } +} + +// Should NOT fire: range assignment mutating slice elements +func indexExprRangeAssign(s []int, x []int) { + for i := len(s) - 1; i >= 0; i-- { + for s[i] = range x { + } + } +} + +// SHOULD fire: slice of slices is indirect reference +func indexExprSliceOfSlices(s [][]int) { + for i := len(s) - 1; i >= 0; i-- { // want "backward loop over slice can be modernized using slices.Backward" + s[i][0]++ + } +} + +// SHOULD fire: slice of pointers is indirect reference +func indexExprPointerSlice(s []*item) { + for i := len(s) - 1; i >= 0; i-- { // want "backward loop over slice can be modernized using slices.Backward" + s[i].n++ + } +} + +// Should NOT fire: slice of arrays, mutating array element is direct mutation +func indexExprSliceOfArrays(s [][3]int) { + for i := len(s) - 1; i >= 0; i-- { + s[i][0]++ + } +} + +// SHOULD fire: index expression used as index of another slice which is mutated +func indexExprUsedAsIndex(s []int) { + for i := len(s) - 1; i >= 0; i-- { // want "backward loop over slice can be modernized using slices.Backward" + s[s[i]] = 5 + } +}
diff --git a/go/analysis/passes/modernize/testdata/src/slicesbackward/slicesbackward.go.golden b/go/analysis/passes/modernize/testdata/src/slicesbackward/slicesbackward.go.golden index e3fb892..1db10b1 100644 --- a/go/analysis/passes/modernize/testdata/src/slicesbackward/slicesbackward.go.golden +++ b/go/analysis/passes/modernize/testdata/src/slicesbackward/slicesbackward.go.golden
@@ -4,7 +4,10 @@ package slicesbackward -import "slices" +import ( + "slices" + "sync" +) var _ = slices.Backward[[]int] // force import of "slices" to avoid duplicate import edits @@ -157,3 +160,118 @@ _ = &s[i] } } + +type item struct { + n int +} + +// Should NOT fire: field mutation +func indexExprMutated(s []item) { + for i := len(s) - 1; i >= 0; i-- { + s[i].n++ + } +} + +type outer struct { + inner item +} + +// Should NOT fire: nested field mutation +func indexExprNestedField(s []outer) { + for i := len(s) - 1; i >= 0; i-- { + s[i].inner.n++ + } +} + +// Should NOT fire: field assignment +func indexExprFieldAssign(s []item) { + for i := len(s) - 1; i >= 0; i-- { + s[i].n = 5 + } +} + +// Should NOT fire: multi-field assignment +func indexExprMultiAssign(s []int) { + for i := len(s) - 2; i >= 0; i-- { + s[i], s[i+1] = 1, 2 + } +} + +// Should NOT fire: parenthesized assignment +func indexExprParenthesizedAssign(s []int) { + for i := len(s) - 1; i >= 0; i-- { + (s[i]) = 5 + } +} + +// Should NOT fire: address-taken +func indexExprFieldAddr(s []item) { + for i := len(s) - 1; i >= 0; i-- { + _ = &s[i].n + } +} + +// Should NOT fire: method call with pointer receiver may mutate element +func indexExprMethodCall(s []sync.Mutex) { + for i := len(s) - 1; i >= 0; i-- { + s[i].Lock() + s[i].Unlock() + } +} + +type stringer struct{} + +func (stringer) String() string { return "" } + +func (*stringer) StringPtr() string { return "" } + +// SHOULD fire: method call with value receiver does not mutate element +func indexExprValueReceiver(s []stringer) { + for _, v := range slices.Backward(s) { // want "backward loop over slice can be modernized using slices.Backward" + _ = v.String() + } +} + +// SHOULD fire: method call with pointer receiver type and pointer receiver value does not mutate +func indexExprPtrValueReceiver(s []*stringer) { + for _, v := range slices.Backward(s) { // want "backward loop over slice can be modernized using slices.Backward" + _ = v.StringPtr() + } +} + +// Should NOT fire: range assignment mutating slice elements +func indexExprRangeAssign(s []int, x []int) { + for i := len(s) - 1; i >= 0; i-- { + for s[i] = range x { + } + } +} + +// SHOULD fire: slice of slices is indirect reference +func indexExprSliceOfSlices(s [][]int) { + for _, v := range slices.Backward(s) { // want "backward loop over slice can be modernized using slices.Backward" + v[0]++ + } +} + +// SHOULD fire: slice of pointers is indirect reference +func indexExprPointerSlice(s []*item) { + for _, v := range slices.Backward(s) { // want "backward loop over slice can be modernized using slices.Backward" + v.n++ + } +} + +// Should NOT fire: slice of arrays, mutating array element is direct mutation +func indexExprSliceOfArrays(s [][3]int) { + for i := len(s) - 1; i >= 0; i-- { + s[i][0]++ + } +} + +// SHOULD fire: index expression used as index of another slice which is mutated +func indexExprUsedAsIndex(s []int) { + for _, v := range slices.Backward(s) { // want "backward loop over slice can be modernized using slices.Backward" + s[v] = 5 + } +} +
diff --git a/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go b/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go new file mode 100644 index 0000000..845a831 --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go
@@ -0,0 +1,46 @@ +package slicesclip + +var g struct{ f []int } + +func h() []int { return []int{} } + +var ch chan []int + +func _(test, other []byte, i int) { + _ = test[:len(test):len(test)] // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = test[1:len(test):len(test)] // non-zero low index: no match + + _ = test[:len(test)] // ordinary two-index slice: no match + + _ = test[:len(other):len(other)] // different slice variable: no match + + _ = test[:len(test):len(other)] // mismatched high/max: no match + + _ = g.f[:len(g.f):len(g.f)] // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = h()[:len(h()):len(h())] // potentially has side effects: no match + + _ = (<-ch)[:len(<-ch):len(<-ch)] // has side effects: no match + + if len(test) > 0 { + test = test[:len(test):len(test)] // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + } + + _ = append(other, test[:len(test):len(test)]...) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = i +} + +func shadowed(test []byte) { + len := func(_ []byte) int { return 0 } + _ = test[:len(test):len(test)] // len is shadowed: no match +} + +func arrayCase() { + var a [3]int + _ = a[:len(a):len(a)] // array, not slice: no match + + pa := &a + _ = pa[:len(pa):len(pa)] // pointer to array, not slice: no match +}
diff --git a/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go.golden b/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go.golden new file mode 100644 index 0000000..15d5b45 --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/slicesclip/slicesclip.go.golden
@@ -0,0 +1,48 @@ +package slicesclip + +import "slices" + +var g struct{ f []int } + +func h() []int { return []int{} } + +var ch chan []int + +func _(test, other []byte, i int) { + _ = slices.Clip(test) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = test[1:len(test):len(test)] // non-zero low index: no match + + _ = test[:len(test)] // ordinary two-index slice: no match + + _ = test[:len(other):len(other)] // different slice variable: no match + + _ = test[:len(test):len(other)] // mismatched high/max: no match + + _ = slices.Clip(g.f) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = h()[:len(h()):len(h())] // potentially has side effects: no match + + _ = (<-ch)[:len(<-ch):len(<-ch)] // has side effects: no match + + if len(test) > 0 { + test = slices.Clip(test) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + } + + _ = append(other, slices.Clip(test)...) // want `x\[:len\(x\):len\(x\)\] can be simplified using slices\.Clip` + + _ = i +} + +func shadowed(test []byte) { + len := func(_ []byte) int { return 0 } + _ = test[:len(test):len(test)] // len is shadowed: no match +} + +func arrayCase() { + var a [3]int + _ = a[:len(a):len(a)] // array, not slice: no match + + pa := &a + _ = pa[:len(pa):len(pa)] // pointer to array, not slice: no match +}
diff --git a/go/analysis/passes/modernize/testdata/src/splitseq/conv/nobytes.go b/go/analysis/passes/modernize/testdata/src/splitseq/conv/nobytes.go new file mode 100644 index 0000000..b77ca1a --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/splitseq/conv/nobytes.go
@@ -0,0 +1,14 @@ +package conv + +import ( + "net" + "strings" +) + +func _(s string) net.IP { + var result net.IP + for _, b := range net.IP(strings.Split(s, ",")[0]) { // nope: cannot modernize with the conversion + result = append(result, b) + } + return result +}
diff --git a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go index 2e879c3..472b2f8 100644 --- a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go +++ b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go
@@ -5,8 +5,8 @@ "strings" ) -func basic(s string) bool { - s = "reassigned" +func basic() bool { + s := "value" i := strings.Index(s, "=") // want "strings.Index can be simplified using strings.Cut" if i >= 0 { print(s[:i]) @@ -14,8 +14,72 @@ return i >= 0 } -func basic_contains(s string) bool { - s = "reassigned" +func basic_reassigned1() bool { + s := "value" + i := strings.Index(s, "=") + s = "foo" + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned2() bool { + s := "value" + i := strings.Index(s, "=") + _ = &(s) + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned3() bool { + s := "value" + i := strings.Index(s, "=") + (s) = "foo" + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned4() bool { + s := "value" + i := strings.Index(s, "=") + _ = &(s) + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned5() bool { + s := "value" + foo := func() { + s = "foo" + } + i := strings.Index(s, "=") + foo() + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned6() bool { + s := "value" + i := strings.Index(s, "=") + for s = range map[string]struct{}{} { + } + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_contains() bool { + s := "value" i := strings.Index(s, "=") // want "strings.Index can be simplified using strings.Contains" return i >= 0 }
diff --git a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden index cba96f7..9dd053e 100644 --- a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden +++ b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden
@@ -5,8 +5,8 @@ "strings" ) -func basic(s string) bool { - s = "reassigned" +func basic() bool { + s := "value" before, _, ok := strings.Cut(s, "=") // want "strings.Index can be simplified using strings.Cut" if ok { print(before) @@ -14,8 +14,72 @@ return ok } -func basic_contains(s string) bool { - s = "reassigned" +func basic_reassigned1() bool { + s := "value" + i := strings.Index(s, "=") + s = "foo" + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned2() bool { + s := "value" + i := strings.Index(s, "=") + _ = &(s) + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned3() bool { + s := "value" + i := strings.Index(s, "=") + (s) = "foo" + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned4() bool { + s := "value" + i := strings.Index(s, "=") + _ = &(s) + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned5() bool { + s := "value" + foo := func() { + s = "foo" + } + i := strings.Index(s, "=") + foo() + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_reassigned6() bool { + s := "value" + i := strings.Index(s, "=") + for s = range map[string]struct{}{} { + } + if i >= 0 { + print(s[:i]) + } + return i >= 0 +} + +func basic_contains() bool { + s := "value" found := strings.Contains(s, "=") // want "strings.Index can be simplified using strings.Contains" return found }
diff --git a/go/analysis/passes/modernize/testdata/src/waitgroupgo/waitgroup.go b/go/analysis/passes/modernize/testdata/src/waitgroupgo/waitgroup.go index b5f2507..da611bd 100644 --- a/go/analysis/passes/modernize/testdata/src/waitgroupgo/waitgroup.go +++ b/go/analysis/passes/modernize/testdata/src/waitgroupgo/waitgroup.go
@@ -49,6 +49,45 @@ wg.Done() }() } + + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" + defer func() { + println("cleanup") + }() + fmt.Println() + wg.Done() + }() + + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" + defer func() { + recover := func() {} + recover() + }() + fmt.Println() + wg.Done() + }() + + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" + defer func() { + func() { + recover() + }() + }() + fmt.Println() + wg.Done() + }() + + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" + func() { + defer cleanup() + }() + fmt.Println() + wg.Done() + }() } // this function puts some wrong usages but waitgroupgo modernizer will still offer fixes. @@ -162,8 +201,26 @@ wg.Done() return 0 }() + + wg.Add(1) // noop: deferred recover changes panic/Done semantics. + go func() { + defer func() { + recover() + }() + panic("x") + wg.Done() + }() + + wg.Add(1) // noop: named-function defer may recover. + go func() { + defer cleanup() + fmt.Println() + wg.Done() + }() } +func cleanup() {} + type Server struct { wg sync.WaitGroup }
diff --git a/go/analysis/passes/modernize/testdata/src/waitgroupgo/waitgroup.go.golden b/go/analysis/passes/modernize/testdata/src/waitgroupgo/waitgroup.go.golden index 6fb58c3..3df02fe 100644 --- a/go/analysis/passes/modernize/testdata/src/waitgroupgo/waitgroup.go.golden +++ b/go/analysis/passes/modernize/testdata/src/waitgroupgo/waitgroup.go.golden
@@ -37,6 +37,37 @@ fmt.Println() }) } + + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" + defer func() { + println("cleanup") + }() + fmt.Println() + }) + + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" + defer func() { + recover := func() {} + recover() + }() + fmt.Println() + }) + + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" + defer func() { + func() { + recover() + }() + }() + fmt.Println() + }) + + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" + func() { + defer cleanup() + }() + fmt.Println() + }) } // this function puts some wrong usages but waitgroupgo modernizer will still offer fixes. @@ -144,8 +175,26 @@ wg.Done() return 0 }() + + wg.Add(1) // noop: deferred recover changes panic/Done semantics. + go func() { + defer func() { + recover() + }() + panic("x") + wg.Done() + }() + + wg.Add(1) // noop: named-function defer may recover. + go func() { + defer cleanup() + fmt.Println() + wg.Done() + }() } +func cleanup() {} + type Server struct { wg sync.WaitGroup }
diff --git a/go/analysis/passes/modernize/testingcontext.go b/go/analysis/passes/modernize/testingcontext.go index 9393305..da72364 100644 --- a/go/analysis/passes/modernize/testingcontext.go +++ b/go/analysis/passes/modernize/testingcontext.go
@@ -33,7 +33,7 @@ typeindexanalyzer.Analyzer, }, Run: testingContext, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#testingcontext", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_testingcontext", } // The testingContext pass replaces calls to context.WithCancel from within
diff --git a/go/analysis/passes/modernize/unsafefuncs.go b/go/analysis/passes/modernize/unsafefuncs.go index 34c135c..08198c9 100644 --- a/go/analysis/passes/modernize/unsafefuncs.go +++ b/go/analysis/passes/modernize/unsafefuncs.go
@@ -34,7 +34,7 @@ Doc: analyzerutil.MustExtractDoc(doc, "unsafefuncs"), Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: unsafefuncs, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#unsafefuncs", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_unsafefuncs", } func unsafefuncs(pass *analysis.Pass) (any, error) {
diff --git a/go/analysis/passes/modernize/waitgroupgo.go b/go/analysis/passes/modernize/waitgroupgo.go index 9af2d3b..4bc88d0 100644 --- a/go/analysis/passes/modernize/waitgroupgo.go +++ b/go/analysis/passes/modernize/waitgroupgo.go
@@ -9,6 +9,7 @@ "fmt" "go/ast" "go/printer" + "go/types" "slices" "golang.org/x/tools/go/analysis" @@ -30,7 +31,7 @@ typeindexanalyzer.Analyzer, }, Run: waitgroup, - URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#waitgroupgo", + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_waitgroupgo", } // The waitgroupgo pass replaces old more complex code with @@ -112,7 +113,7 @@ astutil.EqualSyntax(ast.Unparen(deferStmt.Call.Fun).(*ast.SelectorExpr).X, addCallRecv) { doneStmt = deferStmt // "defer wg.Done()" - } else if lastStmt, ok := list[len(list)-1].(*ast.ExprStmt); ok { + } else if lastStmt, ok := list[len(list)-1].(*ast.ExprStmt); ok && cannotRecover(lit.Body, info) { if doneCall, ok := lastStmt.X.(*ast.CallExpr); ok && typeutil.Callee(info, doneCall) == syncWaitGroupDone && astutil.EqualSyntax(ast.Unparen(doneCall.Fun).(*ast.SelectorExpr).X, addCallRecv) { @@ -175,3 +176,44 @@ } return nil, nil } + +// cannotRecover reports whether no panic arising in body can be +// recovered. It conservatively treats a defer of anything but a +// recover-free function literal (e.g. a named function) as able to recover. +func cannotRecover(body *ast.BlockStmt, info *types.Info) bool { + res := true + ast.Inspect(body, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.DeferStmt: + lit, ok := ast.Unparen(n.Call.Fun).(*ast.FuncLit) + if !ok || containsRecover(lit.Body, info) { + res = false + } + // Each defer is fully handled here; don't descend into it. + return false + case *ast.FuncLit: + // Defers in nested functions cannot recover panics from this body. + return false + } + return true + }) + return res +} + +func containsRecover(body *ast.BlockStmt, info *types.Info) bool { + found := false + ast.Inspect(body, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.CallExpr: + if typeutil.Callee(info, n) == builtinRecover { + found = true + return false + } + case *ast.FuncLit: + // Recover calls in nested functions cannot recover panics from body. + return false + } + return true + }) + return found +}
diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index f82d2ea..6573c3f 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go
@@ -1184,15 +1184,6 @@ } } } - if strings.HasSuffix(name, "ln") { - // The last item, if a string, should not have a newline. - arg = args[len(args)-1] - if s, ok := stringConstantExpr(pass, arg); ok { - if strings.HasSuffix(s, "\n") { - pass.ReportRangef(call, "%s arg list ends with redundant newline", name) - } - } - } for _, arg := range args { if isFunctionValue(pass, arg) { pass.ReportRangef(call, "%s arg %s is a func value, not called", name, astutil.Format(pass.Fset, arg))
diff --git a/go/analysis/passes/printf/testdata/src/a/a.go b/go/analysis/passes/printf/testdata/src/a/a.go index 37961c4..e29b059 100644 --- a/go/analysis/passes/printf/testdata/src/a/a.go +++ b/go/analysis/passes/printf/testdata/src/a/a.go
@@ -803,8 +803,8 @@ fmt.Printf("%s", uei) // want "Printf format %s has arg uei of wrong type a.unexportedErrorInterface" fmt.Println("foo\n", "bar") // not an error - fmt.Println("foo\n") // want "Println arg list ends with redundant newline" - fmt.Println("foo" + "\n") // want "Println arg list ends with redundant newline" + fmt.Println("foo\n") // not an error + fmt.Println("foo" + "\n") // not an error fmt.Println("foo\\n") // not an error fmt.Println(`foo\n`) // not an error
diff --git a/go/analysis/passes/stdversion/stdversion.go b/go/analysis/passes/stdversion/stdversion.go index d1fda88..3d44c4e8 100644 --- a/go/analysis/passes/stdversion/stdversion.go +++ b/go/analysis/passes/stdversion/stdversion.go
@@ -15,6 +15,7 @@ "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" ) @@ -64,8 +65,8 @@ pkg *types.Package version string } - memo := make(map[key]map[types.Object]string) // records symbol's minimum Go version - disallowedSymbols := func(pkg *types.Package, version string) map[types.Object]string { + memo := make(map[key]map[types.Object]stdlib.Symbol) + disallowedSymbols := func(pkg *types.Package, version string) map[types.Object]stdlib.Symbol { k := key{pkg, version} disallowed, ok := memo[k] if !ok { @@ -98,20 +99,12 @@ if fileVersion != "" { if obj, ok := pass.TypesInfo.Uses[n]; ok && obj.Pkg() != nil { disallowed := disallowedSymbols(obj.Pkg(), fileVersion) - if minVersion, ok := disallowed[origin(obj)]; ok { - // Some symbols are accessible before their release but - // only with specific build tags unknown to us here. - // Avoid false positives in such cases. - // TODO(mkalil): move this check into typesinternal.TooNewStdSymbols. - if obj.Pkg().Path() == "testing/synctest" && versions.AtLeast(fileVersion, "go1.24") { - break // requires go1.24 && goexperiment.synctest || go1.25 - } - noun := "module" - if fileVersion != pkgVersion { - noun = "file" - } + if sym, ok := disallowed[origin(obj)]; ok { pass.ReportRangef(n, "%s.%s requires %v or later (%s is %s)", - obj.Pkg().Name(), obj.Name(), minVersion, noun, fileVersion) + obj.Pkg().Name(), sym.Name, + sym.Version, + cond(fileVersion != pkgVersion, "file", "module"), + fileVersion) } } } @@ -134,3 +127,11 @@ } return obj } + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +}
diff --git a/go/analysis/passes/stdversion/stdversion_test.go b/go/analysis/passes/stdversion/stdversion_test.go index 71dc1de..9d86a0b 100644 --- a/go/analysis/passes/stdversion/stdversion_test.go +++ b/go/analysis/passes/stdversion/stdversion_test.go
@@ -10,15 +10,10 @@ "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/stdversion" - "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/testfiles" ) func Test(t *testing.T) { - testenv.NeedsGo1Point(t, 23) // TODO(#68658): Waiting on 1.22 backport. - - // The test relies on go1.21 std symbols, but the analyzer - // itself requires the go1.22 implementation of versions.FileVersions. dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "test.txtar")) analysistest.Run(t, dir, stdversion.Analyzer, "example.com/basic",
diff --git a/go/analysis/passes/stdversion/testdata/test.txtar b/go/analysis/passes/stdversion/testdata/test.txtar index cb04407..3073d07 100644 --- a/go/analysis/passes/stdversion/testdata/test.txtar +++ b/go/analysis/passes/stdversion/testdata/test.txtar
@@ -31,10 +31,10 @@ var _ types.Info // ok: defined by go1.0 // new field of older type - _ = new(types.Info).FileVersions // want `types.FileVersions requires go1.22 or later \(module is go1.21\)` + _ = new(types.Info).FileVersions // want `types.Info.FileVersions requires go1.22 or later \(module is go1.21\)` // new method of older type - new(types.Info).PkgNameOf // want `types.PkgNameOf requires go1.22 or later \(module is go1.21\)` + new(types.Info).PkgNameOf // want `types.\(\*Info\).PkgNameOf requires go1.22 or later \(module is go1.21\)` // new package-level type var a types.Alias // want `types.Alias requires go1.22 or later \(module is go1.21\)` @@ -53,7 +53,7 @@ func _() { // report something before the syntax error. - _ = new(types.Info).FileVersions // want `types.FileVersions requires go1.22 or later \(module is go1.21\)` + _ = new(types.Info).FileVersions // want `types.Info.FileVersions requires go1.22 or later \(module is go1.21\)` } invalid syntax // exercise RunDespiteErrors @@ -122,10 +122,10 @@ var _ types.Info // ok: defined by go1.0 // new field of older type - _ = new(types.Info).FileVersions // want `types.FileVersions requires go1.22 or later \(module is go1.21\)` + _ = new(types.Info).FileVersions // want `types.Info.FileVersions requires go1.22 or later \(module is go1.21\)` // new method of older type - new(types.Info).PkgNameOf // want `types.PkgNameOf requires go1.22 or later \(module is go1.21\)` + new(types.Info).PkgNameOf // want `types.\(\*Info\).PkgNameOf requires go1.22 or later \(module is go1.21\)` // new package-level type var a types.Alias // want `types.Alias requires go1.22 or later \(module is go1.21\)`
diff --git a/go/analysis/passes/unusedresult/testdata/src/typeparams/typeparams.go b/go/analysis/passes/unusedresult/testdata/src/typeparams/typeparams.go index 0add516..b6b3a3b 100644 --- a/go/analysis/passes/unusedresult/testdata/src/typeparams/typeparams.go +++ b/go/analysis/passes/unusedresult/testdata/src/typeparams/typeparams.go
@@ -30,10 +30,10 @@ _ = userdefs.MustUse[int](2) s := userdefs.SingleTypeParam[int]{X: 1} - s.String() // want `result of \(\*typeparams/userdefs.SingleTypeParam\[int\]\).String call not used` + s.String() // want `result of \(\*typeparams/userdefs.SingleTypeParam\[T\]\).String call not used` _ = s.String() m := userdefs.MultiTypeParam[int, string]{X: 1, Y: "one"} - m.String() // want `result of \(\*typeparams/userdefs.MultiTypeParam\[int, string\]\).String call not used` + m.String() // want `result of \(\*typeparams/userdefs.MultiTypeParam\[T, U\]\).String call not used` _ = m.String() }
diff --git a/go/analysis/passes/unusedresult/unusedresult.go b/go/analysis/passes/unusedresult/unusedresult.go index bd32d58..82968f4 100644 --- a/go/analysis/passes/unusedresult/unusedresult.go +++ b/go/analysis/passes/unusedresult/unusedresult.go
@@ -16,7 +16,6 @@ import ( _ "embed" "go/ast" - "go/token" "go/types" "sort" "strings" @@ -27,6 +26,7 @@ "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/analysis/analyzerutil" "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/typesinternal" ) //go:embed doc.go @@ -172,7 +172,7 @@ } // func() string -var sigNoArgsStringResult = types.NewSignatureType(nil, nil, nil, nil, types.NewTuple(types.NewParam(token.NoPos, nil, "", types.Typ[types.String])), false) +var sigNoArgsStringResult = types.NewSignatureType(nil, nil, nil, nil, typesinternal.TupleOf(types.Typ[types.String]), false) type stringSetFlag map[string]bool
diff --git a/go/analysis/suite/vet/vet.go b/go/analysis/suite/vet/vet.go index b85ee65..7d03c9c 100644 --- a/go/analysis/suite/vet/vet.go +++ b/go/analysis/suite/vet/vet.go
@@ -34,9 +34,11 @@ "golang.org/x/tools/go/analysis/passes/lostcancel" "golang.org/x/tools/go/analysis/passes/nilfunc" "golang.org/x/tools/go/analysis/passes/printf" + "golang.org/x/tools/go/analysis/passes/scannererr" "golang.org/x/tools/go/analysis/passes/shift" "golang.org/x/tools/go/analysis/passes/sigchanyzer" "golang.org/x/tools/go/analysis/passes/slog" + "golang.org/x/tools/go/analysis/passes/sqlrowserr" "golang.org/x/tools/go/analysis/passes/stdmethods" "golang.org/x/tools/go/analysis/passes/stdversion" "golang.org/x/tools/go/analysis/passes/stringintconv" @@ -78,12 +80,12 @@ lostcancel.Analyzer, nilfunc.Analyzer, printf.Analyzer, - // scannererr.Analyzer, // TODO(adonovan): add to go vet for 1.28 after the freeze (#17747) + scannererr.Analyzer, // shadow.Analyzer omitted: too noisy shift.Analyzer, sigchanyzer.Analyzer, slog.Analyzer, - // sqlrowserr.Analyzer, // TODO(adonovan): add to go vet for 1.28 after the freeze (#17747) + sqlrowserr.Analyzer, stdmethods.Analyzer, stdversion.Analyzer, stringintconv.Analyzer,
diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go index f6f3cd5..018b4bc 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go
@@ -59,7 +59,7 @@ // whose name ends with ".cfg". type Config struct { ID string // e.g. "fmt [fmt.test]" - Compiler string // gc or gccgo, provided to MakeImporter + Compiler string // gc or gccgo, provided to makeTypesImporter Dir string // (unused) ImportPath string // package path GoVersion string // minimum required Go version, such as "go1.21.0" @@ -296,7 +296,7 @@ return importerFunc(func(importPath string) (*types.Package, error) { path, ok := cfg.ImportMap[importPath] // resolve vendoring, etc if !ok { - return nil, fmt.Errorf("can't resolve import %q", path) + return nil, fmt.Errorf("can't resolve import %q", importPath) } return compilerImporter.Import(path) })
diff --git a/go/ast/inspector/cursor.go b/go/ast/inspector/cursor.go index 239b10c..1c48225 100644 --- a/go/ast/inspector/cursor.go +++ b/go/ast/inspector/cursor.go
@@ -10,6 +10,7 @@ "go/token" "iter" "reflect" + "strings" "golang.org/x/tools/go/ast/edge" ) @@ -110,6 +111,46 @@ return reflect.TypeOf(c.Node()).String() } +// GoString returns a string describing the cursor's path from the +// root, if any. +func (c Cursor) GoString() string { + if !c.Valid() { + return "(invalid)" + } + if c.index < 0 { + return "(root)" + } + // e.g "File.Decls[1].(*ast.GenDecl).Specs[0].(*ast.TypeSpec)" + // + // In hindsight even the File node should have reported a + // virtual ParentEdge of (Root_Files, i) where i is the index + // among the files passed to NewInspector. Then the path would + // read "(root).Files[i]", etc; but we missed the boat. + var buf strings.Builder + buf.WriteString("File") + var visit func(Cursor) + visit = func(c Cursor) { + ek, idx := c.ParentEdge() + if ek == edge.Invalid { + return // File + } + visit(c.Parent()) + fmt.Fprintf(&buf, ".%s", ek.FieldName()) + if idx >= 0 { + fmt.Fprintf(&buf, "[%d]", idx) + } + ftype := ek.FieldType() + if idx >= 0 { + ftype = ftype.Elem() // []T -> T + } + if ftype.Kind() == reflect.Interface { + fmt.Fprintf(&buf, ".(%T)", c.Node()) + } + } + visit(c) + return buf.String() +} + // indices return the [start, end) half-open interval of event indices. func (c Cursor) indices() (int32, int32) { if c.index < 0 {
diff --git a/go/ast/inspector/cursor_test.go b/go/ast/inspector/cursor_test.go index c216492..42ba57b 100644 --- a/go/ast/inspector/cursor_test.go +++ b/go/ast/inspector/cursor_test.go
@@ -542,6 +542,33 @@ } } +func TestCursor_GoString(t *testing.T) { + const src = `package a +func f() +func g() { + print("goodbye") +} +` + fset := token.NewFileSet() + file, _ := parser.ParseFile(fset, "a.go", src, 0) + root := inspector.New([]*ast.File{file}).Root() + + // Find sole (string) literal. + var curLit inspector.Cursor + for cur := range root.Preorder((*ast.BasicLit)(nil)) { + curLit = cur + } + got := curLit.GoString() + want := "File.Decls[1].(*ast.FuncDecl).Body.List[0].(*ast.ExprStmt).X.(*ast.CallExpr).Args[0].(*ast.BasicLit)" + if got != want { + t.Errorf("GoString = %q, want %s", got, want) + } + + if file.Decls[1].(*ast.FuncDecl).Body.List[0].(*ast.ExprStmt).X.(*ast.CallExpr).Args[0].(*ast.BasicLit) != curLit.Node() { + t.Errorf("GoString path was inaccurate") + } +} + func is[T any](x any) bool { _, ok := x.(T) return ok
diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go index 6cee0c3..fdb64e0 100644 --- a/go/callgraph/rta/rta.go +++ b/go/callgraph/rta/rta.go
@@ -45,6 +45,7 @@ "golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/typesinternal" ) // A Result holds the results of Rapid Type Analysis, which includes the @@ -72,6 +73,10 @@ // fmt.Println(new(A)) // Types *A, A and B are accessible to reflection, but the unnamed // type struct{B} is not. + // + // TODO(adonovan): populating this field is expensive yet it + // is never used in x/tools. Add a revised [Analyze] API that + // provides the option not to set it. RuntimeTypes typeutil.Map } @@ -279,7 +284,7 @@ // interface materializes its runtime // type, allowing any of its exported // methods to be called though reflection. - r.addRuntimeType(instr.X.Type(), false) + r.addRuntimeType(instr.X.Type()) } // Process all address-taken functions. @@ -407,127 +412,43 @@ // addRuntimeType is called for each concrete type that can be the // dynamic type of some interface or reflect.Value. -// Adapted from needMethods in go/ssa/builder.go -func (r *rta) addRuntimeType(T types.Type, skip bool) { - // Never record aliases. - T = types.Unalias(T) - - if prev, ok := r.result.RuntimeTypes.At(T).(bool); ok { - if skip && !prev { - r.result.RuntimeTypes.Set(T, skip) +func (r *rta) addRuntimeType(T types.Type) { + methodSetOf := r.prog.MethodSets.MethodSet + typesinternal.ForEachElement(methodSetOf, T, func(T types.Type, access bool) bool { + if prevInaccess, ok := r.result.RuntimeTypes.At(T).(bool); ok { + if prevInaccess && access { + // A type previously marked inaccessible (ok && prevInaccess) + // is now found to be accessible (access): + // record that it is no longer inaccessible (false). + // (The inverted sense of the map is regrettable.) + r.result.RuntimeTypes.Set(T, false) + } + return true // seen; prune traversal } - return - } - r.result.RuntimeTypes.Set(T, skip) + r.result.RuntimeTypes.Set(T, !access) // record inaccessibility - mset := r.prog.MethodSets.MethodSet(T) + if !types.IsInterface(T) { + // T is a new concrete type. - if _, ok := T.Underlying().(*types.Interface); !ok { - // T is a new concrete type. - for i, n := 0, mset.Len(); i < n; i++ { - sel := mset.At(i) - m := sel.Obj() + // Exported methods are always potentially callable via reflection. + for sel := range methodSetOf(T).Methods() { + if sel.Obj().Exported() { + r.addReachable(r.prog.MethodValue(sel), true) + } + } - if m.Exported() { - // Exported methods are always potentially callable via reflection. - r.addReachable(r.prog.MethodValue(sel), true) + // Add callgraph edge for each existing dynamic + // "invoke"-mode call via that interface. + for _, I := range r.interfaces(T) { + sites, _ := r.invokeSites.At(I).([]ssa.CallInstruction) + for _, site := range sites { + r.addInvokeEdge(site, T) + } } } - // Add callgraph edge for each existing dynamic - // "invoke"-mode call via that interface. - for _, I := range r.interfaces(T) { - sites, _ := r.invokeSites.At(I).([]ssa.CallInstruction) - for _, site := range sites { - r.addInvokeEdge(site, T) - } - } - } - - // Precondition: T is not a method signature (*Signature with Recv()!=nil). - // Recursive case: skip => don't call makeMethods(T). - // Each package maintains its own set of types it has visited. - - var n *types.Named - switch T := types.Unalias(T).(type) { - case *types.Named: - n = T - case *types.Pointer: - n, _ = types.Unalias(T.Elem()).(*types.Named) - } - if n != nil { - owner := n.Obj().Pkg() - if owner == nil { - return // built-in error type - } - } - - // Recursion over signatures of each exported method. - for method := range mset.Methods() { - if method.Obj().Exported() { - sig := method.Type().(*types.Signature) - r.addRuntimeType(sig.Params(), true) // skip the Tuple itself - r.addRuntimeType(sig.Results(), true) // skip the Tuple itself - } - } - - switch t := T.(type) { - case *types.Alias: - panic("unreachable") - - case *types.Basic: - // nop - - case *types.Interface: - // nop---handled by recursion over method set. - - case *types.Pointer: - r.addRuntimeType(t.Elem(), false) - - case *types.Slice: - r.addRuntimeType(t.Elem(), false) - - case *types.Chan: - r.addRuntimeType(t.Elem(), false) - - case *types.Map: - r.addRuntimeType(t.Key(), false) - r.addRuntimeType(t.Elem(), false) - - case *types.Signature: - if t.Recv() != nil { - panic(fmt.Sprintf("Signature %s has Recv %s", t, t.Recv())) - } - r.addRuntimeType(t.Params(), true) // skip the Tuple itself - r.addRuntimeType(t.Results(), true) // skip the Tuple itself - - case *types.Named: - // A pointer-to-named type can be derived from a named - // type via reflection. It may have methods too. - r.addRuntimeType(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. - r.addRuntimeType(t.Underlying(), true) - - case *types.Array: - r.addRuntimeType(t.Elem(), false) - - case *types.Struct: - for i, n := 0, t.NumFields(); i < n; i++ { - r.addRuntimeType(t.Field(i).Type(), false) - } - - case *types.Tuple: - for i, n := 0, t.Len(); i < n; i++ { - r.addRuntimeType(t.At(i).Type(), false) - } - - default: - panic(T) - } + return false + }) } // fingerprint returns a bitmask with one bit set per method id,
diff --git a/go/loader/loader.go b/go/loader/loader.go index 9c5f7db..3c9d4fe 100644 --- a/go/loader/loader.go +++ b/go/loader/loader.go
@@ -738,7 +738,9 @@ // Preprocess CgoFiles and parse the outputs (sequentially). if which == 'g' && bp.CgoFiles != nil { + ioLimit <- true cgofiles, err := cgo.ProcessFiles(bp, conf.fset(), conf.DisplayPath, conf.ParserMode) + <-ioLimit if err != nil { errs = append(errs, err) } else {
diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 244f650..f93ec0e 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go
@@ -3493,7 +3493,7 @@ type result struct{ Dir, ForTest string } got := make(map[string]result) for pkg := range packages.Postorder(pkgs) { - if !packagepath.IsStdPackage(pkg.PkgPath) { + if !packagepath.MaybeStdPackage(pkg.PkgPath) { rel, err := filepath.Rel(dir, pkg.Dir) if err != nil { t.Errorf("Rel(%q, %q) failed: %v", dir, pkg.Dir, err)
diff --git a/go/packages/visit.go b/go/packages/visit.go index c546b1b..06747a9 100644 --- a/go/packages/visit.go +++ b/go/packages/visit.go
@@ -5,11 +5,11 @@ package packages import ( - "cmp" "fmt" "iter" "os" - "slices" + + "golang.org/x/tools/internal/moremaps" ) // Visit visits all the packages in the import graph whose roots are @@ -40,7 +40,7 @@ seen[pkg] = true if pre == nil || pre(pkg) { - for _, imp := range sorted(pkg.Imports) { // for determinism + for _, imp := range moremaps.Sorted(pkg.Imports) { // for determinism visit(imp) } } @@ -88,7 +88,7 @@ visit = func(pkg *Package) bool { if !seen[pkg] { seen[pkg] = true - for _, imp := range sorted(pkg.Imports) { // for determinism + for _, imp := range moremaps.Sorted(pkg.Imports) { // for determinism if !visit(imp) { return false } @@ -106,28 +106,3 @@ } } } - -// -- copied from golang.org.x/tools/gopls/internal/util/moremaps -- - -// sorted returns an iterator over the entries of m in key order. -func sorted[M ~map[K]V, K cmp.Ordered, V any](m M) iter.Seq2[K, V] { - // TODO(adonovan): use maps.Sorted if proposal #68598 is accepted. - return func(yield func(K, V) bool) { - keys := keySlice(m) - slices.Sort(keys) - for _, k := range keys { - if !yield(k, m[k]) { - break - } - } - } -} - -// KeySlice returns the keys of the map M, like slices.Collect(maps.Keys(m)). -func keySlice[M ~map[K]V, K comparable, V any](m M) []K { - r := make([]K, 0, len(m)) - for k := range m { - r = append(r, k) - } - return r -}
diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 1669d80..a663af8 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go
@@ -85,6 +85,7 @@ "slices" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" ) @@ -124,7 +125,7 @@ // The ssa:deferstack intrinsic returns the current function's defer stack. vDeferStack = &Builtin{ name: "ssa:deferstack", - sig: types.NewSignatureType(nil, nil, nil, nil, types.NewTuple(anonVar(tDeferStack)), false), + sig: types.NewSignatureType(nil, nil, nil, nil, typesinternal.TupleOf(tDeferStack), false), } ) @@ -1719,7 +1720,7 @@ for _, st := range states { if st.Dir == types.RecvOnly { chtyp := typeparams.CoreType(fn.typ(st.Chan.Type())).(*types.Chan) - vars = append(vars, anonVar(chtyp.Elem())) + vars = append(vars, newVar("", chtyp.Elem())) } } sel.setType(types.NewTuple(vars...))
diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 82faade..8de0785 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go
@@ -167,10 +167,18 @@ // eliminates the need to eagerly compute all the element // types during SSA building. var runtimeTypes []types.Type - add := func(t types.Type) { runtimeTypes = append(runtimeTypes, t) } var set typeutil.Map // for de-duping identical types for t := range prog.makeInterfaceTypes { - typesinternal.ForEachElement(&set, &prog.MethodSets, t, add) + typesinternal.ForEachElement(prog.MethodSets.MethodSet, t, func(t types.Type, access bool) bool { + if !access { + return false // inaccessible to reflection + } + seen, _ := set.Set(t, true).(bool) + if !seen { + runtimeTypes = append(runtimeTypes, t) + } + return seen + }) } return runtimeTypes
diff --git a/go/ssa/util.go b/go/ssa/util.go index 42f9621..5dfca72 100644 --- a/go/ssa/util.go +++ b/go/ssa/util.go
@@ -181,19 +181,13 @@ return types.NewParam(token.NoPos, nil, name, typ) } -// anonVar creates an anonymous 'var' for use in a types.Tuple. -func anonVar(typ types.Type) *types.Var { - return newVar("", typ) -} - -var lenResults = types.NewTuple(anonVar(tInt)) +var lenResults = typesinternal.TupleOf(tInt) // makeLen returns the len builtin specialized to type func(T)int. func makeLen(T types.Type) *Builtin { - lenParams := types.NewTuple(anonVar(T)) return &Builtin{ name: "len", - sig: types.NewSignatureType(nil, nil, nil, lenParams, lenResults, false), + sig: types.NewSignatureType(nil, nil, nil, typesinternal.TupleOf(T), lenResults, false), } }
diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index 6cadd04..fbb0674 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go
@@ -26,6 +26,7 @@ "go/types" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" ) // -- wrappers ----------------------------------------------------------- @@ -118,10 +119,12 @@ // For simple indirection wrappers, perform an informative nil-check: // "value method (T).f called using nil *T pointer" if len(indices) == 1 && !isPointer(recvType(fn.object)) { + params := typesinternal.TupleOf(fn.method.recv, tString, tString) + results := typesinternal.TupleOf(fn.method.recv) var c Call c.Call.Value = &Builtin{ name: "ssa:wrapnilchk", - sig: types.NewSignatureType(nil, nil, nil, types.NewTuple(anonVar(fn.method.recv), anonVar(tString), anonVar(tString)), types.NewTuple(anonVar(fn.method.recv)), false), + sig: types.NewSignatureType(nil, nil, nil, params, results, false), } c.Call.Args = []Value{ v,
diff --git a/go/types/internal/play/play.go b/go/types/internal/play/play.go index f48394c..a682dc2 100644 --- a/go/types/internal/play/play.go +++ b/go/types/internal/play/play.go
@@ -169,10 +169,10 @@ // cases (e.g. around FuncType.Func). curFile, _ := inspector.New([]*ast.File{file}).Root().FirstChild() if cur, ok := curFile.FindByPos(startPos, endPos); ok { - fmt.Fprintf(out, "Cursor.FindByPos().Enclosing() = %v\n", - slices.Collect(cur.Enclosing())) + fmt.Fprintf(out, "Cursor.GoString = %#v\n", cur) + fmt.Fprintf(out, "Cursor.Enclosing = %v\n", slices.Collect(cur.Enclosing())) } else { - fmt.Fprintf(out, "Cursor.FindPos() failed\n") + fmt.Fprintf(out, "Cursor.FindPos failed\n") } // And show the astutil.Select result (enclosing, leftmost & rightmost enclosed). if curEnclosing, curStart, curEnd, err := astutil.Select(curFile, startPos, endPos); err == nil {
diff --git a/go/types/typeutil/callee.go b/go/types/typeutil/callee.go index 3d24a8c..b64a8f4 100644 --- a/go/types/typeutil/callee.go +++ b/go/types/typeutil/callee.go
@@ -7,7 +7,8 @@ import ( "go/ast" "go/types" - _ "unsafe" // for linkname + + "golang.org/x/tools/internal/typesinternal" ) // Callee returns the named target of a function call, if any: @@ -19,14 +20,7 @@ // Note: for calls of instantiated functions and methods, Callee returns // the corresponding generic function or method on the generic type. func Callee(info *types.Info, call *ast.CallExpr) types.Object { - obj := info.Uses[usedIdent(info, call.Fun)] - if obj == nil { - return nil - } - if _, ok := obj.(*types.TypeName); ok { - return nil - } - return obj + return typesinternal.Callee(info, call) } // StaticCallee returns the target (function or method) of a static function @@ -35,52 +29,5 @@ // Note: for calls of instantiated functions and methods, StaticCallee returns // the corresponding generic function or method on the generic type. func StaticCallee(info *types.Info, call *ast.CallExpr) *types.Func { - obj := info.Uses[usedIdent(info, call.Fun)] - fn, _ := obj.(*types.Func) - if fn == nil || interfaceMethod(fn) { - return nil - } - return fn -} - -// usedIdent is the implementation of [internal/typesinternal.UsedIdent]. -// It returns the identifier associated with e. -// See typesinternal.UsedIdent for a fuller description. -// This function should live in typesinternal, but cannot because it would -// create an import cycle. -// -//go:linkname usedIdent golang.org/x/tools/go/types/typeutil.usedIdent -func usedIdent(info *types.Info, e ast.Expr) *ast.Ident { - if info.Types == nil || info.Uses == nil { - panic("one of info.Types or info.Uses is nil; both must be populated") - } - // Look through type instantiation if necessary. - switch d := ast.Unparen(e).(type) { - case *ast.IndexExpr: - if info.Types[d.Index].IsType() { - e = d.X - } - case *ast.IndexListExpr: - e = d.X - } - - switch e := ast.Unparen(e).(type) { - // info.Uses always has the object we want, even for selector expressions. - // We don't need info.Selections. - // See go/types/recording.go:recordSelection. - case *ast.Ident: - return e - case *ast.SelectorExpr: - return e.Sel - } - return nil -} - -// interfaceMethod reports whether its argument is a method of an interface. -// This function should live in typesinternal, but cannot because it would create an import cycle. -// -//go:linkname interfaceMethod golang.org/x/tools/go/types/typeutil.interfaceMethod -func interfaceMethod(f *types.Func) bool { - recv := f.Signature().Recv() - return recv != nil && types.IsInterface(recv.Type()) + return typesinternal.StaticCallee(info, call) }
diff --git a/go/types/typeutil/callee_test.go b/go/types/typeutil/callee_test.go index 3f96533..74d31cc 100644 --- a/go/types/typeutil/callee_test.go +++ b/go/types/typeutil/callee_test.go
@@ -167,3 +167,59 @@ } } } + +// TestCalleeReturnsOrigin ensures that Callee and StaticCallee +// return the generic (origin) symbol, not the instance. +func TestCalleeReturnsOrigin(t *testing.T) { + const src = `package p + +func F[T any]() {} + +type G[T any] struct{} + +func (G[T]) M() {} + +func calls() { + F[int]() + G[int]{}.M() +} +` + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "test.go", src, 0) + if err != nil { + t.Fatal(err) + } + calls := f.Decls[3].(*ast.FuncDecl).Body.List + call := func(index int) *ast.CallExpr { + return calls[index].(*ast.ExprStmt).X.(*ast.CallExpr) + } + + info := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Uses: make(map[*ast.Ident]types.Object), + } + pkg, err := new(types.Config).Check("p", fset, []*ast.File{f}, info) + if err != nil { + t.Fatal(err) + } + fObj := pkg.Scope().Lookup("F") + mObj := pkg.Scope().Lookup("G").Type().(*types.Named).Method(0) + + tests := []struct { + name string + call *ast.CallExpr + want types.Object + }{ + {"F[int]()", call(0), fObj}, + {"G[int]{}.M()", call(1), mObj}, + } + for _, test := range tests { + if got := typeutil.Callee(info, test.call); got != test.want { + t.Errorf("call %s: Callee returned %v, want declaration %v", test.name, got, test.want) + } + if got := typeutil.StaticCallee(info, test.call); got != test.want { + t.Errorf("call %s: StaticCallee returned %v, want declaration %v", test.name, got, test.want) + } + } +}
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 70d2b91..d0bd3b0 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md
@@ -2856,7 +2856,7 @@ Default: on. -Package documentation: [any](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any) +Package documentation: [any](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_any) <a id='appendclipped'></a> ## `appendclipped`: simplify append chains using slices.Concat @@ -2872,7 +2872,7 @@ Default: off. Enable by setting `"analyses": {"appendclipped": true}`. -Package documentation: [appendclipped](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#appendclipped) +Package documentation: [appendclipped](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_appendclipped) <a id='appends'></a> ## `appends`: check for missing values after append @@ -2949,7 +2949,7 @@ Default: on. -Package documentation: [atomictypes](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#atomictypes) +Package documentation: [atomictypes](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_atomictypes) <a id='bloop'></a> ## `bloop`: replace for-range over b.N with b.Loop @@ -2963,7 +2963,7 @@ Default: on. -Package documentation: [bloop](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#bloop) +Package documentation: [bloop](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_bloop) <a id='bools'></a> ## `bools`: check for common mistakes involving boolean operators @@ -3119,7 +3119,7 @@ Default: on. -Package documentation: [embedlit](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#embedlit) +Package documentation: [embedlit](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_embedlit) <a id='errorsas'></a> ## `errorsas`: report passing non-pointer or non-error values to errors.As @@ -3155,7 +3155,7 @@ Default: on. -Package documentation: [errorsastype](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype) +Package documentation: [errorsastype](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_errorsastype) <a id='errorsastypeshadow'></a> ## `errorsastypeshadow`: report shadowing of errors.AsType[T] in if/else chains @@ -3239,7 +3239,7 @@ Default: on. -Package documentation: [fmtappendf](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#fmtappendf) +Package documentation: [fmtappendf](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_fmtappendf) <a id='forvar'></a> ## `forvar`: remove redundant re-declaration of loop variables @@ -3251,7 +3251,7 @@ Default: on. -Package documentation: [forvar](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#forvar) +Package documentation: [forvar](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_forvar) <a id='framepointer'></a> ## `framepointer`: report assembly that clobbers the frame pointer before saving it @@ -3332,7 +3332,7 @@ Default: on. -Package documentation: [importcomment](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#importcomment) +Package documentation: [importcomment](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_importcomment) <a id='infertypeargs'></a> ## `infertypeargs`: check for unnecessary type arguments in call expressions @@ -3554,7 +3554,7 @@ Default: on. -Package documentation: [mapsloop](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop) +Package documentation: [mapsloop](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_mapsloop) <a id='minmax'></a> ## `minmax`: replace if/else statements with calls to min or max @@ -3572,7 +3572,7 @@ Default: on. -Package documentation: [minmax](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax) +Package documentation: [minmax](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_minmax) <a id='newexpr'></a> ## `newexpr`: simplify code by using go1.26's new(expr) @@ -3601,7 +3601,7 @@ Default: on. -Package documentation: [newexpr](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#newexpr) +Package documentation: [newexpr](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_newexpr) <a id='nilfunc'></a> ## `nilfunc`: check for useless comparisons between functions and nil @@ -3719,7 +3719,7 @@ Default: on. -Package documentation: [omitzero](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero) +Package documentation: [omitzero](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_omitzero) <a id='plusbuild'></a> ## `plusbuild`: remove obsolete //+build comments @@ -3737,7 +3737,7 @@ Default: on. -Package documentation: [plusbuild](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#plusbuild) +Package documentation: [plusbuild](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_plusbuild) <a id='printf'></a> ## `printf`: check consistency of Printf format strings and arguments @@ -3751,6 +3751,18 @@ Package documentation: [printf](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/printf) +<a id='ptrtoerror'></a> +## `ptrtoerror`: detect inconsistent conversions of concrete types to error + +The ptrtoerror analyzer detects when a concrete type E is converted to the error interface inconsistently, both as a value of type E and as a pointer of type \*E. Such inconsistency defeats attempts by client code to test for specific error types using type assertions or library functions such as [errors.As](/errors#As) and [errors.Is](/errors#Is). + +The analyzer also detects when both E and \*E implement error but neither of those types is converted to error within the defining package, leaving the intended error form (E or \*E) ambiguous. This diagnostic offers two alternative fixes to add declarations that make the intent explicit. + + +Default: on. + +Package documentation: [ptrtoerror](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/ptrtoerror) + <a id='rangeint'></a> ## `rangeint`: replace 3-clause for loops with for-range over integers @@ -3767,7 +3779,7 @@ Default: on. -Package documentation: [rangeint](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint) +Package documentation: [rangeint](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_rangeint) <a id='recursiveiter'></a> ## `recursiveiter`: check for inefficient recursive iterators @@ -3838,6 +3850,20 @@ Package documentation: [recursiveiter](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/recursiveiter) +<a id='reflecttypeassert'></a> +## `reflecttypeassert`: replace v.Interface().(T) with reflect.TypeAssert[T](v) + +This analyzer suggests fixes to replace two-valued type assertions on the result of (reflect.Value).Interface with reflect.TypeAssert, introduced in go1.25, which avoids the intermediate allocation of an interface value, for example: + + x, ok := v.Interface().(string) -> x, ok := reflect.TypeAssert[string](v) + +No fix is offered for single-valued assertions, since they panic when the assertion fails whereas reflect.TypeAssert does not. Nor is a fix offered for a type switch. + + +Default: on. + +Package documentation: [reflecttypeassert](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_reflecttypeassert) + <a id='reflecttypefor'></a> ## `reflecttypefor`: replace reflect.TypeOf(x) with TypeFor[T]() @@ -3868,7 +3894,7 @@ Default: on. -Package documentation: [reflecttypefor](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypefor) +Package documentation: [reflecttypefor](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_reflecttypefor) <a id='scannererr'></a> ## `scannererr`: report failure to check bufio.Scanner.Err @@ -4038,7 +4064,25 @@ Default: on. -Package documentation: [slicesbackward](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesbackward) +Package documentation: [slicesbackward](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesbackward) + +<a id='slicesclip'></a> +## `slicesclip`: replace three-index slice expressions with slices.Clip + +The slicesclip analyzer suggests replacing a full slice expression of the form + + x[:len(x):len(x)] + +which clips the capacity of a slice to its length, with the simpler and more readable + + slices.Clip(x) + +added in Go 1.21. + + +Default: on. + +Package documentation: [slicesclip](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesclip) <a id='slicescontains'></a> ## `slicescontains`: replace loops with slices.Contains or slices.ContainsFunc @@ -4050,7 +4094,7 @@ Default: on. -Package documentation: [slicescontains](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicescontains) +Package documentation: [slicescontains](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicescontains) <a id='slicesdelete'></a> ## `slicesdelete`: replace append-based slice deletion with slices.Delete @@ -4070,7 +4114,7 @@ Default: off. Enable by setting `"analyses": {"slicesdelete": true}`. -Package documentation: [slicesdelete](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesdelete) +Package documentation: [slicesdelete](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesdelete) <a id='slicessort'></a> ## `slicessort`: replace sort.Slice with slices.Sort for basic types @@ -4084,7 +4128,7 @@ Default: on. -Package documentation: [slicessort](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort) +Package documentation: [slicessort](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicessort) <a id='slog'></a> ## `slog`: check for invalid structured logging calls @@ -4156,7 +4200,7 @@ or its "for elem := range x.Len()" equivalent by a range loop over an iterator offered by the same data type: for elem := range x.All() { - use(x.At(i) + use(elem) } where x is one of various well-known types in the standard library. @@ -4164,7 +4208,7 @@ Default: on. -Package documentation: [stditerators](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stditerators) +Package documentation: [stditerators](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stditerators) <a id='stdmethods'></a> ## `stdmethods`: check signature of methods of well-known interfaces @@ -4265,7 +4309,7 @@ Default: on. -Package documentation: [stringsbuilder](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringbuilder) +Package documentation: [stringsbuilder](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringsbuilder) <a id='stringscut'></a> ## `stringscut`: replace strings.Index etc. with strings.Cut @@ -4317,7 +4361,7 @@ Default: on. -Package documentation: [stringscut](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscut) +Package documentation: [stringscut](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringscut) <a id='stringscutprefix'></a> ## `stringscutprefix`: replace HasPrefix/TrimPrefix with CutPrefix @@ -4351,7 +4395,7 @@ Default: on. -Package documentation: [stringscutprefix](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscutprefix) +Package documentation: [stringscutprefix](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringscutprefix) <a id='stringsseq'></a> ## `stringsseq`: replace ranging over Split/Fields with SplitSeq/FieldsSeq @@ -4369,7 +4413,7 @@ Default: on. -Package documentation: [stringsseq](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq) +Package documentation: [stringsseq](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringsseq) <a id='structtag'></a> ## `structtag`: check that struct field tags conform to reflect.StructTag.Get @@ -4396,7 +4440,7 @@ Default: on. -Package documentation: [testingcontext](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#testingcontext) +Package documentation: [testingcontext](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_testingcontext) <a id='testinggoroutine'></a> ## `testinggoroutine`: report calls to (*testing.T).Fatal from goroutines started by a test @@ -4472,7 +4516,7 @@ Default: on. -Package documentation: [unsafefuncs](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#unsafefuncs) +Package documentation: [unsafefuncs](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_unsafefuncs) <a id='unsafeptr'></a> ## `unsafeptr`: check for invalid conversions of uintptr to unsafe.Pointer @@ -4637,7 +4681,7 @@ Default: on. -Package documentation: [waitgroupgo](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#waitgroupgo) +Package documentation: [waitgroupgo](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_waitgroupgo) <a id='writestring'></a> ## `writestring`: detect inefficient string concatenation in uses of WriteString
diff --git a/gopls/doc/features/assembly.md b/gopls/doc/features/assembly.md index 7302999..f6074e8 100644 --- a/gopls/doc/features/assembly.md +++ b/gopls/doc/features/assembly.md
@@ -11,21 +11,39 @@ directory containing at least one `*.go` file, then the `.s` file is Go assembly, and its appropriate language server is gopls. -Only Definition (`textDocument/definition`) requests are currently -supported. For example, a Definition request on the `sigpanic` -symbol in this file in GOROOT/src/runtime/asm.s: +The following requests are currently supported: -```asm - JMP ·sigpanic<ABIInternal>(SB) -``` +- Definition (`textDocument/definition`): on a reference to a symbol, + returns the location of its declaration. For example, a Definition + request on the `sigpanic` symbol in this file in + GOROOT/src/runtime/asm.s: -returns the location of the function declaration in -GOROOT/src/runtime/signal_unix.go: + ```asm + JMP ·sigpanic<ABIInternal>(SB) + ``` -```go -//go:linkname sigpanic -func sigpanic() { -``` + returns the location of the function declaration in + GOROOT/src/runtime/signal_unix.go: + + ```go + //go:linkname sigpanic + func sigpanic() { + ``` + +- References (`textDocument/references`): finds all references to the + symbol under the cursor, in both Go and assembly files within the + same package. + +- Hover (`textDocument/hover`): reports the signature and doc comment + of the symbol's Go declaration. + +- DocumentHighlight (`textDocument/documentHighlight`): highlights all + occurrences of the symbol, control label, or machine register under + the cursor. Labels and registers are scoped to the enclosing TEXT + function, and occurrences are classified as reads or writes. + Register highlighting requires the file name to carry a GOARCH + suffix (e.g. `foo_amd64.s`) and currently supports x86 (amd64, 386) + and arm64. See also issue https://go.dev/issue/71754, which tracks the development of LSP -features in Go assembly files. \ No newline at end of file +features in Go assembly files.
diff --git a/gopls/doc/inlayHints.md b/gopls/doc/inlayHints.md index 4b3b38e..74207a1 100644 --- a/gopls/doc/inlayHints.md +++ b/gopls/doc/inlayHints.md
@@ -81,10 +81,18 @@ implicitly ignored. To suppress the hint, write an actual comment containing -"ignore error" following the call statement, or explicitly -assign the result to a blank variable. A handful of common -functions such as `fmt.Println` are excluded from the -check. +one of the following strings: +``` +ignore error +discard error +can't fail +cannot fail +``` +following the call statement, or explicitly assign the +result to a blank variable. + +A handful of common functions such as `fmt.Println` are +excluded from the check. **Disabled by default. Enable it by setting `"hints": {"ignoredError": true}`.**
diff --git a/gopls/doc/release/v0.23.0.md b/gopls/doc/release/v0.23.0.md index ffe2d81..f36b15b 100644 --- a/gopls/doc/release/v0.23.0.md +++ b/gopls/doc/release/v0.23.0.md
@@ -1,5 +1,5 @@ --- -title: "Gopls release v0.23.0 (expected July 2026)" +title: "Gopls release v0.23.0 (July 2026)" --- In this release:
diff --git a/gopls/doc/release/v0.24.0.md b/gopls/doc/release/v0.24.0.md index aaf6a1c..f4d0db3 100644 --- a/gopls/doc/release/v0.24.0.md +++ b/gopls/doc/release/v0.24.0.md
@@ -22,12 +22,47 @@ for lower steady state memory usage; but it may cause GC thrashing if set too low. +The gopls server now enforces +[`SemanticTokensClientCapabilities`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#semanticTokensClientCapabilities) +fields: `TokenTypes` and `TokenModifiers`. The semantic token types and +modifiers gopls returns are now limited to what these lists contain, so +it's important that clients fully list their semantic token +capabilities. The gopls UI settings +[`semanticTokenTypes`](https://go.dev/gopls/settings#semantictokentypes-mapstringbool) +and +[`semanticTokenModifiers`](https://go.dev/gopls/settings#semantictokenmodifiers-mapstringbool) +can still be used by users to further restrict these lists. + ## Web-based features ## Editing features +Gopls now supports the Hover request in Go assembly files: hovering +over a symbol reports the signature and doc comment of its Go +declaration. + +Gopls now supports the `textDocument/documentHighlight` request in Go +assembly files: all occurrences of the symbol, label, or machine +register under the cursor are highlighted within the enclosing TEXT +function, with definitions classified as writes and references as +reads. + ## Analysis features <!-- TODO Gopls is now using staticcheck [v0.8.0-rc1](https://github.com/dominikh/go-tools/releases/tag/2026.2rc1). --> +### `ptrtoerror` analyzer + +This new analyzer reports inconsistent use of a named type `E` and its +pointer type `*E` as an error. +<!-- #80159 --> + +### `slicesclip` modernizer + +The new `slicesclip` modernizer suggests replacing a full slice +expression of the form `x[:len(x):len(x)]`, which clips the capacity +of a slice to its length, with the simpler and more readable +`slices.Clip(x)`, added in Go 1.21. +<!-- #80438 --> + ## Code transformation features
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 8e0602c..63415d9 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md
@@ -301,6 +301,16 @@ Default: `false`. +<a id='moveDeclaration'></a> +### `moveDeclaration bool` + +**This setting is experimental and may be deleted.** + +moveDeclaration enables producing Move Declaration codeactions. The implementation +is unfinished so we use this setting to gate its use. + +Default: `false`. + <a id='completion'></a> ## Completion
diff --git a/gopls/go.mod b/gopls/go.mod index 9bf9fad..8c3441c 100644 --- a/gopls/go.mod +++ b/gopls/go.mod
@@ -8,14 +8,14 @@ github.com/google/go-cmp v0.7.0 github.com/google/jsonschema-go v0.4.3 github.com/jba/templatecheck v0.7.1 - github.com/modelcontextprotocol/go-sdk v1.6.0 - golang.org/x/mod v0.37.0 - golang.org/x/net v0.56.0 - golang.org/x/sync v0.21.0 - golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 - golang.org/x/text v0.38.0 - golang.org/x/tools v0.46.0 - golang.org/x/vuln v1.4.0 + github.com/modelcontextprotocol/go-sdk v1.6.1 + golang.org/x/mod v0.39.0 + golang.org/x/net v0.58.0 + golang.org/x/sync v0.22.0 + golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 + golang.org/x/text v0.41.0 + golang.org/x/tools v0.48.0 + golang.org/x/vuln v1.6.0 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.8.0-rc.1 mvdan.cc/gofumpt v0.10.0 @@ -30,9 +30,9 @@ github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/encoding v0.5.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect + golang.org/x/exp/typeparams v0.0.0-20260812173653-3d80eb74bc5b // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.46.0 // indirect + golang.org/x/sys v0.47.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect )
diff --git a/gopls/go.sum b/gopls/go.sum index 78be546..6fb0b9d 100644 --- a/gopls/go.sum +++ b/gopls/go.sum
@@ -25,8 +25,8 @@ github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/modelcontextprotocol/go-sdk v1.6.0 h1:PPLS3kn7WtOEnR+Af4X5H96SG0qSab8R/ZQT/HkhPkY= -github.com/modelcontextprotocol/go-sdk v1.6.0/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= +github.com/modelcontextprotocol/go-sdk v1.6.1 h1:0zOSupjKUxPKSocPT1Wtago+mUHU2/uZ4xSOY0FGReU= +github.com/modelcontextprotocol/go-sdk v1.6.1/go.mod h1:kzm3kzFL1/+AziGOE0nUs3gvPoNxMCvkxokMkuFapXQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= @@ -50,8 +50,10 @@ golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 h1:GTD/WuaexTazIG/SxLOz4rEKZPDVilmVVC2nz4xhwfE= -golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/exp/typeparams v0.0.0-20260812173653-3d80eb74bc5b h1:F5AefKrjGj4r4Ht9ov1buP2PTyVb65dyf4bmdVKleV0= +golang.org/x/exp/typeparams v0.0.0-20260812173653-3d80eb74bc5b/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= @@ -65,8 +67,10 @@ golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= @@ -81,8 +85,10 @@ golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= @@ -91,8 +97,9 @@ golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -109,10 +116,11 @@ golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= -golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 h1:ZUSxONxc981v7AW7QUg+I9WwZzSTTJ019ENBYr5pV/Q= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5/go.mod h1:LVehoXe41cL5SCVQilsV7Gg6BNG+Js6P9PhSbYTIUkQ= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= @@ -128,6 +136,7 @@ golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -143,14 +152,16 @@ golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= -golang.org/x/vuln v1.4.0 h1:FpmTZiV4PyqY3lFfuCkz1JftEXb/+8M2NEkjJM5TF4g= -golang.org/x/vuln v1.4.0/go.mod h1:FJ7XyKs83nAdxQ7PMsia2PoynwZMJ/QajXVMBBIgFe8= +golang.org/x/vuln v1.6.0 h1:FeMO9Rm/HwyduOztbvKcOw+zvDEPr4I4aQNSfevFcKY= +golang.org/x/vuln v1.6.0/go.mod h1:bWlG2493/sjR7ksvicBgMrznH3eYQEyK8ifUYBrqUbg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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=
diff --git a/gopls/internal/analysis/fillswitch/fillswitch.go b/gopls/internal/analysis/fillswitch/fillswitch.go index 7b1a7e8..f72efed 100644 --- a/gopls/internal/analysis/fillswitch/fillswitch.go +++ b/gopls/internal/analysis/fillswitch/fillswitch.go
@@ -12,6 +12,7 @@ "go/types" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" ) @@ -21,6 +22,7 @@ // If either start or end is invalid, the entire file is inspected. func Diagnose(f *ast.File, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { var diags []analysis.Diagnostic + qual := typesinternal.FileQualifier(f, pkg) ast.Inspect(f, func(n ast.Node) bool { if n == nil { return true // pop @@ -32,9 +34,9 @@ var fix *analysis.SuggestedFix switch n := n.(type) { case *ast.SwitchStmt: - fix = suggestedFixSwitch(n, pkg, info) + fix = suggestedFixSwitch(f, n, pkg, info, qual) case *ast.TypeSwitchStmt: - fix = suggestedFixTypeSwitch(n, pkg, info) + fix = suggestedFixTypeSwitch(f, n, pkg, info, qual) } if fix != nil { diags = append(diags, analysis.Diagnostic{ @@ -50,7 +52,7 @@ return diags } -func suggestedFixTypeSwitch(stmt *ast.TypeSwitchStmt, pkg *types.Package, info *types.Info) *analysis.SuggestedFix { +func suggestedFixTypeSwitch(f *ast.File, stmt *ast.TypeSwitchStmt, pkg *types.Package, info *types.Info, qual types.Qualifier) *analysis.SuggestedFix { if hasDefaultCase(stmt.Body) { return nil } @@ -103,9 +105,10 @@ } if p := key.named.Obj().Pkg(); p != pkg { - // TODO: use the correct package name when the import is renamed - buf.WriteString(p.Name()) - buf.WriteByte('.') + if name := qual(p); name != "" { // not a dot import + buf.WriteString(name) + buf.WriteByte('.') + } } buf.WriteString(key.named.Obj().Name()) buf.WriteString(":\n") @@ -116,26 +119,29 @@ return nil } + rBracePos := stmt.Body.Rbrace + var edits []analysis.TextEdit switch assign := stmt.Assign.(type) { case *ast.AssignStmt: - addDefaultCase(&buf, namedType, assign.Lhs[0]) + edits = addDefaultCase(f, info, &buf, namedType, assign.Lhs[0], rBracePos) case *ast.ExprStmt: if assert, ok := assign.X.(*ast.TypeAssertExpr); ok { - addDefaultCase(&buf, namedType, assert.X) + edits = addDefaultCase(f, info, &buf, namedType, assert.X, rBracePos) } } + edits = append(edits, analysis.TextEdit{ + Pos: rBracePos, + End: rBracePos, + NewText: buf.Bytes(), + }) return &analysis.SuggestedFix{ - Message: "Add cases for " + types.TypeString(namedType, typesinternal.NameRelativeTo(pkg)), - TextEdits: []analysis.TextEdit{{ - Pos: stmt.End() - token.Pos(len("}")), - End: stmt.End() - token.Pos(len("}")), - NewText: buf.Bytes(), - }}, + Message: "Add cases for " + types.TypeString(namedType, qual), + TextEdits: edits, } } -func suggestedFixSwitch(stmt *ast.SwitchStmt, pkg *types.Package, info *types.Info) *analysis.SuggestedFix { +func suggestedFixSwitch(f *ast.File, stmt *ast.SwitchStmt, pkg *types.Package, info *types.Info, qual types.Qualifier) *analysis.SuggestedFix { if hasDefaultCase(stmt.Body) { return nil } @@ -162,8 +168,10 @@ buf.WriteString("case ") if c.Pkg() != pkg { - buf.WriteString(c.Pkg().Name()) - buf.WriteByte('.') + if name := qual(c.Pkg()); name != "" { // not a dot import + buf.WriteString(name) + buf.WriteByte('.') + } } buf.WriteString(c.Name()) buf.WriteString(":\n") @@ -174,19 +182,23 @@ return nil } - addDefaultCase(&buf, namedType, stmt.Tag) + rBracePos := stmt.Body.Rbrace + edits := addDefaultCase(f, info, &buf, namedType, stmt.Tag, rBracePos) + edits = append(edits, analysis.TextEdit{ + Pos: rBracePos, + End: rBracePos, + NewText: buf.Bytes(), + }) return &analysis.SuggestedFix{ - Message: "Add cases for " + types.TypeString(namedType, typesinternal.NameRelativeTo(pkg)), - TextEdits: []analysis.TextEdit{{ - Pos: stmt.End() - token.Pos(len("}")), - End: stmt.End() - token.Pos(len("}")), - NewText: buf.Bytes(), - }}, + Message: "Add cases for " + types.TypeString(namedType, qual), + TextEdits: edits, } } -func addDefaultCase(buf *bytes.Buffer, named *types.Named, expr ast.Expr) { +// addDefaultCase writes a default switch case to buf containing an +// "unexpected" panic message. It returns edits for any added imports. +func addDefaultCase(f *ast.File, info *types.Info, buf *bytes.Buffer, named *types.Named, expr ast.Expr, rBracePos token.Pos) (importEdits []analysis.TextEdit) { var dottedBuf bytes.Buffer // writeDotted emits a dotted path a.b.c. var writeDotted func(e ast.Expr) bool @@ -211,11 +223,15 @@ if writeDotted(expr) { // Switch tag expression is a dotted path. // It is safe to re-evaluate it in the default case. + var fmtPrefix string + fmtPrefix, importEdits = refactor.AddImport(info, f, "fmt", "fmt", "Sprintf", rBracePos) format := fmt.Sprintf("unexpected %s: %%#v", typeName) - fmt.Fprintf(buf, "\t\tpanic(fmt.Sprintf(%q, %s))\n\t", format, dottedBuf.String()) + fmt.Fprintf(buf, "\t\tpanic(%sSprintf(%q, %s))\n\t", fmtPrefix, format, dottedBuf.String()) + return importEdits } else { // Emit simpler message, without re-evaluating tag expression. fmt.Fprintf(buf, "\t\tpanic(%q)\n\t", "unexpected "+typeName) + return nil } }
diff --git a/gopls/internal/analysis/fillswitch/testdata/src/a/a.go b/gopls/internal/analysis/fillswitch/testdata/src/a/a.go index 6fa33ec..a87ad42 100644 --- a/gopls/internal/analysis/fillswitch/testdata/src/a/a.go +++ b/gopls/internal/analysis/fillswitch/testdata/src/a/a.go
@@ -35,7 +35,7 @@ } var b altb.TypeB - switch b { // want `Add cases for b.TypeB` + switch b { // want `Add cases for altb.TypeB` case altb.TypeBOne: } }
diff --git a/gopls/internal/analysis/modernize/cmd/modernize/main.go b/gopls/internal/analysis/modernize/cmd/modernize/main.go deleted file mode 100644 index 0f1f5e6..0000000 --- a/gopls/internal/analysis/modernize/cmd/modernize/main.go +++ /dev/null
@@ -1,21 +0,0 @@ -// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// The modernize command suggests (or, with -fix, applies) fixes that -// clarify Go code by using more modern features. -// -// See [golang.org/x/tools/go/analysis/passes/modernize] for details. -// -// Deprecated: use 'go run -// golang.org/x/tools/go/analysis/passes/modernize/cmd/modernize' instead. In -// due course the modernizer suite will be accessed through "go fix"; -// see https://go.dev/issue/71859. -package main - -import ( - "golang.org/x/tools/go/analysis/multichecker" - "golang.org/x/tools/go/analysis/passes/modernize" -) - -func main() { multichecker.Main(modernize.Suite...) }
diff --git a/gopls/internal/analysis/ptrtoerror/conv.go b/gopls/internal/analysis/ptrtoerror/conv.go new file mode 100644 index 0000000..c3b1b9b --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/conv.go
@@ -0,0 +1,278 @@ +// Copyright 2026 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 ptrtoerror + +import ( + "go/ast" + "go/types" + "iter" + + "golang.org/x/tools/go/ast/inspector" +) + +// A conversion represents a syntax node at which an assignment or explicit +// conversion occurs, along with the source and destination types. +type conversion struct { + tLHS, tRHS types.Type + expr ast.Expr +} + +// conversions returns an iterator over all (LHS, RHS) assignment and +// conversion pairs in the package, where LHS is the destination type +// (types.Type) and RHS is the expression (ast.Expr). Some of these +// may involve a widening. +// +// TODO(adonovan): eliminate when https://go.dev/issue/70638 is done. +func conversions(root inspector.Cursor, info *types.Info) iter.Seq[conversion] { + return func(yield func(conversion) bool) { + // nodes where assignability conversions occur + nodeFilter := []ast.Node{ + (*ast.AssignStmt)(nil), + (*ast.ValueSpec)(nil), + (*ast.CallExpr)(nil), + (*ast.ReturnStmt)(nil), + (*ast.CompositeLit)(nil), + (*ast.SendStmt)(nil), + (*ast.TypeAssertExpr)(nil), + (*ast.TypeSwitchStmt)(nil), + } + + for c := range root.Preorder(nodeFilter...) { + switch n := c.Node().(type) { + case *ast.AssignStmt: + if len(n.Lhs) == len(n.Rhs) { + // simple or tuple assignment + for i, lhs := range n.Lhs { + if tLHS := info.TypeOf(lhs); tLHS != nil && + !yield(conversion{tLHS, info.TypeOf(n.Rhs[i]), n.Rhs[i]}) { + return + } + } + } else if len(n.Lhs) > 1 && len(n.Rhs) == 1 { + // spread assignment + if tuple, ok := info.TypeOf(n.Rhs[0]).(*types.Tuple); ok && tuple.Len() == len(n.Lhs) { + for i, lhs := range n.Lhs { + if tLHS := info.TypeOf(lhs); tLHS != nil && + !yield(conversion{info.TypeOf(lhs), tuple.At(i).Type(), n.Rhs[0]}) { + return + } + } + } + } + case *ast.ValueSpec: + if len(n.Values) > 0 { + if len(n.Names) == len(n.Values) { + // simple or tuple initialization + for i := range n.Names { + if obj := info.ObjectOf(n.Names[i]); obj != nil { + if !yield(conversion{obj.Type(), info.TypeOf(n.Values[i]), n.Values[i]}) { + return + } + } + } + } else if len(n.Names) > 1 && len(n.Values) == 1 { + // spread initialization + if tuple, ok := info.TypeOf(n.Values[0]).(*types.Tuple); ok && tuple.Len() == len(n.Names) { + for i := range n.Names { + if obj := info.ObjectOf(n.Names[i]); obj != nil { + if !yield(conversion{obj.Type(), tuple.At(i).Type(), n.Values[0]}) { + return + } + } + } + } + } + } + case *ast.CallExpr: + tv := info.Types[n.Fun] + + // explicit conversion? + if tv.IsType() { + if !yield(conversion{tv.Type, info.TypeOf(n.Args[0]), n.Args[0]}) { + return + } + break + } + + sig, ok := tv.Type.Underlying().(*types.Signature) + if !ok { + break + } + + // spread call? + if len(n.Args) == 1 && sig.Params().Len() > 1 { + if tuple, ok := info.TypeOf(n.Args[0]).(*types.Tuple); ok { + for i := 0; i < sig.Params().Len() && i < tuple.Len(); i++ { + if !yield(conversion{sig.Params().At(i).Type(), tuple.At(i).Type(), n.Args[0]}) { + return + } + } + } + break + } + + // argument -> parameter assignment + last := sig.Params().Len() - 1 + for i, arg := range n.Args { + if sig.Variadic() && i >= last { + // variadic callee + if slice, ok := sig.Params().At(last).Type().(*types.Slice); ok { + var t types.Type = slice // f(args...) call of func(...T) + if !n.Ellipsis.IsValid() { + t = slice.Elem() // f(a, b, c) call of func(...T) + } + if !yield(conversion{t, info.TypeOf(arg), arg}) { + return + } + } + } else { + if !yield(conversion{sig.Params().At(i).Type(), info.TypeOf(arg), arg}) { + return + } + } + } + + case *ast.ReturnStmt: + // Handle return statements by matching results against enclosing function signatures. + var sig *types.Signature + for anc := range c.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { + if fd, ok := anc.Node().(*ast.FuncDecl); ok { + if obj := info.Defs[fd.Name]; obj != nil { + sig = obj.Type().Underlying().(*types.Signature) + } + break + } + if fl, ok := anc.Node().(*ast.FuncLit); ok { + sig = info.TypeOf(fl).Underlying().(*types.Signature) + break + } + } + if sig == nil { + break // can't happen? + } + + // spread return? + if len(n.Results) == 1 && sig.Results().Len() > 1 { + if tuple, ok := info.TypeOf(n.Results[0]).(*types.Tuple); ok { + for i := 0; i < sig.Results().Len() && i < tuple.Len(); i++ { + if !yield(conversion{sig.Results().At(i).Type(), tuple.At(i).Type(), n.Results[0]}) { + return + } + } + } + break + } + + // simple or tuple return + if len(n.Results) == sig.Results().Len() { + for i, res := range n.Results { + if !yield(conversion{sig.Results().At(i).Type(), info.TypeOf(res), res}) { + return + } + } + } + + case *ast.CompositeLit: + // element assignments in slice, array, map, and struct literals. + switch t := info.TypeOf(n).Underlying().(type) { + case *types.Slice, *types.Array: + for _, elt := range n.Elts { + if kv, ok := elt.(*ast.KeyValueExpr); ok { + elt = kv.Value + } + if !yield(conversion{t.(interface{ Elem() types.Type }).Elem(), info.TypeOf(elt), elt}) { + return + } + } + + case *types.Map: + for _, elt := range n.Elts { + kv := elt.(*ast.KeyValueExpr) + if !yield(conversion{t.Key(), info.TypeOf(kv.Key), kv.Key}) || + !yield(conversion{t.Elem(), info.TypeOf(kv.Value), kv.Value}) { + return + } + } + + case *types.Struct: + for i, elt := range n.Elts { + if kv, ok := elt.(*ast.KeyValueExpr); ok { + if id, ok := kv.Key.(*ast.Ident); ok { + var fieldType types.Type + if obj := info.Uses[id]; obj != nil { + fieldType = obj.Type() + } else { + for field := range t.Fields() { + if field.Name() == id.Name { + fieldType = field.Type() + break + } + } + } + if !yield(conversion{fieldType, info.TypeOf(kv.Value), kv.Value}) { + return + } + } + } else if i < t.NumFields() { + if !yield(conversion{t.Field(i).Type(), info.TypeOf(elt), elt}) { + return + } + } + } + } + + case *ast.SendStmt: + // channel send + if chanType, ok := info.TypeOf(n.Chan).Underlying().(*types.Chan); ok { + if !yield(conversion{chanType.Elem(), info.TypeOf(n.Value), n.Value}) { + return + } + } + + case *ast.TypeAssertExpr: + // I(x).(E) acts like a pseudoconversion from E to E. + if n.Type != nil && // (not beneath type switch) + !yield(conversion{info.TypeOf(n.X), info.TypeOf(n.Type), n.Type}) { + return + } + + case *ast.TypeSwitchStmt: + // switch I(x).(type) { case E: } depends on E being assignable to I. + // Report the (pseudo)conversion of type (not term) E to I. + var assert *ast.TypeAssertExpr + switch assign := n.Assign.(type) { + case *ast.ExprStmt: + assert = assign.X.(*ast.TypeAssertExpr) + case *ast.AssignStmt: + assert = assign.Rhs[0].(*ast.TypeAssertExpr) + } + for _, cc := range n.Body.List { + for _, typ := range cc.(*ast.CaseClause).List { + if !yield(conversion{info.TypeOf(assert.X), info.TypeOf(typ), typ}) { + return + } + } + } + } + } + + // Yield conversions from type parameter instantiations + // e.g. errors.AsType[*E](err) + for id, inst := range info.Instances { + t := info.ObjectOf(id).Type() + type hasTypeParams interface{ TypeParams() *types.TypeParamList } // = Signature, Named, Alias + if t, ok := t.(hasTypeParams); ok { + tparams := t.TypeParams() + for i := 0; i < tparams.Len(); i++ { + tparam := tparams.At(i) + targ := inst.TypeArgs.At(i) + if !yield(conversion{tparam.Constraint(), targ, id}) { + return + } + } + } + } + } +}
diff --git a/gopls/internal/analysis/ptrtoerror/doc.go b/gopls/internal/analysis/ptrtoerror/doc.go new file mode 100644 index 0000000..85952f3 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/doc.go
@@ -0,0 +1,63 @@ +// Copyright 2026 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 ptrtoerror defines an analyzer that detects inconsistent +// conversions of concrete error types to the error interface. +// +// # Analyzer ptrtoerror +// +// ptrtoerror: detect inconsistent conversions of concrete types to error +// +// The ptrtoerror analyzer detects when a concrete type E is converted +// to the error interface inconsistently, both as a value of type E +// and as a pointer of type *E. Such inconsistency defeats attempts by +// client code to test for specific error types using type assertions +// or library functions such as [errors.As] and [errors.Is]. +// +// The analyzer also detects when both E and *E implement error but +// neither of those types is converted to error within the defining +// package, leaving the intended error form (E or *E) ambiguous. This +// diagnostic offers two alternative fixes to add declarations that +// make the intent explicit. +// +// # Examples +// +// In this example, both MyError and *MyError implement error, and +// both types are used as errors: +// +// type MyError struct{ Msg string } // error: type MyError is converted to error both as a value and as a pointer +// +// func (MyError) Error() string { return "error" } +// +// func foo() error { return MyError{"foo"} } // "conversion of MyError (value) to error" here +// func bar() error { return &MyError{"bar"} } // "conversion of *MyError (pointer) to error" here +// +// To fix the issue, adopt a single conversion form consistently: +// +// func foo() error { return &MyError{"foo"} } // pointer conversion (*E) +// func bar() error { return &MyError{"bar"} } // pointer conversion (*E) +// +// In this example, again both CustomError and *CustomError implement +// error, but neither type is used as an error: +// +// type CustomError struct{ error } // "both CustomError and *CustomError implement the error interface, making the intent ambiguous" +// +// func (e *CustomError) Unwrap() error ( return e.error } +// +// To resolve the ambiguity, apply one of the two suggested fixes to +// add a declaration that makes the intent explicit: +// +// var _ error = *new(CustomError) // (declares that CustomError is the intended form) +// +// or: +// +// var _ error = (*CustomError)(nil) // (declares that *CustomError is the intended form) +// +// In this example, the presence of the Unwrap method is a clue that +// *CustomError is the preferred form. An alternative fix would be to +// declare an explicit Error method so that CustomError is no longer +// assignable to error: +// +// func (err *CustomError) Error() string ( return err.error.Error() } +package ptrtoerror
diff --git a/gopls/internal/analysis/ptrtoerror/main.go b/gopls/internal/analysis/ptrtoerror/main.go new file mode 100644 index 0000000..edf8d17 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/main.go
@@ -0,0 +1,16 @@ +// Copyright 2026 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 + +// The ptrtoerror command applies the golang.org/x/tools/gopls/internal/analysis/ptrtoerror +// analysis to the specified packages of Go source code. +package main + +import ( + "golang.org/x/tools/go/analysis/singlechecker" + "golang.org/x/tools/gopls/internal/analysis/ptrtoerror" +) + +func main() { singlechecker.Main(ptrtoerror.Analyzer) }
diff --git a/gopls/internal/analysis/ptrtoerror/ptrtoerror.go b/gopls/internal/analysis/ptrtoerror/ptrtoerror.go new file mode 100644 index 0000000..f35bd81 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/ptrtoerror.go
@@ -0,0 +1,241 @@ +// Copyright 2026 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 ptrtoerror + +import ( + _ "embed" + "fmt" + "go/ast" + "go/token" + "go/types" + "path/filepath" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/analysis/analyzerutil" +) + +//go:embed doc.go +var doc string + +// Analyzer detects inconsistent conversions of concrete types to error. +var Analyzer = &analysis.Analyzer{ + Name: "ptrtoerror", + Doc: analyzerutil.MustExtractDoc(doc, "ptrtoerror"), + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/ptrtoerror", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + FactTypes: []analysis.Fact{(*isErrorFact)(nil)}, + Run: run, +} + +// run executes the ptrtoerror analysis pass. +func run(pass *analysis.Pass) (any, error) { + var ( + inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + info = pass.TypesInfo + ) + + type localType struct{ val, ptr []conversion } // all conversions from local type E, *E to error + var ( + localTypes = make(map[*types.TypeName]*localType) // error conversion info about types from this package + importedTypes = make(map[*types.TypeName]*isErrorFact) // error conversion info about imported types (nil => unknown) + ) + for conv := range conversions(inspect.Root(), info) { + if conv.tLHS == nil { + panic("nil LHS type") + } + if conv.tRHS == nil { + panic("nil RHS type") + } + if conv.expr == nil { + panic("nil Expr") + } + + if !types.IsInterface(conv.tLHS) || + !types.AssignableTo(conv.tLHS, builtinError.Type()) { + continue + } + + // E or *E? + t := conv.tRHS + ptr, isPtr := types.Unalias(t).(*types.Pointer) + if isPtr { + t = ptr.Elem() + } + named, ok := types.Unalias(t).(*types.Named) + if !ok || types.IsInterface(named) { + continue // not a concrete named type + } + tname := named.Obj() + + // local type? + if tname.Pkg() == pass.Pkg { + ltinfo, ok := localTypes[tname] + if !ok { + ltinfo = new(localType) + localTypes[tname] = ltinfo + } + // Save it for the second pass. + list := cond(isPtr, <info.ptr, <info.val) + *list = append(*list, conv) + continue + } + + // imported type + f, ok := importedTypes[tname] + if !ok { + var fact isErrorFact + if pass.ImportObjectFact(tname, &fact) { + f = &fact + } + importedTypes[tname] = f // memoize even if nil + } + if f != nil && f.Pointer != isPtr { + var message string + if isPtr { + message = fmt.Sprintf("conversion of *%s to error, but package %q uses %s (sans pointer) as an error (e.g. at %s)", + types.TypeString(named, (*types.Package).Name), + tname.Pkg().Path(), + tname.Name(), + f.Where) + } else { + message = fmt.Sprintf("conversion of %s to error, but package %q uses pointer *%s as an error (e.g. at %s)", + types.TypeString(named, (*types.Package).Name), + tname.Pkg().Path(), + tname.Name(), + f.Where) + } + pass.Report(analysis.Diagnostic{ + Pos: conv.expr.Pos(), + End: conv.expr.End(), + Message: message, + }) + } + } + + // Second pass over conversions of local types. + for tname, ltinfo := range localTypes { + isPtr := ltinfo.ptr != nil + + // If used as both E and *E, + // report all conversions as ambiguous. + if isPtr && ltinfo.val != nil { + conflicts := func(convs []conversion, other conversion, otherKind string) { + for _, conv := range convs { + pass.Report(analysis.Diagnostic{ + Pos: conv.expr.Pos(), + End: conv.expr.End(), + Message: fmt.Sprintf("%s is converted to error both as a value and as a pointer", tname.Name()), + Related: []analysis.RelatedInformation{{ + Pos: other.expr.Pos(), + End: other.expr.End(), + Message: fmt.Sprintf("converted as a %s here", otherKind), + }}, + }) + } + } + conflicts(ltinfo.ptr, ltinfo.val[0], "value") + conflicts(ltinfo.val, ltinfo.ptr[0], "pointer") + continue + } + + // E or *E is used consistently. + // Export a fact. + if tname.Exported() { + var first conversion + if isPtr { + first = ltinfo.ptr[0] + } else { + first = ltinfo.val[0] + } + posn := safetoken.StartPosition(pass.Fset, first.expr.Pos()) + where := fmt.Sprintf("%s:%d:%d", filepath.Base(posn.Filename), posn.Line, posn.Column) + pass.ExportObjectFact(tname, &isErrorFact{ + Pointer: isPtr, + Where: where, + }) + } + } + + // Report a diagnostic for each local named type E such that E + // and *E implement error yet neither type is converted to error + // (which would indicate intent). + for curTypeSpec := range inspect.Root().Preorder((*ast.TypeSpec)(nil)) { + tspec := curTypeSpec.Node().(*ast.TypeSpec) + if tspec.Assign.IsValid() { + continue // ignore aliases (need more thorough treatment) + } + tname := info.Defs[tspec.Name].(*types.TypeName) + if _, ok := localTypes[tname]; ok { + continue // E or *E was converted to error + } + if types.IsInterface(tname.Type()) || !types.AssignableTo(tname.Type(), builtinError.Type()) { + continue + } + + // Both E and (implicitly) *E implement error + // but neither conversion was used in this package. + + // Suggest a fix to insert 'var _ error = ...' after + // the enclosing type decl to declare the intent. + var fixes []analysis.SuggestedFix + if curIdent, ok := inspect.Root().FindByPos(tname.Pos(), tname.Pos()); ok { + for curDecl := range curIdent.Enclosing((*ast.GenDecl)(nil)) { + pos := curDecl.Node().End() + fixes = []analysis.SuggestedFix{ + { + Message: fmt.Sprintf(`Declare that %s implements error`, tname.Name()), + TextEdits: []analysis.TextEdit{{ + Pos: pos, + End: pos, + NewText: fmt.Appendf(nil, "\n\nvar _ error = *new(%s)\n", tname.Name()), + }}, + }, + { + Message: fmt.Sprintf(`Declare that *%s implements error`, tname.Name()), + TextEdits: []analysis.TextEdit{{ + Pos: pos, + End: pos, + NewText: fmt.Appendf(nil, "\n\nvar _ error = (*%s)(nil)\n", tname.Name()), + }}, + }, + } + break + } + } + pass.Report(analysis.Diagnostic{ + Pos: tname.Pos(), + End: tname.Pos() + token.Pos(len(tname.Name())), + Message: fmt.Sprintf("both %[1]s and *%[1]s implement the error interface, making the intent ambiguous", tname.Name()), + SuggestedFixes: fixes, + }) + } + + return nil, nil +} + +// An isErrorFact associated with an exported type E indicates that +// either E or *E (depending on the value of Pointer) implements error. +type isErrorFact struct { + Pointer bool // the preferred error type is *E, not E + Where string // a hint to where the fact was established of the form "foo.go:123:45" +} + +func (f *isErrorFact) AFact() {} +func (f *isErrorFact) String() string { return cond(f.Pointer, "*E", "E") } + +// -- helpers -- + +var builtinError = types.Universe.Lookup("error") + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +}
diff --git a/gopls/internal/analysis/ptrtoerror/ptrtoerror_test.go b/gopls/internal/analysis/ptrtoerror/ptrtoerror_test.go new file mode 100644 index 0000000..a5dd9b5 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/ptrtoerror_test.go
@@ -0,0 +1,16 @@ +// Copyright 2026 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 ptrtoerror_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/gopls/internal/analysis/ptrtoerror" +) + +func Test(t *testing.T) { + analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), ptrtoerror.Analyzer, "a", "b", "c", "d") +}
diff --git a/gopls/internal/analysis/ptrtoerror/testdata/src/a/a.go b/gopls/internal/analysis/ptrtoerror/testdata/src/a/a.go new file mode 100644 index 0000000..59db6d0 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/testdata/src/a/a.go
@@ -0,0 +1,73 @@ +// Copyright 2026 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 a + +import "io/fs" + +type ConflictErr struct { + error +} + +func _(c ConflictErr) { + var _ error = c // want "ConflictErr is converted to error both as a value and as a pointer" + var _ error = &c // want "ConflictErr is converted to error both as a value and as a pointer" +} + +type AmbiguousErr struct { // want `both AmbiguousErr and \*AmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +type unexportedAmbiguousErr struct { // want `both unexportedAmbiguousErr and \*unexportedAmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +type PtrOnlyErr struct { // want PtrOnlyErr:`\*E` + msg string +} + +func (p *PtrOnlyErr) Error() string { return p.msg } + +func _(p *PtrOnlyErr) { + var _ error = p +} + +type ValErr struct { // want ValErr:`E` + msg string +} + +func (v ValErr) Error() string { return v.msg } + +func _(v ValErr) { + var _ error = v +} + +type PtrConvErr struct { // want PtrConvErr:`\*E` + error +} + +func _(p *PtrConvErr) { + var _ error = p +} + +type unexportedPtrConvErr struct { + error +} + +func _(p *unexportedPtrConvErr) { + var _ error = p +} + +func test() { + var pe fs.PathError + var _ error = &pe // ok + + var _ error = &struct{ error }{} // OK (unnamed type) + + type local struct{ error } + var _ error = local{} // want `local is converted to error both as a value and as a pointer` + var _ error = &local{} // want `local is converted to error both as a value and as a pointer` + + type localAmbiguousErr struct{ error } // want `both localAmbiguousErr and \*localAmbiguousErr implement the error interface, making the intent ambiguous` +}
diff --git a/gopls/internal/analysis/ptrtoerror/testdata/src/a/a.go.golden b/gopls/internal/analysis/ptrtoerror/testdata/src/a/a.go.golden new file mode 100644 index 0000000..33fb6a3 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/testdata/src/a/a.go.golden
@@ -0,0 +1,304 @@ +-- Declare that AmbiguousErr implements error -- +// Copyright 2026 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 a + +import "io/fs" + +type ConflictErr struct { + error +} + +func _(c ConflictErr) { + var _ error = c // want "ConflictErr is converted to error both as a value and as a pointer" + var _ error = &c // want "ConflictErr is converted to error both as a value and as a pointer" +} + +type AmbiguousErr struct { // want `both AmbiguousErr and \*AmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +var _ error = *new(AmbiguousErr) + +type unexportedAmbiguousErr struct { // want `both unexportedAmbiguousErr and \*unexportedAmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +type PtrOnlyErr struct { // want PtrOnlyErr:`\*E` + msg string +} + +func (p *PtrOnlyErr) Error() string { return p.msg } + +func _(p *PtrOnlyErr) { + var _ error = p +} + +type ValErr struct { // want ValErr:`E` + msg string +} + +func (v ValErr) Error() string { return v.msg } + +func _(v ValErr) { + var _ error = v +} + +type PtrConvErr struct { // want PtrConvErr:`\*E` + error +} + +func _(p *PtrConvErr) { + var _ error = p +} + +type unexportedPtrConvErr struct { + error +} + +func _(p *unexportedPtrConvErr) { + var _ error = p +} + +func test() { + var pe fs.PathError + var _ error = &pe // ok + + var _ error = &struct{ error }{} // OK (unnamed type) + + type local struct{ error } + var _ error = local{} // want `local is converted to error both as a value and as a pointer` + var _ error = &local{} // want `local is converted to error both as a value and as a pointer` + + type localAmbiguousErr struct{ error } // want `both localAmbiguousErr and \*localAmbiguousErr implement the error interface, making the intent ambiguous` +} +-- Declare that *AmbiguousErr implements error -- +// Copyright 2026 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 a + +import "io/fs" + +type ConflictErr struct { + error +} + +func _(c ConflictErr) { + var _ error = c // want "ConflictErr is converted to error both as a value and as a pointer" + var _ error = &c // want "ConflictErr is converted to error both as a value and as a pointer" +} + +type AmbiguousErr struct { // want `both AmbiguousErr and \*AmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +var _ error = (*AmbiguousErr)(nil) + +type unexportedAmbiguousErr struct { // want `both unexportedAmbiguousErr and \*unexportedAmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +type PtrOnlyErr struct { // want PtrOnlyErr:`\*E` + msg string +} + +func (p *PtrOnlyErr) Error() string { return p.msg } + +func _(p *PtrOnlyErr) { + var _ error = p +} + +type ValErr struct { // want ValErr:`E` + msg string +} + +func (v ValErr) Error() string { return v.msg } + +func _(v ValErr) { + var _ error = v +} + +type PtrConvErr struct { // want PtrConvErr:`\*E` + error +} + +func _(p *PtrConvErr) { + var _ error = p +} + +type unexportedPtrConvErr struct { + error +} + +func _(p *unexportedPtrConvErr) { + var _ error = p +} + +func test() { + var pe fs.PathError + var _ error = &pe // ok + + var _ error = &struct{ error }{} // OK (unnamed type) + + type local struct{ error } + var _ error = local{} // want `local is converted to error both as a value and as a pointer` + var _ error = &local{} // want `local is converted to error both as a value and as a pointer` + + type localAmbiguousErr struct{ error } // want `both localAmbiguousErr and \*localAmbiguousErr implement the error interface, making the intent ambiguous` +} +-- Declare that unexportedAmbiguousErr implements error -- +// Copyright 2026 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 a + +import "io/fs" + +type ConflictErr struct { + error +} + +func _(c ConflictErr) { + var _ error = c // want "ConflictErr is converted to error both as a value and as a pointer" + var _ error = &c // want "ConflictErr is converted to error both as a value and as a pointer" +} + +type AmbiguousErr struct { // want `both AmbiguousErr and \*AmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +type unexportedAmbiguousErr struct { // want `both unexportedAmbiguousErr and \*unexportedAmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +var _ error = *new(unexportedAmbiguousErr) + +type PtrOnlyErr struct { // want PtrOnlyErr:`\*E` + msg string +} + +func (p *PtrOnlyErr) Error() string { return p.msg } + +func _(p *PtrOnlyErr) { + var _ error = p +} + +type ValErr struct { // want ValErr:`E` + msg string +} + +func (v ValErr) Error() string { return v.msg } + +func _(v ValErr) { + var _ error = v +} + +type PtrConvErr struct { // want PtrConvErr:`\*E` + error +} + +func _(p *PtrConvErr) { + var _ error = p +} + +type unexportedPtrConvErr struct { + error +} + +func _(p *unexportedPtrConvErr) { + var _ error = p +} + +func test() { + var pe fs.PathError + var _ error = &pe // ok + + var _ error = &struct{ error }{} // OK (unnamed type) + + type local struct{ error } + var _ error = local{} // want `local is converted to error both as a value and as a pointer` + var _ error = &local{} // want `local is converted to error both as a value and as a pointer` + + type localAmbiguousErr struct{ error } // want `both localAmbiguousErr and \*localAmbiguousErr implement the error interface, making the intent ambiguous` +} +-- Declare that *unexportedAmbiguousErr implements error -- +// Copyright 2026 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 a + +import "io/fs" + +type ConflictErr struct { + error +} + +func _(c ConflictErr) { + var _ error = c // want "ConflictErr is converted to error both as a value and as a pointer" + var _ error = &c // want "ConflictErr is converted to error both as a value and as a pointer" +} + +type AmbiguousErr struct { // want `both AmbiguousErr and \*AmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +type unexportedAmbiguousErr struct { // want `both unexportedAmbiguousErr and \*unexportedAmbiguousErr implement the error interface, making the intent ambiguous` + error +} + +var _ error = (*unexportedAmbiguousErr)(nil) + +type PtrOnlyErr struct { // want PtrOnlyErr:`\*E` + msg string +} + +func (p *PtrOnlyErr) Error() string { return p.msg } + +func _(p *PtrOnlyErr) { + var _ error = p +} + +type ValErr struct { // want ValErr:`E` + msg string +} + +func (v ValErr) Error() string { return v.msg } + +func _(v ValErr) { + var _ error = v +} + +type PtrConvErr struct { // want PtrConvErr:`\*E` + error +} + +func _(p *PtrConvErr) { + var _ error = p +} + +type unexportedPtrConvErr struct { + error +} + +func _(p *unexportedPtrConvErr) { + var _ error = p +} + +func test() { + var pe fs.PathError + var _ error = &pe // ok + + var _ error = &struct{ error }{} // OK (unnamed type) + + type local struct{ error } + var _ error = local{} // want `local is converted to error both as a value and as a pointer` + var _ error = &local{} // want `local is converted to error both as a value and as a pointer` + + type localAmbiguousErr struct{ error } // want `both localAmbiguousErr and \*localAmbiguousErr implement the error interface, making the intent ambiguous` +}
diff --git a/gopls/internal/analysis/ptrtoerror/testdata/src/b/b.go b/gopls/internal/analysis/ptrtoerror/testdata/src/b/b.go new file mode 100644 index 0000000..22501ad --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/testdata/src/b/b.go
@@ -0,0 +1,27 @@ +// Copyright 2026 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 b + +import "a" + +func test() { + var p *a.ValErr + var _ error = p // want `conversion of \*a.ValErr to error, but package "a" uses ValErr \(sans pointer\) as an error \(e.g. at a.go:43:16\)` + + var v a.ValErr + var _ error = v // ok + + var pc a.PtrConvErr + var _ error = pc // want `conversion of a.PtrConvErr to error, but package "a" uses pointer \*PtrConvErr as an error \(e.g. at a.go:51:16\)` + + var ppc *a.PtrConvErr + var _ error = ppc // ok +} + +func variadic(errs ...error) {} + +func testVariadic() { + variadic(nil, a.ValErr{}, new(a.ValErr)) // want `conversion of \*a.ValErr to error, but package "a" uses ValErr \(sans pointer\) as an error` +}
diff --git a/gopls/internal/analysis/ptrtoerror/testdata/src/c/c.go b/gopls/internal/analysis/ptrtoerror/testdata/src/c/c.go new file mode 100644 index 0000000..dcfae79 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/testdata/src/c/c.go
@@ -0,0 +1,63 @@ +// Copyright 2026 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 c + +import "errors" + +type SwitchErr struct{} + +func (SwitchErr) Error() string { return "" } + +func testSwitch(err error) { + switch err.(type) { + case SwitchErr: // want "SwitchErr is converted to error both as a value and as a pointer" + } + + var p *SwitchErr + var _ error = p // want "SwitchErr is converted to error both as a value and as a pointer" +} + +type AsTypeErr struct{} + +func (AsTypeErr) Error() string { return "" } + +func testAsType(err error) { + errors.AsType[AsTypeErr](err) // want "AsTypeErr is converted to error both as a value and as a pointer" + + var p *AsTypeErr + var _ error = p // want "AsTypeErr is converted to error both as a value and as a pointer" +} + +type ConsistentSwitchErr struct{} // want ConsistentSwitchErr:`E` + +func (ConsistentSwitchErr) Error() string { return "" } + +func _(err error) { + switch err.(type) { + case ConsistentSwitchErr: // ok, value conversion + } +} + +type ConsistentAsTypeErr struct{} // want ConsistentAsTypeErr:`E` + +func (ConsistentAsTypeErr) Error() string { return "" } + +func _(err error) { + errors.AsType[ConsistentAsTypeErr](err) // ok, value conversion +} + +type ConsistentSwitchPtrErr struct{ error } // want ConsistentSwitchPtrErr:`\*E` + +func _(err error) { + switch err.(type) { + case *ConsistentSwitchPtrErr: // ok, pointer conversion + } +} + +type ConsistentAsTypePtrErr struct{ error } // want ConsistentAsTypePtrErr:`\*E` + +func _(err error) { + errors.AsType[*ConsistentAsTypePtrErr](err) // ok, pointer conversion +}
diff --git a/gopls/internal/analysis/ptrtoerror/testdata/src/c/c.go.golden b/gopls/internal/analysis/ptrtoerror/testdata/src/c/c.go.golden new file mode 100644 index 0000000..dcfae79 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/testdata/src/c/c.go.golden
@@ -0,0 +1,63 @@ +// Copyright 2026 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 c + +import "errors" + +type SwitchErr struct{} + +func (SwitchErr) Error() string { return "" } + +func testSwitch(err error) { + switch err.(type) { + case SwitchErr: // want "SwitchErr is converted to error both as a value and as a pointer" + } + + var p *SwitchErr + var _ error = p // want "SwitchErr is converted to error both as a value and as a pointer" +} + +type AsTypeErr struct{} + +func (AsTypeErr) Error() string { return "" } + +func testAsType(err error) { + errors.AsType[AsTypeErr](err) // want "AsTypeErr is converted to error both as a value and as a pointer" + + var p *AsTypeErr + var _ error = p // want "AsTypeErr is converted to error both as a value and as a pointer" +} + +type ConsistentSwitchErr struct{} // want ConsistentSwitchErr:`E` + +func (ConsistentSwitchErr) Error() string { return "" } + +func _(err error) { + switch err.(type) { + case ConsistentSwitchErr: // ok, value conversion + } +} + +type ConsistentAsTypeErr struct{} // want ConsistentAsTypeErr:`E` + +func (ConsistentAsTypeErr) Error() string { return "" } + +func _(err error) { + errors.AsType[ConsistentAsTypeErr](err) // ok, value conversion +} + +type ConsistentSwitchPtrErr struct{ error } // want ConsistentSwitchPtrErr:`\*E` + +func _(err error) { + switch err.(type) { + case *ConsistentSwitchPtrErr: // ok, pointer conversion + } +} + +type ConsistentAsTypePtrErr struct{ error } // want ConsistentAsTypePtrErr:`\*E` + +func _(err error) { + errors.AsType[*ConsistentAsTypePtrErr](err) // ok, pointer conversion +}
diff --git a/gopls/internal/analysis/ptrtoerror/testdata/src/d/d.go b/gopls/internal/analysis/ptrtoerror/testdata/src/d/d.go new file mode 100644 index 0000000..a9b4eda --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/testdata/src/d/d.go
@@ -0,0 +1,21 @@ +// Copyright 2026 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 d + +import "c" + +func testConsistent() { + var p *c.ConsistentSwitchErr + var _ error = p // want `conversion of \*c.ConsistentSwitchErr to error, but package "c" uses ConsistentSwitchErr \(sans pointer\) as an error` + + var p2 *c.ConsistentAsTypeErr + var _ error = p2 // want `conversion of \*c.ConsistentAsTypeErr to error, but package "c" uses ConsistentAsTypeErr \(sans pointer\) as an error` + + var v c.ConsistentSwitchPtrErr + var _ error = v // want `conversion of c.ConsistentSwitchPtrErr to error, but package "c" uses pointer \*ConsistentSwitchPtrErr as an error` + + var v2 c.ConsistentAsTypePtrErr + var _ error = v2 // want `conversion of c.ConsistentAsTypePtrErr to error, but package "c" uses pointer \*ConsistentAsTypePtrErr as an error` +}
diff --git a/gopls/internal/analysis/ptrtoerror/testdata/src/errors/errors.go b/gopls/internal/analysis/ptrtoerror/testdata/src/errors/errors.go new file mode 100644 index 0000000..ccc0376 --- /dev/null +++ b/gopls/internal/analysis/ptrtoerror/testdata/src/errors/errors.go
@@ -0,0 +1,10 @@ +// Copyright 2026 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 errors + +func AsType[T error](err error) (T, bool) { + var zero T + return zero, false +}
diff --git a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go index db5012a..52d87ea 100644 --- a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go +++ b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go
@@ -8,18 +8,17 @@ package simplifycompositelit import ( - "bytes" _ "embed" "fmt" "go/ast" - "go/printer" "go/token" - "reflect" + "go/types" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysis/analyzerutil" + "golang.org/x/tools/internal/astutil" ) //go:embed doc.go @@ -43,163 +42,113 @@ } inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} - inspect.Preorder(nodeFilter, func(n ast.Node) { - if _, ok := generated[pass.Fset.File(n.Pos())]; ok { - return // skip checking if it's generated code + + // Find each CompositeLit with an explicit type. + // Then attempt to simplify each element that is + // itself a CompositeLit with an explicit type. + // + // TODO(adonovan): note that go1.28 will permit types to be + // omitted much more generally, so instead of starting with + // the "outer" CompositeLit, we'll need to look for "inner" + // literals appearing in a context that determines their type + // (assuming we want to take a maximal approach to + // simplification). We should also support named and alias + // types more thoroughly. + + for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) { + lit := curLit.Node().(*ast.CompositeLit) + + // Skip generated code. + if _, ok := generated[pass.Fset.File(lit.Pos())]; ok { + continue } - expr := n.(*ast.CompositeLit) - - outer := expr - var keyType, eltType ast.Expr - switch typ := outer.Type.(type) { + var ( + kind string + keyType, eltType ast.Expr + ) + switch typ := lit.Type.(type) { case *ast.ArrayType: eltType = typ.Elt + if typ.Len != nil { + kind = "array" + } else { + kind = "slice" + } case *ast.MapType: keyType = typ.Key eltType = typ.Value + kind = "map" + default: + // e.g. struct, named, or nil (no explicit type) + continue } - if eltType == nil { - return - } - var ktyp reflect.Value - if keyType != nil { - ktyp = reflect.ValueOf(keyType) - } - typ := reflect.ValueOf(eltType) - for _, x := range outer.Elts { - // look at value of indexed/named elements - if t, ok := x.(*ast.KeyValueExpr); ok { - if keyType != nil { - simplifyLiteral(pass, ktyp, keyType, t.Key) + for _, elt := range lit.Elts { + if kve, ok := elt.(*ast.KeyValueExpr); ok { + if keyType != nil { // map + simplifyLiteral(pass, keyType, kve.Key, kind) } - x = t.Value + elt = kve.Value } - simplifyLiteral(pass, typ, eltType, x) + simplifyLiteral(pass, eltType, elt, kind) } - }) + } return nil, nil } -func simplifyLiteral(pass *analysis.Pass, typ reflect.Value, astType, x ast.Expr) { - // if the element is a composite literal and its literal type - // matches the outer literal's element type exactly, the inner - // literal type may be omitted - if inner, ok := x.(*ast.CompositeLit); ok && match(typ, reflect.ValueOf(inner.Type)) { - var b bytes.Buffer - printer.Fprint(&b, pass.Fset, inner.Type) // ignore error - createDiagnostic(pass, inner.Type.Pos(), inner.Type.End(), b.String()) +// simplifyLiteral reports a diagnostic if expr's is a T{...} or +// &T{...} literal whose type is identical to want and therefore +// redundant. +func simplifyLiteral(pass *analysis.Pass, want, expr ast.Expr, kind string) { + info := pass.TypesInfo + + report := func(ampPos token.Pos, innerType ast.Expr) { + var ( + start = innerType.Pos() + end = innerType.End() + amp string + edits []analysis.TextEdit + ) + if ampPos.IsValid() { + amp = "&" + edits = append(edits, analysis.TextEdit{ + Pos: ampPos, + End: ampPos + token.Pos(len("&")), + }) + start = ampPos + } + edits = append(edits, analysis.TextEdit{Pos: innerType.Pos(), End: end}) + pass.Report(analysis.Diagnostic{ + Pos: start, + End: end, + Message: fmt.Sprintf("redundant type in %s literal", kind), + SuggestedFixes: []analysis.SuggestedFix{{ + Message: fmt.Sprintf("Remove '%s%s'", amp, astutil.Format(pass.Fset, innerType)), + TextEdits: edits, + }}, + }) } + + // If the element is a composite literal whose explicit type + // is identical to the outer literal's element type, + // the inner literal's type may be omitted + if inner, ok := expr.(*ast.CompositeLit); ok && + inner.Type != nil && + types.Identical(info.TypeOf(want), info.TypeOf(inner.Type)) { + report(token.NoPos, inner.Type) + } + // if the outer literal's element type is a pointer type *T // and the element is & of a composite literal of type T, // the inner &T may be omitted. - if ptr, ok := astType.(*ast.StarExpr); ok { - if addr, ok := x.(*ast.UnaryExpr); ok && addr.Op == token.AND { - if inner, ok := addr.X.(*ast.CompositeLit); ok { - if match(reflect.ValueOf(ptr.X), reflect.ValueOf(inner.Type)) { - var b bytes.Buffer - printer.Fprint(&b, pass.Fset, inner.Type) // ignore error - // Account for the & by subtracting 1 from typ.Pos(). - createDiagnostic(pass, inner.Type.Pos()-1, inner.Type.End(), "&"+b.String()) - } + if star, ok := want.(*ast.StarExpr); ok { + if addr, ok := expr.(*ast.UnaryExpr); ok && addr.Op == token.AND { + if inner, ok := addr.X.(*ast.CompositeLit); ok && + inner.Type != nil && + types.Identical(info.TypeOf(star.X), info.TypeOf(inner.Type)) { + report(addr.OpPos, inner.Type) } } } } - -func createDiagnostic(pass *analysis.Pass, start, end token.Pos, typ string) { - pass.Report(analysis.Diagnostic{ - Pos: start, - End: end, - Message: "redundant type from array, slice, or map composite literal", - SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("Remove '%s'", typ), - TextEdits: []analysis.TextEdit{{ - Pos: start, - End: end, - NewText: []byte{}, - }}, - }}, - }) -} - -// match reports whether pattern matches val, -// recording wildcard submatches in m. -// If m == nil, match checks whether pattern == val. -// from https://github.com/golang/go/blob/26154f31ad6c801d8bad5ef58df1e9263c6beec7/src/cmd/gofmt/rewrite.go#L160 -func match(pattern, val reflect.Value) bool { - // Otherwise, pattern and val must match recursively. - if !pattern.IsValid() || !val.IsValid() { - return !pattern.IsValid() && !val.IsValid() - } - if pattern.Type() != val.Type() { - return false - } - - // Special cases. - switch pattern.Type() { - case identType: - // For identifiers, only the names need to match - // (and none of the other *ast.Object information). - // This is a common case, handle it all here instead - // of recursing down any further via reflection. - p := pattern.Interface().(*ast.Ident) - v := val.Interface().(*ast.Ident) - return p == nil && v == nil || p != nil && v != nil && p.Name == v.Name - case objectPtrType, positionType: - // object pointers and token positions always match - return true - case callExprType: - // For calls, the Ellipsis fields (token.Position) must - // match since that is how f(x) and f(x...) are different. - // Check them here but fall through for the remaining fields. - p := pattern.Interface().(*ast.CallExpr) - v := val.Interface().(*ast.CallExpr) - if p.Ellipsis.IsValid() != v.Ellipsis.IsValid() { - return false - } - } - - p := reflect.Indirect(pattern) - v := reflect.Indirect(val) - if !p.IsValid() || !v.IsValid() { - return !p.IsValid() && !v.IsValid() - } - - switch p.Kind() { - case reflect.Slice: - if p.Len() != v.Len() { - return false - } - for i := 0; i < p.Len(); i++ { - if !match(p.Index(i), v.Index(i)) { - return false - } - } - return true - - case reflect.Struct: - for i := 0; i < p.NumField(); i++ { - if !match(p.Field(i), v.Field(i)) { - return false - } - } - return true - - case reflect.Interface: - return match(p.Elem(), v.Elem()) - } - - // Handle token integers, etc. - return p.Interface() == v.Interface() -} - -// Values/types for special cases. -var ( - identType = reflect.TypeFor[*ast.Ident]() - objectPtrType = reflect.TypeFor[*ast.Object]() - positionType = reflect.TypeFor[token.Pos]() - callExprType = reflect.TypeFor[*ast.CallExpr]() -)
diff --git a/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go b/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go index 14e0fa3..50ebb40 100644 --- a/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go +++ b/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go
@@ -13,35 +13,35 @@ } var _ = [42]T{ - T{}, // want "redundant type from array, slice, or map composite literal" - T{1, 2}, // want "redundant type from array, slice, or map composite literal" - T{3, 4}, // want "redundant type from array, slice, or map composite literal" + T{}, // want "redundant type in array literal" + T{1, 2}, // want "redundant type in array literal" + T{3, 4}, // want "redundant type in array literal" } var _ = [...]T{ - T{}, // want "redundant type from array, slice, or map composite literal" - T{1, 2}, // want "redundant type from array, slice, or map composite literal" - T{3, 4}, // want "redundant type from array, slice, or map composite literal" + T{}, // want "redundant type in array literal" + T{1, 2}, // want "redundant type in array literal" + T{3, 4}, // want "redundant type in array literal" } var _ = []T{ - T{}, // want "redundant type from array, slice, or map composite literal" - T{1, 2}, // want "redundant type from array, slice, or map composite literal" - T{3, 4}, // want "redundant type from array, slice, or map composite literal" + T{}, // want "redundant type in slice literal" + T{1, 2}, // want "redundant type in slice literal" + T{3, 4}, // want "redundant type in slice literal" } var _ = []T{ - T{}, // want "redundant type from array, slice, or map composite literal" - 10: T{1, 2}, // want "redundant type from array, slice, or map composite literal" - 20: T{3, 4}, // want "redundant type from array, slice, or map composite literal" + T{}, // want "redundant type in slice literal" + 10: T{1, 2}, // want "redundant type in slice literal" + 20: T{3, 4}, // want "redundant type in slice literal" } var _ = []struct { x, y int }{ - struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" - 10: struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" - 20: struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" + struct{ x, y int }{}, // want "redundant type in slice literal" + 10: struct{ x, y int }{1, 2}, // want "redundant type in slice literal" + 20: struct{ x, y int }{3, 4}, // want "redundant type in slice literal" } var _ = []interface{}{ @@ -51,38 +51,38 @@ } var _ = [][]int{ - []int{}, // want "redundant type from array, slice, or map composite literal" - []int{1, 2}, // want "redundant type from array, slice, or map composite literal" - []int{3, 4}, // want "redundant type from array, slice, or map composite literal" + []int{}, // want "redundant type in slice literal" + []int{1, 2}, // want "redundant type in slice literal" + []int{3, 4}, // want "redundant type in slice literal" } var _ = [][]int{ ([]int{}), ([]int{1, 2}), - []int{3, 4}, // want "redundant type from array, slice, or map composite literal" + []int{3, 4}, // want "redundant type in slice literal" } var _ = [][][]int{ - [][]int{}, // want "redundant type from array, slice, or map composite literal" - [][]int{ // want "redundant type from array, slice, or map composite literal" - []int{}, // want "redundant type from array, slice, or map composite literal" - []int{0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" - []int{4, 5}, // want "redundant type from array, slice, or map composite literal" + [][]int{}, // want "redundant type in slice literal" + [][]int{ // want "redundant type in slice literal" + []int{}, // want "redundant type in slice literal" + []int{0, 1, 2, 3}, // want "redundant type in slice literal" + []int{4, 5}, // want "redundant type in slice literal" }, } var _ = map[string]T{ - "foo": T{}, // want "redundant type from array, slice, or map composite literal" - "bar": T{1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": T{3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": T{}, // want "redundant type in map literal" + "bar": T{1, 2}, // want "redundant type in map literal" + "bal": T{3, 4}, // want "redundant type in map literal" } var _ = map[string]struct { x, y int }{ - "foo": struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" - "bar": struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": struct{ x, y int }{}, // want "redundant type in map literal" + "bar": struct{ x, y int }{1, 2}, // want "redundant type in map literal" + "bal": struct{ x, y int }{3, 4}, // want "redundant type in map literal" } var _ = map[string]interface{}{ @@ -92,15 +92,15 @@ } var _ = map[string][]int{ - "foo": []int{}, // want "redundant type from array, slice, or map composite literal" - "bar": []int{1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": []int{3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": []int{}, // want "redundant type in map literal" + "bar": []int{1, 2}, // want "redundant type in map literal" + "bal": []int{3, 4}, // want "redundant type in map literal" } var _ = map[string][]int{ "foo": ([]int{}), "bar": ([]int{1, 2}), - "bal": []int{3, 4}, // want "redundant type from array, slice, or map composite literal" + "bal": []int{3, 4}, // want "redundant type in map literal" } type Point struct { @@ -117,44 +117,49 @@ f *Point } -// from exp/4s/data.go +// in exp/4s/data.go var pieces3 = []Piece{ - Piece{0, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - Piece{1, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - Piece{2, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - Piece{3, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" + Piece{0, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + Piece{1, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + Piece{2, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + Piece{3, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" } var _ = [42]*T{ - &T{}, // want "redundant type from array, slice, or map composite literal" - &T{1, 2}, // want "redundant type from array, slice, or map composite literal" - &T{3, 4}, // want "redundant type from array, slice, or map composite literal" + &T{}, // want "redundant type in array literal" + &T{1, 2}, // want "redundant type in array literal" + &T{3, 4}, // want "redundant type in array literal" } var _ = [...]*T{ - &T{}, // want "redundant type from array, slice, or map composite literal" - &T{1, 2}, // want "redundant type from array, slice, or map composite literal" - &T{3, 4}, // want "redundant type from array, slice, or map composite literal" + &T{}, // want "redundant type in array literal" + &T{1, 2}, // want "redundant type in array literal" + &T{3, 4}, // want "redundant type in array literal" } var _ = []*T{ - &T{}, // want "redundant type from array, slice, or map composite literal" - &T{1, 2}, // want "redundant type from array, slice, or map composite literal" - &T{3, 4}, // want "redundant type from array, slice, or map composite literal" + &T{}, // want "redundant type in slice literal" + &T{1, 2}, // want "redundant type in slice literal" + &T{3, 4}, // want "redundant type in slice literal" +} + +// Regression test for golang/go#80459. +var _ = []*T{ + & /* keep */ T{}, // want "redundant type in slice literal" } var _ = []*T{ - &T{}, // want "redundant type from array, slice, or map composite literal" - 10: &T{1, 2}, // want "redundant type from array, slice, or map composite literal" - 20: &T{3, 4}, // want "redundant type from array, slice, or map composite literal" + &T{}, // want "redundant type in slice literal" + 10: &T{1, 2}, // want "redundant type in slice literal" + 20: &T{3, 4}, // want "redundant type in slice literal" } var _ = []*struct { x, y int }{ - &struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" - 10: &struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" - 20: &struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" + &struct{ x, y int }{}, // want "redundant type in slice literal" + 10: &struct{ x, y int }{1, 2}, // want "redundant type in slice literal" + 20: &struct{ x, y int }{3, 4}, // want "redundant type in slice literal" } var _ = []interface{}{ @@ -164,38 +169,38 @@ } var _ = []*[]int{ - &[]int{}, // want "redundant type from array, slice, or map composite literal" - &[]int{1, 2}, // want "redundant type from array, slice, or map composite literal" - &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" + &[]int{}, // want "redundant type in slice literal" + &[]int{1, 2}, // want "redundant type in slice literal" + &[]int{3, 4}, // want "redundant type in slice literal" } var _ = []*[]int{ (&[]int{}), (&[]int{1, 2}), - &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" + &[]int{3, 4}, // want "redundant type in slice literal" } var _ = []*[]*[]int{ - &[]*[]int{}, // want "redundant type from array, slice, or map composite literal" - &[]*[]int{ // want "redundant type from array, slice, or map composite literal" - &[]int{}, // want "redundant type from array, slice, or map composite literal" - &[]int{0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" - &[]int{4, 5}, // want "redundant type from array, slice, or map composite literal" + &[]*[]int{}, // want "redundant type in slice literal" + &[]*[]int{ // want "redundant type in slice literal" + &[]int{}, // want "redundant type in slice literal" + &[]int{0, 1, 2, 3}, // want "redundant type in slice literal" + &[]int{4, 5}, // want "redundant type in slice literal" }, } var _ = map[string]*T{ - "foo": &T{}, // want "redundant type from array, slice, or map composite literal" - "bar": &T{1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": &T{3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": &T{}, // want "redundant type in map literal" + "bar": &T{1, 2}, // want "redundant type in map literal" + "bal": &T{3, 4}, // want "redundant type in map literal" } var _ = map[string]*struct { x, y int }{ - "foo": &struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" - "bar": &struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": &struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": &struct{ x, y int }{}, // want "redundant type in map literal" + "bar": &struct{ x, y int }{1, 2}, // want "redundant type in map literal" + "bal": &struct{ x, y int }{3, 4}, // want "redundant type in map literal" } var _ = map[string]interface{}{ @@ -205,30 +210,54 @@ } var _ = map[string]*[]int{ - "foo": &[]int{}, // want "redundant type from array, slice, or map composite literal" - "bar": &[]int{1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": &[]int{}, // want "redundant type in map literal" + "bar": &[]int{1, 2}, // want "redundant type in map literal" + "bal": &[]int{3, 4}, // want "redundant type in map literal" } var _ = map[string]*[]int{ "foo": (&[]int{}), "bar": (&[]int{1, 2}), - "bal": &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" + "bal": &[]int{3, 4}, // want "redundant type in map literal" } var pieces4 = []*Piece{ - &Piece{0, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - &Piece{1, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - &Piece{2, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - &Piece{3, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" + &Piece{0, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + &Piece{1, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + &Piece{2, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + &Piece{3, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" } var _ = map[T]T2{ - T{1, 2}: T2{3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - T{5, 6}: T2{7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" + T{1, 2}: T2{3, 4}, // want "redundant type in map literal" "redundant type in map literal" + T{5, 6}: T2{7, 8}, // want "redundant type in map literal" "redundant type in map literal" } var _ = map[*T]*T2{ - &T{1, 2}: &T2{3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - &T{5, 6}: &T2{7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" + &T{1, 2}: &T2{3, 4}, // want "redundant type in map literal" "redundant type in map literal" + &T{5, 6}: &T2{7, 8}, // want "redundant type in map literal" "redundant type in map literal" +} + +type AliasT = T + +var _ = []T{ + AliasT{}, // want "redundant type in slice literal" +} + +var _ = []AliasT{ + T{}, // want "redundant type in slice literal" +} + +var _ = []*AliasT{ + &T{}, // want "redundant type in slice literal" +} + +var _ = map[string]AliasT{ + "foo": T{}, // want "redundant type in map literal" +} + +type SliceAlias = []T + +var _ = SliceAlias{ + T{}, }
diff --git a/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go.golden b/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go.golden index 6bfed45..36ef8cb 100644 --- a/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go.golden +++ b/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go.golden
@@ -13,35 +13,35 @@ } var _ = [42]T{ - {}, // want "redundant type from array, slice, or map composite literal" - {1, 2}, // want "redundant type from array, slice, or map composite literal" - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in array literal" + {1, 2}, // want "redundant type in array literal" + {3, 4}, // want "redundant type in array literal" } var _ = [...]T{ - {}, // want "redundant type from array, slice, or map composite literal" - {1, 2}, // want "redundant type from array, slice, or map composite literal" - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in array literal" + {1, 2}, // want "redundant type in array literal" + {3, 4}, // want "redundant type in array literal" } var _ = []T{ - {}, // want "redundant type from array, slice, or map composite literal" - {1, 2}, // want "redundant type from array, slice, or map composite literal" - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + {1, 2}, // want "redundant type in slice literal" + {3, 4}, // want "redundant type in slice literal" } var _ = []T{ - {}, // want "redundant type from array, slice, or map composite literal" - 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" - 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + 10: {1, 2}, // want "redundant type in slice literal" + 20: {3, 4}, // want "redundant type in slice literal" } var _ = []struct { x, y int }{ - {}, // want "redundant type from array, slice, or map composite literal" - 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" - 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + 10: {1, 2}, // want "redundant type in slice literal" + 20: {3, 4}, // want "redundant type in slice literal" } var _ = []interface{}{ @@ -51,38 +51,38 @@ } var _ = [][]int{ - {}, // want "redundant type from array, slice, or map composite literal" - {1, 2}, // want "redundant type from array, slice, or map composite literal" - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + {1, 2}, // want "redundant type in slice literal" + {3, 4}, // want "redundant type in slice literal" } var _ = [][]int{ ([]int{}), ([]int{1, 2}), - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {3, 4}, // want "redundant type in slice literal" } var _ = [][][]int{ - {}, // want "redundant type from array, slice, or map composite literal" - { // want "redundant type from array, slice, or map composite literal" - {}, // want "redundant type from array, slice, or map composite literal" - {0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" - {4, 5}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + { // want "redundant type in slice literal" + {}, // want "redundant type in slice literal" + {0, 1, 2, 3}, // want "redundant type in slice literal" + {4, 5}, // want "redundant type in slice literal" }, } var _ = map[string]T{ - "foo": {}, // want "redundant type from array, slice, or map composite literal" - "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": {}, // want "redundant type in map literal" + "bar": {1, 2}, // want "redundant type in map literal" + "bal": {3, 4}, // want "redundant type in map literal" } var _ = map[string]struct { x, y int }{ - "foo": {}, // want "redundant type from array, slice, or map composite literal" - "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": {}, // want "redundant type in map literal" + "bar": {1, 2}, // want "redundant type in map literal" + "bal": {3, 4}, // want "redundant type in map literal" } var _ = map[string]interface{}{ @@ -92,15 +92,15 @@ } var _ = map[string][]int{ - "foo": {}, // want "redundant type from array, slice, or map composite literal" - "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": {}, // want "redundant type in map literal" + "bar": {1, 2}, // want "redundant type in map literal" + "bal": {3, 4}, // want "redundant type in map literal" } var _ = map[string][]int{ "foo": ([]int{}), "bar": ([]int{1, 2}), - "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" + "bal": {3, 4}, // want "redundant type in map literal" } type Point struct { @@ -117,44 +117,49 @@ f *Point } -// from exp/4s/data.go +// in exp/4s/data.go var pieces3 = []Piece{ - {0, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - {1, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - {2, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - {3, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" + {0, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + {1, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + {2, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + {3, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" } var _ = [42]*T{ - {}, // want "redundant type from array, slice, or map composite literal" - {1, 2}, // want "redundant type from array, slice, or map composite literal" - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in array literal" + {1, 2}, // want "redundant type in array literal" + {3, 4}, // want "redundant type in array literal" } var _ = [...]*T{ - {}, // want "redundant type from array, slice, or map composite literal" - {1, 2}, // want "redundant type from array, slice, or map composite literal" - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in array literal" + {1, 2}, // want "redundant type in array literal" + {3, 4}, // want "redundant type in array literal" } var _ = []*T{ - {}, // want "redundant type from array, slice, or map composite literal" - {1, 2}, // want "redundant type from array, slice, or map composite literal" - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + {1, 2}, // want "redundant type in slice literal" + {3, 4}, // want "redundant type in slice literal" +} + +// Regression test for golang/go#80459. +var _ = []*T{ + /* keep */ {}, // want "redundant type in slice literal" } var _ = []*T{ - {}, // want "redundant type from array, slice, or map composite literal" - 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" - 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + 10: {1, 2}, // want "redundant type in slice literal" + 20: {3, 4}, // want "redundant type in slice literal" } var _ = []*struct { x, y int }{ - {}, // want "redundant type from array, slice, or map composite literal" - 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" - 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + 10: {1, 2}, // want "redundant type in slice literal" + 20: {3, 4}, // want "redundant type in slice literal" } var _ = []interface{}{ @@ -164,38 +169,38 @@ } var _ = []*[]int{ - {}, // want "redundant type from array, slice, or map composite literal" - {1, 2}, // want "redundant type from array, slice, or map composite literal" - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + {1, 2}, // want "redundant type in slice literal" + {3, 4}, // want "redundant type in slice literal" } var _ = []*[]int{ (&[]int{}), (&[]int{1, 2}), - {3, 4}, // want "redundant type from array, slice, or map composite literal" + {3, 4}, // want "redundant type in slice literal" } var _ = []*[]*[]int{ - {}, // want "redundant type from array, slice, or map composite literal" - { // want "redundant type from array, slice, or map composite literal" - {}, // want "redundant type from array, slice, or map composite literal" - {0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" - {4, 5}, // want "redundant type from array, slice, or map composite literal" + {}, // want "redundant type in slice literal" + { // want "redundant type in slice literal" + {}, // want "redundant type in slice literal" + {0, 1, 2, 3}, // want "redundant type in slice literal" + {4, 5}, // want "redundant type in slice literal" }, } var _ = map[string]*T{ - "foo": {}, // want "redundant type from array, slice, or map composite literal" - "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": {}, // want "redundant type in map literal" + "bar": {1, 2}, // want "redundant type in map literal" + "bal": {3, 4}, // want "redundant type in map literal" } var _ = map[string]*struct { x, y int }{ - "foo": {}, // want "redundant type from array, slice, or map composite literal" - "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": {}, // want "redundant type in map literal" + "bar": {1, 2}, // want "redundant type in map literal" + "bal": {3, 4}, // want "redundant type in map literal" } var _ = map[string]interface{}{ @@ -205,30 +210,54 @@ } var _ = map[string]*[]int{ - "foo": {}, // want "redundant type from array, slice, or map composite literal" - "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" - "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" + "foo": {}, // want "redundant type in map literal" + "bar": {1, 2}, // want "redundant type in map literal" + "bal": {3, 4}, // want "redundant type in map literal" } var _ = map[string]*[]int{ "foo": (&[]int{}), "bar": (&[]int{1, 2}), - "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" + "bal": {3, 4}, // want "redundant type in map literal" } var pieces4 = []*Piece{ - {0, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - {1, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - {2, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - {3, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" + {0, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + {1, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + {2, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" + {3, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" "redundant type in slice literal" } var _ = map[T]T2{ - {1, 2}: {3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - {5, 6}: {7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" + {1, 2}: {3, 4}, // want "redundant type in map literal" "redundant type in map literal" + {5, 6}: {7, 8}, // want "redundant type in map literal" "redundant type in map literal" } var _ = map[*T]*T2{ - {1, 2}: {3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" - {5, 6}: {7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" + {1, 2}: {3, 4}, // want "redundant type in map literal" "redundant type in map literal" + {5, 6}: {7, 8}, // want "redundant type in map literal" "redundant type in map literal" +} + +type AliasT = T + +var _ = []T{ + {}, // want "redundant type in slice literal" +} + +var _ = []AliasT{ + {}, // want "redundant type in slice literal" +} + +var _ = []*AliasT{ + {}, // want "redundant type in slice literal" +} + +var _ = map[string]AliasT{ + "foo": {}, // want "redundant type in map literal" +} + +type SliceAlias = []T + +var _ = SliceAlias{ + T{}, }
diff --git a/gopls/internal/analysis/unusedfunc/testdata/issue80555.txtar b/gopls/internal/analysis/unusedfunc/testdata/issue80555.txtar new file mode 100644 index 0000000..05bdfa4 --- /dev/null +++ b/gopls/internal/analysis/unusedfunc/testdata/issue80555.txtar
@@ -0,0 +1,11 @@ +Regression test for a false negative on a private module with name +that looks like a standard package. + +-- go.mod -- +module myapp +go 1.21 + +-- a/a.go -- +package a + +func unused() // want `function "unused" is unused`
diff --git a/gopls/internal/analysis/unusedfunc/unusedfunc.go b/gopls/internal/analysis/unusedfunc/unusedfunc.go index 48380c0..7dcf1db 100644 --- a/gopls/internal/analysis/unusedfunc/unusedfunc.go +++ b/gopls/internal/analysis/unusedfunc/unusedfunc.go
@@ -19,8 +19,8 @@ "golang.org/x/tools/internal/analysis/analyzerutil" typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" "golang.org/x/tools/internal/astutil" - "golang.org/x/tools/internal/packagepath" "golang.org/x/tools/internal/refactor" + "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/typesinternal/typeindex" ) @@ -74,7 +74,7 @@ func run(pass *analysis.Pass) (any, error) { // The standard library makes heavy use of intrinsics, linknames, etc, // that confuse this algorithm; so skip it (#74130). - if packagepath.IsStdPackage(pass.Pkg.Path()) { + if stdlib.HasPackage(pass.Pkg.Path()) { return nil, nil }
diff --git a/gopls/internal/analysis/unusedfunc/unusedfunc_test.go b/gopls/internal/analysis/unusedfunc/unusedfunc_test.go index db117b1..5f376e1 100644 --- a/gopls/internal/analysis/unusedfunc/unusedfunc_test.go +++ b/gopls/internal/analysis/unusedfunc/unusedfunc_test.go
@@ -5,7 +5,6 @@ package unusedfunc_test import ( - "path/filepath" "testing" "golang.org/x/tools/go/analysis/analysistest" @@ -14,6 +13,6 @@ ) func Test(t *testing.T) { - dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "basic.txtar")) - analysistest.RunWithSuggestedFixes(t, dir, unusedfunc.Analyzer, "example.com/a") + analysistest.RunWithSuggestedFixes(t, testfiles.ExtractTxtarFileToTmp(t, "testdata/basic.txtar"), unusedfunc.Analyzer, "example.com/a") + analysistest.Run(t, testfiles.ExtractTxtarFileToTmp(t, "testdata/issue80555.txtar"), unusedfunc.Analyzer, "myapp/a") }
diff --git a/gopls/internal/analysis/unusedparams/unusedparams.go b/gopls/internal/analysis/unusedparams/unusedparams.go index 5af2fa7..ca6a055 100644 --- a/gopls/internal/analysis/unusedparams/unusedparams.go +++ b/gopls/internal/analysis/unusedparams/unusedparams.go
@@ -14,9 +14,9 @@ "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/moreslices" "golang.org/x/tools/internal/analysis/analyzerutil" "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/moreslices" "golang.org/x/tools/internal/typesinternal" )
diff --git a/gopls/internal/analysis/yield/yield.go b/gopls/internal/analysis/yield/yield.go index 90300e5..8e899c9 100644 --- a/gopls/internal/analysis/yield/yield.go +++ b/gopls/internal/analysis/yield/yield.go
@@ -35,10 +35,10 @@ "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/go/ssa" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/analysis/analyzerutil" "golang.org/x/tools/internal/flow" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/typesinternal" )
diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index 8923d22..8347b39 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go
@@ -41,13 +41,13 @@ "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/frob" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/persistent" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/analysis/driverutil" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/facts" + "golang.org/x/tools/internal/moremaps" ) /*
diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index 112c4ba..a66608a 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go
@@ -36,11 +36,11 @@ "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/gopls/internal/util/tokeninternal" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gcimporter" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" @@ -1466,12 +1466,12 @@ id PackageID // Used for type checking: - pkgPath PackagePath - name PackageName + pkgPath PackagePath + name PackageName goFiles, compiledGoFiles, asmFiles []file.Handle sizes types.Sizes - depsByImpPath map[ImportPath]PackageID - goVersion string // packages.Module.GoVersion, e.g. "1.18" + depsByImpPath map[ImportPath]PackageID + goVersion string // packages.Module.GoVersion, e.g. "1.18" // Used for type check diagnostics: // TODO(rfindley): consider storing less data in gobDiagnostics, and
diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index ee1e8b8..0fd4b60 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go
@@ -39,13 +39,13 @@ "golang.org/x/tools/gopls/internal/util/constraints" "golang.org/x/tools/gopls/internal/util/immutable" "golang.org/x/tools/gopls/internal/util/memoize" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/pathutil" "golang.org/x/tools/gopls/internal/util/persistent" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/label" "golang.org/x/tools/internal/gocommand" + "golang.org/x/tools/internal/moremaps" ) // A Snapshot represents the current state for a given view.
diff --git a/gopls/internal/cache/source.go b/gopls/internal/cache/source.go index 7db1384..5b7e1b2 100644 --- a/gopls/internal/cache/source.go +++ b/gopls/internal/cache/source.go
@@ -13,8 +13,8 @@ "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/symbols" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/moremaps" ) // goplsSource is an imports.Source that provides import information using
diff --git a/gopls/internal/cache/typerefs/packageset.go b/gopls/internal/cache/typerefs/packageset.go index af495d1..71bb469 100644 --- a/gopls/internal/cache/typerefs/packageset.go +++ b/gopls/internal/cache/typerefs/packageset.go
@@ -11,7 +11,7 @@ "sync" "golang.org/x/tools/gopls/internal/cache/metadata" - "golang.org/x/tools/gopls/internal/util/moremaps" + "golang.org/x/tools/internal/moremaps" ) // PackageIndex stores common data to enable efficient representation of
diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go index 6bfc7e1..0e0e9b7 100644 --- a/gopls/internal/cache/view.go +++ b/gopls/internal/cache/view.go
@@ -30,13 +30,13 @@ "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/pathutil" "golang.org/x/tools/gopls/internal/vulncheck" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/modindex" + "golang.org/x/tools/internal/moremaps" ) // A Folder represents an LSP workspace folder, together with its per-folder
diff --git a/gopls/internal/cmd/call_hierarchy.go b/gopls/internal/cmd/call_hierarchy.go index 890b21f..906e0ba 100644 --- a/gopls/internal/cmd/call_hierarchy.go +++ b/gopls/internal/cmd/call_hierarchy.go
@@ -11,12 +11,11 @@ "strings" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // callHierarchy implements the callHierarchy verb for gopls. type callHierarchy struct { - app *Application + app *application } func (c *callHierarchy) Name() string { return "call_hierarchy" } @@ -36,7 +35,7 @@ func (c *callHierarchy) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("call_hierarchy expects 1 argument (position)") + return commandLineErrorf("call_hierarchy expects 1 argument (position)") } cli, _, err := c.app.connect(ctx)
diff --git a/gopls/internal/cmd/capabilities_test.go b/gopls/internal/cmd/capabilities_test.go index a3a438f..0bac958 100644 --- a/gopls/internal/cmd/capabilities_test.go +++ b/gopls/internal/cmd/capabilities_test.go
@@ -37,7 +37,7 @@ } defer os.RemoveAll(tmpDir) - app := New() + app := newApplication() ctx := context.Background() // Initialize the client.
diff --git a/gopls/internal/cmd/check.go b/gopls/internal/cmd/check.go index a73d1dc..83e7291 100644 --- a/gopls/internal/cmd/check.go +++ b/gopls/internal/cmd/check.go
@@ -16,7 +16,7 @@ // check implements the check verb for gopls. type check struct { - app *Application + app *application Severity string `flag:"severity" help:"minimum diagnostic severity (hint, info, warning, or error)"` }
diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index aca47c0..4bdb374 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go
@@ -22,40 +22,36 @@ "time" "golang.org/x/tools/gopls/internal/cache" - "golang.org/x/tools/gopls/internal/debug" - "golang.org/x/tools/gopls/internal/filecache" "golang.org/x/tools/gopls/internal/lsprpc" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/protocol/command" + protocolcommand "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/protocol/semtok" "golang.org/x/tools/gopls/internal/server" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/tool" "golang.org/x/tools/gopls/internal/util/browser" "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/gopls/internal/util/moreslices" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/moreslices" ) -// Application is the main application as passed to tool.Main -// It handles the main command line parsing and dispatch to the sub commands. -type Application struct { +// application represents the root gopls command and coordinates subcommand dispatch. +type application struct { // Core application flags // Embed the basic profiling flags supported by the tool package - tool.Profile + ProfileFlags // We include the server configuration directly for now, so the flags work // even without the verb. // TODO: Remove this when we stop allowing the serve verb by default. - Serve Serve + serve serve // the options configuring function to invoke when building a server options func(*settings.Options) - // Support for remote LSP server. - Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment."` + // Remote LSP client connection flags. + RemoteFlags // Verbose enables verbose logging. Verbose bool `flag:"v,verbose" help:"verbose output"` @@ -91,35 +87,65 @@ List bool `flag:"l,list" help:"display names of edited files"` } -func (app *Application) verbose() bool { +// RemoteFlags defines the set of flags for the forward mode. +type RemoteFlags struct { + Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment."` + + // The following flags are used with -remote=auto mode. + RemoteDebug string `flag:"remote.debug" help:"when used with -remote=auto, the -debug value used to start the daemon"` + RemoteListenTimeout time.Duration `flag:"remote.listen.timeout" help:"when used with -remote=auto, the -listen.timeout value used to start the daemon"` + + RemoteLogfile string `flag:"remote.logfile" help:"when used with -remote=auto, the -logfile value used to start the daemon"` +} + +func (r *RemoteFlags) remoteArgs(network, address string) []string { + args := []string{ + "serve", + "-listen", fmt.Sprintf(`%s;%s`, network, address), + } + if r.RemoteDebug != "" { + args = append(args, "-debug", r.RemoteDebug) + } + timeout := r.RemoteListenTimeout + if timeout == 0 { + timeout = 1 * time.Minute + } + args = append(args, "-listen.timeout", timeout.String()) + if r.RemoteLogfile != "" { + args = append(args, "-logfile", r.RemoteLogfile) + } + return args +} + +func (app *application) verbose() bool { return app.Verbose || app.VeryVerbose } -// New returns a new Application ready to run. -func New() *Application { - app := &Application{ - Serve: Serve{ +// newApplication returns a new application ready to run. +func newApplication() *application { + app := &application{ + RemoteFlags: RemoteFlags{ RemoteListenTimeout: 1 * time.Minute, }, } - app.Serve.app = app + app.serve.app = app return app } -// Name implements tool.Application returning the binary name. -func (app *Application) Name() string { return "gopls" } +// Name implements command returning the binary name. +func (app *application) Name() string { return "gopls" } -// Usage implements tool.Application returning empty extra argument usage. -func (app *Application) Usage() string { return "" } +// Usage implements command returning empty extra argument usage. +func (app *application) Usage() string { return "" } -// ShortHelp implements tool.Application returning the main binary help. -func (app *Application) ShortHelp() string { +// ShortHelp implements command returning the main binary help. +func (app *application) ShortHelp() string { return "" } -// DetailedHelp implements tool.Application returning the main binary help. +// DetailedHelp implements command returning the main binary help. // This includes the short help for all the sub commands. -func (app *Application) DetailedHelp(f *flag.FlagSet) { +func (app *application) DetailedHelp(f *flag.FlagSet) { w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) defer w.Flush() @@ -127,8 +153,10 @@ gopls is a Go language server. It is typically used with an editor to provide language features. When no -command is specified, gopls will default to the 'serve' command. The language -features can also be accessed via the gopls command-line interface. +command is specified, gopls will default to the 'serve' command. To see flags +for the 'serve' command, run 'gopls help serve'. + +The language features can also be accessed via the gopls command-line interface. For documentation of all its features, see: @@ -155,6 +183,7 @@ } fmt.Fprint(w, "\nflags:\n") printFlagDefaults(f) + fmt.Fprint(w, "\nFor flags specific to running the language server (such as -listen, -logfile, or -debug), run 'gopls help serve'.\n") } // this is a slightly modified version of flag.PrintDefaults to give us control @@ -226,45 +255,26 @@ return value == z.Interface().(flag.Value).String() } -// Run takes the args after top level flag processing, and invokes the correct -// sub command as specified by the first argument. -// If no arguments are passed it will invoke the server sub command, as a -// temporary measure for compatibility. -func (app *Application) Run(ctx context.Context, args ...string) error { - // In the category of "things we can do while waiting for the Go command": - // Pre-initialize the filecache, which takes ~50ms to hash the gopls - // executable, and immediately runs a gc. - filecache.Start() - - ctx = debug.WithInstance(ctx, app.OTel) - if len(args) == 0 { - s := flag.NewFlagSet(app.Name(), flag.ExitOnError) - return tool.Run(ctx, s, &app.Serve, args) - } - command, args := args[0], args[1:] - for _, c := range app.Commands() { - if c.Name() == command { - s := flag.NewFlagSet(app.Name(), flag.ExitOnError) - return tool.Run(ctx, s, c, args) - } - } - return tool.CommandLineErrorf("Unknown command %v", command) +// Run implements command, but should never be invoked directly. +// Main coordinates normalization and subcommand dispatching. +func (app *application) Run(ctx context.Context, args ...string) error { + panic("unreachable: application.Run should never be called directly") } // Commands returns the set of commands supported by the gopls tool on the // command line. // The command is specified by the first non flag argument. -func (app *Application) Commands() []tool.Application { - var commands []tool.Application +func (app *application) Commands() []command { + var commands []command commands = append(commands, app.mainCommands()...) commands = append(commands, app.featureCommands()...) commands = append(commands, app.internalCommands()...) return commands } -func (app *Application) mainCommands() []tool.Application { - return []tool.Application{ - &app.Serve, +func (app *application) mainCommands() []command { + return []command{ + &app.serve, &version{app: app}, &help{app: app}, &apiJSON{app: app}, @@ -272,14 +282,14 @@ } } -func (app *Application) internalCommands() []tool.Application { - return []tool.Application{ +func (app *application) internalCommands() []command { + return []command{ &vulncheck{app: app}, } } -func (app *Application) featureCommands() []tool.Application { - return []tool.Application{ +func (app *application) featureCommands() []command { + return []command{ &callHierarchy{app: app}, &check{app: app, Severity: "warning"}, &codeaction{app: app}, @@ -307,7 +317,8 @@ } // connect creates and initializes a new in-process gopls LSP session. -func (app *Application) connect(ctx context.Context) (*client, *cache.Session, error) { +func (app *application) connect(ctx context.Context) (*client, *cache.Session, error) { + root, err := os.Getwd() if err != nil { return nil, nil, fmt.Errorf("finding workdir: %v", err) @@ -399,7 +410,7 @@ // connection; it conceptually corresponds to a single call to // connect(2). type client struct { - app *Application + app *application server protocol.Server initializeResult *protocol.InitializeResult // includes server capabilities @@ -421,7 +432,7 @@ diagnostics []protocol.Diagnostic } -func newClient(app *Application) *client { +func newClient(app *application) *client { return &client{ app: app, files: make(map[protocol.DocumentURI]*cmdFile), @@ -817,7 +828,7 @@ } func diagnoseFiles(ctx context.Context, server protocol.Server, files []protocol.DocumentURI) error { - cmd := command.NewDiagnoseFilesCommand("Diagnose files", command.DiagnoseFilesArgs{ + cmd := protocolcommand.NewDiagnoseFilesCommand("Diagnose files", protocolcommand.DiagnoseFilesArgs{ Files: files, }) _, err := executeCommand(ctx, server, cmd)
diff --git a/gopls/internal/cmd/codeaction.go b/gopls/internal/cmd/codeaction.go index 974b499..f15ccdf 100644 --- a/gopls/internal/cmd/codeaction.go +++ b/gopls/internal/cmd/codeaction.go
@@ -13,7 +13,6 @@ "strings" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // codeaction implements the codeaction verb for gopls. @@ -23,7 +22,7 @@ Title string `flag:"title" help:"regular expression to match title"` Exec bool `flag:"exec" help:"execute the first matching code action"` - app *Application + app *application } func (cmd *codeaction) Name() string { return "codeaction" } @@ -105,7 +104,7 @@ func (cmd *codeaction) Run(ctx context.Context, args ...string) error { if len(args) < 1 { - return tool.CommandLineErrorf("codeaction expects at least 1 argument") + return commandLineErrorf("codeaction expects at least 1 argument") } cmd.app.editFlags = &cmd.EditFlags cli, _, err := cmd.app.connect(ctx)
diff --git a/gopls/internal/cmd/codelens.go b/gopls/internal/cmd/codelens.go index 1d32493..e48462a 100644 --- a/gopls/internal/cmd/codelens.go +++ b/gopls/internal/cmd/codelens.go
@@ -11,13 +11,12 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/tool" ) // codelens implements the codelens verb for gopls. type codelens struct { EditFlags - app *Application + app *application Exec bool `flag:"exec" help:"execute the first matching code lens"` } @@ -56,14 +55,14 @@ var filename, title string switch len(args) { case 0: - return tool.CommandLineErrorf("codelens requires a file name") + return commandLineErrorf("codelens requires a file name") case 2: title = args[1] fallthrough case 1: filename = args[0] default: - return tool.CommandLineErrorf("codelens expects at most two arguments") + return commandLineErrorf("codelens expects at most two arguments") } r.app.editFlags = &r.EditFlags // in case a codelens perform an edit
diff --git a/gopls/internal/cmd/definition.go b/gopls/internal/cmd/definition.go index 26dd86f..99fe5dc 100644 --- a/gopls/internal/cmd/definition.go +++ b/gopls/internal/cmd/definition.go
@@ -14,11 +14,10 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/tool" ) -// A Definition is the result of a 'definition' query. -type Definition struct { +// A definitionJSON is the result of a 'definition' query. +type definitionJSON struct { Span span `json:"span"` // span of the definition Description string `json:"description"` // description of the denoted object } @@ -34,7 +33,7 @@ // definition implements the definition verb for gopls. type definition struct { - app *Application + app *application JSON bool `flag:"json" help:"emit output in JSON format"` MarkdownSupported bool `flag:"markdown" help:"support markdown in responses"` @@ -60,7 +59,7 @@ // results to stdout. func (d *definition) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("definition expects 1 argument") + return commandLineErrorf("definition expects 1 argument") } // Plaintext makes more sense for the command line. opts := d.app.options @@ -119,7 +118,7 @@ description = strings.TrimSpace(hover.Contents.Value) } - result := &Definition{ + result := &definitionJSON{ Span: definition, Description: description, }
diff --git a/gopls/internal/cmd/execute.go b/gopls/internal/cmd/execute.go index 0c40e59..c139635 100644 --- a/gopls/internal/cmd/execute.go +++ b/gopls/internal/cmd/execute.go
@@ -15,15 +15,14 @@ "golang.org/x/tools/gopls/internal/filecache" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/protocol/command" - "golang.org/x/tools/gopls/internal/tool" + protocolcommand "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/util/bug" ) // execute implements the LSP ExecuteCommand verb for gopls. type execute struct { EditFlags - app *Application + app *application } func (e *execute) Name() string { return "execute" } @@ -48,6 +47,7 @@ execute-flags: `) + printFlagDefaults(f) } @@ -62,11 +62,11 @@ } if len(args) == 0 { - return tool.CommandLineErrorf("execute requires a command name") + return commandLineErrorf("execute requires a command name") } cmd := args[0] - if !slices.Contains(command.Commands, command.Command(cmd)) { - return tool.CommandLineErrorf("unrecognized command: %s", cmd) + if !slices.Contains(protocolcommand.Commands, protocolcommand.Command(cmd)) { + return commandLineErrorf("unrecognized command: %s", cmd) } // A command may have multiple arguments, though the only one
diff --git a/gopls/internal/cmd/export_test.go b/gopls/internal/cmd/export_test.go new file mode 100644 index 0000000..800a9d7 --- /dev/null +++ b/gopls/internal/cmd/export_test.go
@@ -0,0 +1,24 @@ +// Copyright 2026 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 exports unexported internal symbols exclusively +// for use in package cmd_test. + +package cmd + +type ( + DefinitionJSON = definitionJSON + StatsJSON = statsJSON +) + +// CommandNames returns the names of all commands, including the root command. +func CommandNames() []string { + var names []string + app := newApplication() + for _, c := range app.Commands() { + names = append(names, c.Name()) + } + names = append(names, app.Name()) + return names +}
diff --git a/gopls/internal/cmd/folding_range.go b/gopls/internal/cmd/folding_range.go index e2c2f4b..37c9617 100644 --- a/gopls/internal/cmd/folding_range.go +++ b/gopls/internal/cmd/folding_range.go
@@ -10,12 +10,11 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // foldingRanges implements the folding_ranges verb for gopls type foldingRanges struct { - app *Application + app *application } func (r *foldingRanges) Name() string { return "folding_ranges" } @@ -33,7 +32,7 @@ func (r *foldingRanges) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("folding_ranges expects 1 argument (file)") + return commandLineErrorf("folding_ranges expects 1 argument (file)") } cli, _, err := r.app.connect(ctx)
diff --git a/gopls/internal/cmd/format.go b/gopls/internal/cmd/format.go index 8766d9c..16da4ff 100644 --- a/gopls/internal/cmd/format.go +++ b/gopls/internal/cmd/format.go
@@ -15,7 +15,7 @@ // format implements the format verb for gopls. type format struct { EditFlags - app *Application + app *application } func (c *format) Name() string { return "format" }
diff --git a/gopls/internal/cmd/harness.go b/gopls/internal/cmd/harness.go new file mode 100644 index 0000000..134814c --- /dev/null +++ b/gopls/internal/cmd/harness.go
@@ -0,0 +1,439 @@ +// 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. + +package cmd + +import ( + "context" + "flag" + "fmt" + "io" + "log" + "os" + "reflect" + "runtime" + "runtime/pprof" + "runtime/trace" + "slices" + "strings" + "time" + + "golang.org/x/tools/gopls/internal/debug" + "golang.org/x/tools/gopls/internal/filecache" +) + +// This file defines common flags and helper functions +// that coordinate flag registration via reflection. + +// ProfileFlags can be embedded in your application struct to automatically +// add command line arguments and handling for common profiling methods. +type ProfileFlags struct { + CPU string `flag:"profile.cpu" help:"write CPU profile to this file"` + Memory string `flag:"profile.mem" help:"write memory profile to this file"` + Alloc string `flag:"profile.alloc" help:"write alloc profile to this file"` + Trace string `flag:"profile.trace" help:"write trace log to this file"` + Block string `flag:"profile.block" help:"write block profile to this file"` +} + +// command represents an executable CLI command or subcommand within gopls. +type command interface { + // Name returns the command's name. It is used in help and error messages. + Name() string + // Most of the help usage is automatically generated, this string should only + // describe the contents of non flag arguments. + Usage() string + // ShortHelp returns the one line overview of the command. + ShortHelp() string + // DetailedHelp should print a detailed help message. It will only ever be shown + // when the ShortHelp is also printed, so there is no need to duplicate + // anything from there. + // It is passed the flag set so it can print the default values of the flags. + // It should use the flag sets configured Output to write the help to. + DetailedHelp(*flag.FlagSet) + // Run is invoked after all flag processing, and inside the profiling and + // error handling harness. + Run(ctx context.Context, args ...string) error +} + +type subcommand interface { + // TODO(hyangah): merge with command. It is unclear why we need + // to keep command and subcommand separate. + + command + Parent() string +} + +// This is the type returned by commandLineErrorf, which causes the outer main +// to trigger printing of the command line help. +type commandLineError string + +func (e commandLineError) Error() string { return string(e) } + +// commandLineErrorf is like fmt.Errorf except that it returns a value that +// triggers printing of the command line help. +// In general you should use this when generating command line validation errors. +func commandLineErrorf(message string, args ...any) error { + return commandLineError(fmt.Sprintf(message, args...)) +} + +// Main is the main entry point for the gopls application, called by gopls main. +// It never returns. +func Main() { + ctx := context.Background() + args := os.Args[1:] + app := newApplication() + cmd, globalArgs, cmdArgs, err := normalize(app, args) + if err != nil { + if cmd == nil { + cmd = app + } + fmt.Fprintf(os.Stderr, "%s: %v\n", cmdPath(cmd), err) + if isCommandLineError(err) { + printCommandHelp(os.Stderr, cmd) + } + os.Exit(2) + } + + parseFlags(app, globalArgs) + cmdFlags := parseFlags(cmd, cmdArgs) + + err = runWithProfile(&app.ProfileFlags, func() error { + // In the category of "things we can do while waiting for the + // Go command": + + // Pre-initialize the filecache as early as possible. + // The work (hashing the executable, starting GC, and checking cache access) + // is done in a separate goroutine, so it doesn't block startup of short-lived + // commands (like help/version) which will exit before the goroutine finishes. + // Concurrent gopls processes (e.g. CLI and daemon) won't conflict because + // GC is delayed and concurrent writes to the shared cache are handled safely. + filecache.Start() + + ctx = debug.WithInstance(ctx, app.OTel) + + return cmd.Run(ctx, cmdFlags.Args()...) + }) + if err != nil { + fmt.Fprintf(os.Stderr, "gopls: %v\n", err) + if isCommandLineError(err) { + printCommandHelp(os.Stderr, cmd) + } + os.Exit(2) + } + os.Exit(0) +} + +// isCommandLineError reports whether the error was created by [commandLineErrorf]. +func isCommandLineError(err error) bool { + _, ok := err.(commandLineError) + return ok +} + +// cmdPath returns the full command path (e.g. "gopls remote debug") for target. +func cmdPath(target command) string { + if sub, ok := target.(subcommand); ok && sub.Parent() != "" { + return sub.Parent() + " " + target.Name() + } + return target.Name() +} + +// printHelp prints the usage and detailed help for any command to s.Output(). +func printHelp(s *flag.FlagSet, cmd command) { + if _, ok := cmd.(*application); !ok { + printCommandHelp(s.Output(), cmd) + } + cmd.DetailedHelp(s) +} + +// printCommandHelp prints a concise usage summary for cmd to w. +func printCommandHelp(w io.Writer, cmd command) { + if _, ok := cmd.(*application); ok { + fmt.Fprintln(w, "Usage:\n gopls help [<subject>]") + return + } + if short := cmd.ShortHelp(); short != "" { + fmt.Fprintf(w, "%s\n\n", short) + } + fmt.Fprintf(w, "Usage:\n gopls [flags] %s", strings.TrimPrefix(cmdPath(cmd), "gopls ")) + if usage := cmd.Usage(); usage != "" { + fmt.Fprintf(w, " %s", usage) + } + fmt.Fprintln(w) +} + +// runWithProfile executes fn with active CPU, trace, memory, alloc, or block profiling +// if requested in p. +func runWithProfile(p *ProfileFlags, fn func() error) (resultErr error) { + if p.CPU != "" { + f, err := os.Create(p.CPU) + if err != nil { + return err + } + if err := pprof.StartCPUProfile(f); err != nil { + f.Close() // ignore error + return err + } + defer func() { + pprof.StopCPUProfile() + if closeErr := f.Close(); resultErr == nil { + resultErr = closeErr + } + }() + } + + if p.Trace != "" { + f, err := os.Create(p.Trace) + if err != nil { + return err + } + if err := trace.Start(f); err != nil { + f.Close() // ignore error + return err + } + defer func() { + trace.Stop() + if closeErr := f.Close(); resultErr == nil { + resultErr = closeErr + } + log.Printf("To view the trace, run:\n$ go tool trace view %s", p.Trace) + }() + } + + if p.Memory != "" { + f, err := os.Create(p.Memory) + if err != nil { + return err + } + defer func() { + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + log.Printf("Writing memory profile: %v", err) + } + if err := f.Close(); err != nil { + log.Printf("Closing memory profile: %v", err) + } + }() + } + + if p.Alloc != "" { + f, err := os.Create(p.Alloc) + if err != nil { + return err + } + defer func() { + if err := pprof.Lookup("allocs").WriteTo(f, 0); err != nil { + log.Printf("Writing alloc profile: %v", err) + } + if err := f.Close(); err != nil { + log.Printf("Closing alloc profile: %v", err) + } + }() + } + + if p.Block != "" { + f, err := os.Create(p.Block) + if err != nil { + return err + } + runtime.SetBlockProfileRate(1) // record all blocking events + defer func() { + if err := pprof.Lookup("block").WriteTo(f, 0); err != nil { + log.Printf("Writing block profile: %v", err) + } + if err := f.Close(); err != nil { + log.Printf("Closing block profile: %v", err) + } + }() + } + return fn() +} + +// addFlags scans fields of structs recursively to find things with flag tags +// and add them to the flag set. +func addFlags(f *flag.FlagSet, field reflect.StructField, value reflect.Value) *ProfileFlags { + // is it a field we are allowed to reflect on? + if field.PkgPath != "" { + return nil + } + // now see if is actually a flag + flagNames, isFlag := field.Tag.Lookup("flag") + help := field.Tag.Get("help") + if isFlag { + nameList := strings.Split(flagNames, ",") + // add the main flag + addFlag(f, value, nameList[0], help) + if len(nameList) > 1 { + // and now add any aliases using the same flag value + fv := f.Lookup(nameList[0]).Value + for _, flagName := range nameList[1:] { + f.Var(fv, flagName, help) + } + } + return nil + } + // not a flag, but it might be a struct with flags in it + value = resolve(value.Elem()) + if value.Kind() != reflect.Struct { + return nil + } + + // TODO(adonovan): there's no need for this special treatment of Profile: + // The caller can use f.Lookup("profile.cpu") etc instead. + p, _ := value.Addr().Interface().(*ProfileFlags) + // go through all the fields of the struct + for i := 0; i < value.Type().NumField(); i++ { + child := value.Type().Field(i) + v := value.Field(i) + // make sure we have a pointer + if v.Kind() != reflect.Pointer { + v = v.Addr() + } + // check if that field is a flag or contains flags + if fp := addFlags(f, child, v); fp != nil { + p = fp + } + } + return p +} + +func addFlag(f *flag.FlagSet, value reflect.Value, flagName string, help string) { + switch v := value.Interface().(type) { + case flag.Value: + f.Var(v, flagName, help) + case *bool: + f.BoolVar(v, flagName, *v, help) + case *time.Duration: + f.DurationVar(v, flagName, *v, help) + case *float64: + f.Float64Var(v, flagName, *v, help) + case *int64: + f.Int64Var(v, flagName, *v, help) + case *int: + f.IntVar(v, flagName, *v, help) + case *string: + f.StringVar(v, flagName, *v, help) + case *uint: + f.UintVar(v, flagName, *v, help) + case *uint64: + f.Uint64Var(v, flagName, *v, help) + default: + log.Fatalf("field %q of type %T is not assignable to flag.Value", flagName, v) + } +} + +func resolve(v reflect.Value) reflect.Value { + for { + switch v.Kind() { + case reflect.Interface, reflect.Pointer: + v = v.Elem() + default: + return v + } + } +} + +// parseFlags creates, configures, and parses a FlagSet for cmd using args. +// If parsing fails or help is requested, it prints contextual help and exits. +func parseFlags(cmd command, args []string) *flag.FlagSet { + // We use ContinueOnError and discard initial error output so we can intercept flag errors + // and produce contextual, user-friendly diagnostic messages rather than standard Go flag usage. + fs := flag.NewFlagSet(cmd.Name(), flag.ContinueOnError) + fs.SetOutput(io.Discard) + addCommandFlags(fs, cmd) + err := fs.Parse(args) + if err == nil { + return fs + } + + if err == flag.ErrHelp { + // POSIX convention requires writing explicit help requests + // (-h/-help) to stdout on exit 0. + fs.SetOutput(os.Stdout) + printHelp(fs, cmd) + os.Exit(0) + } + + fs.SetOutput(os.Stderr) + // When standard flag parsing fails due to an undefined flag, + // inspect command hierarchy so we can guide the user + // if they misplaced a flag before or after a subcommand. + if prefix := "flag provided but not defined: -"; strings.HasPrefix(err.Error(), prefix) { + checkMisplacedFlag(fs, cmd, strings.TrimPrefix(err.Error(), prefix)) + } + + // Fallback diagnostic for general flag syntax errors + // or truly unknown flags. + fmt.Fprintf(os.Stderr, "%s: %v\n", cmdPath(cmd), err) + printCommandHelp(os.Stderr, cmd) + os.Exit(2) + return nil +} + +// findCommandByName searches the command tree starting from root for a command named name. +func findCommandByName(root command, name string) command { + if root.Name() == name { + return root + } + for _, sub := range getSubcommands(root) { + if sub.Name() == name { + return sub + } + if found := findCommandByName(sub, name); found != nil { + return found + } + } + return nil +} + +// checkMisplacedFlag inspects ancestors and descendants to diagnose undefined flag errors. +// If a misplaced flag is found, it prints where the flag belongs and exits with code 2. +func checkMisplacedFlag(fs *flag.FlagSet, cmd command, name string) { + // Check descendants: e.g. placing a subcommand flag before specifying the subcommand. + if sub := findSubcommandWithFlag(cmd, name); sub != nil { + fmt.Fprintf(os.Stderr, "%s: flag -%s belongs to subcommand %s\n", cmdPath(cmd), name, sub.Name()) + printCommandHelp(fs.Output(), cmd) + os.Exit(2) + } + + // Walk up ancestors via lineage string: e.g. placing a global application flag after the subcommand name. + // Strict flag ordering requires parent/global flags to precede subcommands. + if sub, ok := cmd.(subcommand); ok && sub.Parent() != "" { + root := newApplication() + for _, currName := range slices.Backward(strings.Fields(sub.Parent())) { + curr := findCommandByName(root, currName) + if curr != nil && hasFlag(curr, name) { + fmt.Fprintf(os.Stderr, "%s: flag -%s must be placed before subcommand %s (after %s)\n", cmdPath(cmd), name, cmd.Name(), currName) + printCommandHelp(fs.Output(), cmd) + os.Exit(2) + } + } + } +} + +// findSubcommandWithFlag recursively searches getSubcommands(target) to check +// if flagName is registered on any child or descendant subcommand. +func findSubcommandWithFlag(target command, flagName string) command { + for _, sub := range getSubcommands(target) { + if hasFlag(sub, flagName) { + return sub + } + if found := findSubcommandWithFlag(sub, flagName); found != nil { + return found + } + } + return nil +} + +// hasFlag reports whether flagName is registered on cmd. +func hasFlag(cmd command, flagName string) bool { + fs := flag.NewFlagSet(cmd.Name(), flag.ContinueOnError) + fs.SetOutput(io.Discard) + addCommandFlags(fs, cmd) + return fs.Lookup(flagName) != nil +} + +// addCommandFlags registers the flags defined in the app struct onto the FlagSet. +func addCommandFlags(f *flag.FlagSet, app command) *ProfileFlags { + return addFlags(f, reflect.StructField{}, reflect.ValueOf(app)) +}
diff --git a/gopls/internal/cmd/help_test.go b/gopls/internal/cmd/help_test.go index 43f9daf..946a28f 100644 --- a/gopls/internal/cmd/help_test.go +++ b/gopls/internal/cmd/help_test.go
@@ -29,21 +29,19 @@ func TestHelpFiles(t *testing.T) { testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. t.Parallel() - app := cmd.New() tree := writeTree(t, "") - for _, cmd := range append(app.Commands(), app) { - name := cmd.Name() + for _, name := range cmd.CommandNames() { t.Run(name, func(t *testing.T) { t.Parallel() args := []string{name, "-h"} // The output of 'gopls -h' is in usage.hlp - if cmd == app { + if name == "gopls" { args = args[1:] name = "usage" } res := gopls(t, tree, args...) res.checkExit(true) // -h should result in exit 0 - got := res.stderr + got := res.stdout helpFile := filepath.Join("usage", name+".hlp") if *updateHelpFiles { if err := os.WriteFile(helpFile, []byte(got), 0666); err != nil { @@ -67,7 +65,7 @@ tree := writeTree(t, "") res := gopls(t, tree, "-v", "-h") res.checkExit(true) // -h should result in exit 0 - got := res.stderr + got := res.stdout helpFile := filepath.Join("usage", "usage-v.hlp") if *updateHelpFiles { if err := os.WriteFile(helpFile, []byte(got), 0666); err != nil { @@ -140,9 +138,16 @@ t.Run(strings.Join(test.args, " "), func(t *testing.T) { res := gopls(t, tree, test.args...) res.checkExit(test.wantSuccess) - res.checkStdout("^$") // no stdout - for _, pattern := range test.wantPatterns { - res.checkStderr(pattern) + if test.wantSuccess { + res.checkStderr("^$") // no stderr + for _, pattern := range test.wantPatterns { + res.checkStdout(pattern) + } + } else { + res.checkStdout("^$") // no stdout + for _, pattern := range test.wantPatterns { + res.checkStderr(pattern) + } } }) }
diff --git a/gopls/internal/cmd/highlight.go b/gopls/internal/cmd/highlight.go index 26c7fe6..6675336 100644 --- a/gopls/internal/cmd/highlight.go +++ b/gopls/internal/cmd/highlight.go
@@ -10,12 +10,11 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // highlight implements the highlight verb for gopls. type highlight struct { - app *Application + app *application } func (r *highlight) Name() string { return "highlight" } @@ -35,7 +34,7 @@ func (r *highlight) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("highlight expects 1 argument (position)") + return commandLineErrorf("highlight expects 1 argument (position)") } cli, _, err := r.app.connect(ctx)
diff --git a/gopls/internal/cmd/implementation.go b/gopls/internal/cmd/implementation.go index 0c845f4..8332603 100644 --- a/gopls/internal/cmd/implementation.go +++ b/gopls/internal/cmd/implementation.go
@@ -11,12 +11,11 @@ "sort" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // implementation implements the implementation verb for gopls type implementation struct { - app *Application + app *application } func (i *implementation) Name() string { return "implementation" } @@ -36,7 +35,7 @@ func (i *implementation) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("implementation expects 1 argument (position)") + return commandLineErrorf("implementation expects 1 argument (position)") } cli, _, err := i.app.connect(ctx)
diff --git a/gopls/internal/cmd/imports.go b/gopls/internal/cmd/imports.go index 0b8f143..8a29976 100644 --- a/gopls/internal/cmd/imports.go +++ b/gopls/internal/cmd/imports.go
@@ -10,13 +10,12 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // imports implements the import verb for gopls. type imports struct { EditFlags - app *Application + app *application } func (t *imports) Name() string { return "imports" } @@ -40,7 +39,7 @@ // - otherwise, prints the new versions to stdout. func (t *imports) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("imports expects 1 argument") + return commandLineErrorf("imports expects 1 argument") } t.app.editFlags = &t.EditFlags cli, _, err := t.app.connect(ctx)
diff --git a/gopls/internal/cmd/info.go b/gopls/internal/cmd/info.go index b099046..1f3a244 100644 --- a/gopls/internal/cmd/info.go +++ b/gopls/internal/cmd/info.go
@@ -18,12 +18,11 @@ "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/doc" licensespkg "golang.org/x/tools/gopls/internal/licenses" - "golang.org/x/tools/gopls/internal/tool" ) // help implements the help command. type help struct { - app *Application + app *application } func (h *help) Name() string { return "help" } @@ -43,7 +42,7 @@ // Run prints help information about a subcommand. func (h *help) Run(ctx context.Context, args ...string) error { - find := func(cmds []tool.Application, name string) tool.Application { + find := func(cmds []command, name string) command { for _, cmd := range cmds { if cmd.Name() == name { return cmd @@ -53,27 +52,31 @@ } // Find the subcommand denoted by args (empty => h.app). - var cmd tool.Application = h.app + var cmd command = h.app for i, arg := range args { cmd = find(getSubcommands(cmd), arg) if cmd == nil { - return tool.CommandLineErrorf( + return commandLineErrorf( "no such subcommand: %s", strings.Join(args[:i+1], " ")) } } // 'gopls help cmd subcmd' is equivalent to 'gopls cmd subcmd -h'. - // The flag package prints the usage information (defined by tool.Run) - // when it sees the -h flag. - fs := flag.NewFlagSet(cmd.Name(), flag.ExitOnError) - return tool.Run(ctx, fs, h.app, append(args[:len(args):len(args)], "-h")) + // parseFlags prints the usage information when it sees the -h flag. + // + // TODO(hyangah): should we treat `gopls help cmd` and `gopls cmd -h` + // differently? For example, `gopls help cmd` can give a long help + // that explains a lot more details (DetailedHelp) than + // `gopls cmd -h` outputs (ShortHelp). + parseFlags(cmd, []string{"-h"}) + return nil } // version implements the version command. type version struct { JSON bool `flag:"json" help:"outputs in json format."` - app *Application + app *application } func (v *version) Name() string { return "version" } @@ -98,7 +101,7 @@ } type apiJSON struct { - app *Application + app *application } func (j *apiJSON) Name() string { return "api-json" } @@ -121,7 +124,7 @@ } type licenses struct { - app *Application + app *application } func (l *licenses) Name() string { return "licenses" }
diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index 170629c..845b891 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go
@@ -27,7 +27,6 @@ import ( "bytes" - "context" "encoding/json" "fmt" "math/rand" @@ -41,7 +40,6 @@ "golang.org/x/tools/gopls/internal/cmd" "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/version" "golang.org/x/tools/internal/testenv" @@ -84,6 +82,17 @@ } } +func TestProfileFlags(t *testing.T) { + t.Parallel() + tree := writeTree(t, "") + cpuProfile := filepath.Join(t.TempDir(), "cpu.prof") + res := gopls(t, tree, "-profile.cpu="+cpuProfile, "version") + res.checkExit(true) + if info, err := os.Stat(cpuProfile); err != nil || info.Size() == 0 { + t.Errorf("expected non-empty profile file %s, got err=%v", cpuProfile, err) + } +} + // TestCheck tests the 'check' subcommand (check.go). func TestCheck(t *testing.T) { t.Parallel() @@ -302,7 +311,7 @@ { res := gopls(t, tree, "definition", "-json", "-markdown", "a.go:4:7") res.checkExit(true) - var defn cmd.Definition + var defn cmd.DefinitionJSON if res.toJSON(&defn) { if !strings.HasPrefix(defn.Description, "```go\nfunc fmt.Println") { t.Errorf("Description does not start with markdown code block. Got: %s", defn.Description) @@ -878,7 +887,7 @@ res := gopls(t, tree, "stats") res.checkExit(true) - var stats cmd.GoplsStats + var stats cmd.StatsJSON if err := json.Unmarshal([]byte(res.stdout), &stats); err != nil { t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) } @@ -930,7 +939,7 @@ res2 := gopls(t, tree, "stats", "-anon") res2.checkExit(true) - var stats2 cmd.GoplsStats + var stats2 cmd.StatsJSON if err := json.Unmarshal([]byte(res2.stdout), &stats2); err != nil { t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) } @@ -1093,6 +1102,57 @@ } } +func TestCommandLineErrors(t *testing.T) { + testenv.NeedsGoBuild(t) + t.Parallel() + tree := writeTree(t, "") + for _, tc := range []struct { + name string + args []string + wantErrs []string + }{ + { + name: "MissingPositionalArgShowsUsage", + args: []string{"definition"}, + wantErrs: []string{"definition expects 1 argument", "Usage:\n gopls \\[flags\\] definition \\[definition-flags\\] <position>"}, + }, + { + name: "GlobalFlagAfterServe", + args: []string{"serve", "-otel=http://localhost:4318"}, + wantErrs: []string{`flag -otel must be placed before subcommand serve \(after gopls\)`}, + }, + { + name: "MisplacedContainerFlag", + args: []string{"remote", "-remote=localhost:12345", "sessions"}, + wantErrs: []string{`flag -remote must be placed before subcommand remote \(after gopls\)`}, + }, + + { + name: "UnknownSubcommandFlag", + args: []string{"-v", "execute", "-unknown"}, + wantErrs: []string{"flag provided but not defined: -unknown"}, + }, + { + name: "GlobalFlagAfterSubcommand", + args: []string{"references", "-v", "./gopls/main.go:35:8"}, + wantErrs: []string{`flag -v must be placed before subcommand references \(after gopls\)`}, + }, + { + name: "GlobalFlagAfterNestedSubcommand", + args: []string{"remote", "debug", "-v"}, + wantErrs: []string{`gopls remote debug: flag -v must be placed before subcommand debug \(after gopls\)`}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + res := gopls(t, tree, tc.args...) + res.checkExit(false) + for _, wantErr := range tc.wantErrs { + res.checkStderr(wantErr) + } + }) + } +} + // -- test framework -- func TestMain(m *testing.M) { @@ -1116,7 +1176,7 @@ version.VersionOverride = v } - tool.Main(context.Background(), cmd.New(), os.Args[1:]) + cmd.Main() } // writeTree extracts a txtar archive into a new directory and returns its path.
diff --git a/gopls/internal/cmd/links.go b/gopls/internal/cmd/links.go index 3b08868..4681793 100644 --- a/gopls/internal/cmd/links.go +++ b/gopls/internal/cmd/links.go
@@ -12,14 +12,13 @@ "os" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // links implements the links verb for gopls. type links struct { JSON bool `flag:"json" help:"emit document links in JSON format"` - app *Application + app *application } func (l *links) Name() string { return "links" } @@ -42,7 +41,7 @@ // - otherwise, prints the a list of unique links func (l *links) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("links expects 1 argument") + return commandLineErrorf("links expects 1 argument") } cli, _, err := l.app.connect(ctx) if err != nil {
diff --git a/gopls/internal/cmd/mcp.go b/gopls/internal/cmd/mcp.go index a35f049..1175ae0 100644 --- a/gopls/internal/cmd/mcp.go +++ b/gopls/internal/cmd/mcp.go
@@ -21,7 +21,7 @@ ) type headlessMCP struct { - app *Application + app *application Address string `flag:"listen" help:"the address on which to run the mcp server"` Logfile string `flag:"logfile" help:"filename to log to; if unset, logs to stderr"` @@ -47,7 +47,12 @@ } func (m *headlessMCP) Run(ctx context.Context, args ...string) error { + // TODO(hxjiang): properly support remote mode (https://github.com/golang/go/issues/78668). + if m.app.Remote != "" { + return commandLineErrorf("mcp does not currently support remote mode") + } if m.Instructions { + fmt.Println(internalmcp.Instructions) return nil }
diff --git a/gopls/internal/cmd/mcp_test.go b/gopls/internal/cmd/mcp_test.go index 2b1c63c..6071804 100644 --- a/gopls/internal/cmd/mcp_test.go +++ b/gopls/internal/cmd/mcp_test.go
@@ -10,13 +10,11 @@ "context" "encoding/json" "fmt" - "net" "os" "os/exec" "path/filepath" "runtime" "slices" - "strconv" "strings" "testing" "time" @@ -205,9 +203,7 @@ func MyFun() {} `) - port := strconv.Itoa(getRandomPort()) - addr := "localhost:" + port - goplsCmd := exec.Command(os.Args[0], "-v", "mcp", "-listen="+addr) + goplsCmd := exec.Command(os.Args[0], "-v", "mcp", "-listen=localhost:0") goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain") goplsCmd.Dir = tree goplsCmd.Stdout = os.Stderr @@ -232,18 +228,19 @@ goplsCmd.Wait() }() - // Wait for the MCP server to start listening. The referenced log occurs - // after the connection is opened via net.Listen and the HTTP handlers are - // set up. - ready := make(chan bool) + // Wait for the MCP server to start listening and send the address through + // the channel. The referenced log occurs after the connection is opened via + // net.Listen and the HTTP handlers are set up. + ready := make(chan string, 1) go func() { - // Copy from the pipe to stderr, keeping an eye out for the "mcp http - // server listening" string. + defer close(ready) + // Copy from the pipe to stderr, keeping an eye out for the "Gopls MCP + // server: listening on <addr>" string. scan := bufio.NewScanner(stderr) for scan.Scan() { line := scan.Text() - if strings.Contains(line, "mcp http server listening") { - ready <- true + if _, after, ok := strings.Cut(line, "Gopls MCP server: listening on "); ok { + ready <- strings.TrimSpace(after) } fmt.Fprintln(os.Stderr, line) } @@ -252,7 +249,16 @@ } }() - <-ready + var addr string + select { + case addr = <-ready: + if addr == "" { + t.Fatalf("gopls mcp server exited without starting") + } + case <-time.After(60 * time.Second): + t.Fatalf("timed out waiting for gopls mcp server to start") + } + client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) ctx := t.Context() mcpSession, err := client.Connect(ctx, &mcp.SSEClientTransport{Endpoint: "http://" + addr}, nil) @@ -421,18 +427,6 @@ return buf.String() } -// getRandomPort returns the number of a random available port. Inherently racy: -// nothing stops another process from listening on it - but this should be fine -// for testing purposes. -func getRandomPort() int { - listener, err := net.Listen("tcp", "localhost:0") - if err != nil { - panic(err) - } - defer listener.Close() - return listener.Addr().(*net.TCPAddr).Port -} - // supportsFsnotify returns true if fsnotify supports the os. func supportsFsnotify(os string) bool { return os == "darwin" || os == "linux" || os == "windows"
diff --git a/gopls/internal/cmd/normalize.go b/gopls/internal/cmd/normalize.go new file mode 100644 index 0000000..892c340 --- /dev/null +++ b/gopls/internal/cmd/normalize.go
@@ -0,0 +1,152 @@ +// Copyright 2026 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 cmd + +import ( + "flag" + "io" + "strings" +) + +// normalize scans command-line arguments to find the top-level subcommand, +// separating global application flags from subcommand arguments without executing FlagSet.Parse. +// +// Returns: +// - cmd: the resolved target subcommand (or &app.serve if none is specified). +// If flag validation errors, cmd is returned populated so callers can display contextual command help. +// - globalArgs: flags belonging to global application scope. +// - cmdArgs: arguments and flags belonging to cmd. +func normalize(app *application, args []string) (cmd command, globalArgs, cmdArgs []string, err error) { + silentFlagSet := func(name string, cmds ...command) *flag.FlagSet { + fs := flag.NewFlagSet(name, flag.ContinueOnError) + fs.SetOutput(io.Discard) + for _, c := range cmds { + addCommandFlags(fs, c) + } + return fs + } + + findSubcommand := func(curr command, name string) command { + for _, c := range getSubcommands(curr) { + if c.Name() == name { + return c + } + } + return nil + } + + // Flag sets to be used for flag name/type lookup. + appFlagSet := silentFlagSet(app.Name(), app) + serveFlagSet := silentFlagSet("serve", &app.serve) + + var preServeArgs []string + + i := 0 + for i < len(args) { + arg := args[i] + + if arg == "--" { + // e.g. gopls -v -- check file.go + // gopls vulncheck -- -mode=... + if i+1 < len(args) { + if matched := findSubcommand(app, args[i+1]); matched != nil { + cmd = matched + i++ // skip "--" + cmdArgs = append(cmdArgs, args[i+1:]...) + break + } + } + i++ + cmd = &app.serve + cmdArgs = append(cmdArgs, args[i:]...) + break + } + + // expect a valid subcommand if not a flag. + if arg == "-" || !strings.HasPrefix(arg, "-") { + if matched := findSubcommand(app, arg); matched != nil { + cmd = matched + i++ + cmdArgs = append(cmdArgs, args[i:]...) + break + } + break + } + + // below: arg is a string that has "-" as the prefix. + cleanArg := strings.TrimPrefix(strings.TrimPrefix(arg, "-"), "-") + name, _, hasValue := strings.Cut(cleanArg, "=") + + if name == "h" || name == "help" { // -h or -help + globalArgs = append(globalArgs, arg) + i++ + continue + } + + if appFlag := appFlagSet.Lookup(name); appFlag != nil { + consumed, err := consume(args, i, appFlag, name, hasValue) + if err != nil { + return nil, nil, nil, err + } + globalArgs = append(globalArgs, consumed...) + i += len(consumed) + continue + } + if serveFlag := serveFlagSet.Lookup(name); serveFlag != nil { + consumed, err := consume(args, i, serveFlag, name, hasValue) + if err != nil { + return nil, nil, nil, err + } + preServeArgs = append(preServeArgs, consumed...) + i += len(consumed) + continue + } + return nil, nil, nil, commandLineErrorf("unknown flag: %s", arg) + } + + if cmd == nil { + if i < len(args) { + return nil, nil, nil, commandLineErrorf("unknown command %q", args[i]) + } + cmd = &app.serve + cmdArgs = preServeArgs + } else if cmd.Name() == "serve" { + // For backwards compatibility, allow flags to be placed after serve. + cmdArgs = append(preServeArgs, cmdArgs...) + } else if len(preServeArgs) > 0 { + // All explicitly specified subcommands other than serve must + // follow strict flag ordering. + arg := preServeArgs[0] + cleanArg := strings.TrimPrefix(strings.TrimPrefix(arg, "-"), "-") + name, _, _ := strings.Cut(cleanArg, "=") + if hasFlag(cmd, name) { + return cmd, nil, nil, commandLineErrorf("flag -%s must be placed after the command %s", name, cmd.Name()) + } + if sub := findSubcommandWithFlag(app, name); sub != nil { + return cmd, nil, nil, commandLineErrorf("flag -%s belongs to subcommand %s", name, sub.Name()) + } + + return cmd, nil, nil, commandLineErrorf("flag provided but not defined: -%s", name) + } + + return cmd, globalArgs, cmdArgs, nil +} + +// consume returns the token(s) corresponding to flag f from args starting at index i. +func consume(args []string, i int, f *flag.Flag, name string, hasValue bool) ([]string, error) { + if hasValue || isBoolFlag(f) { + return args[i : i+1], nil + } + if i+1 >= len(args) { + return nil, commandLineErrorf("flag needs an argument: -%s", name) + } + return args[i : i+2], nil +} + +// isBoolFlag reports whether f is a boolean flag. +func isBoolFlag(f *flag.Flag) bool { + bf, ok := f.Value.(interface{ IsBoolFlag() bool }) + return ok && bf.IsBoolFlag() +}
diff --git a/gopls/internal/cmd/normalize_test.go b/gopls/internal/cmd/normalize_test.go new file mode 100644 index 0000000..0d45df5 --- /dev/null +++ b/gopls/internal/cmd/normalize_test.go
@@ -0,0 +1,341 @@ +// Copyright 2026 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 cmd + +import ( + "slices" + "strings" + "testing" +) + +// TODO(adonovan): turn this into an integration test. Tests of +// internal helper functions are not robust nor do they reflect actual +// application behavior when state (e.g. flags) is involved. +// +// Also, check that the correct usage message is generated. +func TestNormalize(t *testing.T) { + tests := []struct { + name string + args []string + wantCmd string + wantGlobalArgs []string + wantArgs []string + wantErr string + }{ + // ========================================================== + // Implicit & Explicit Serve + // ========================================================== + { + name: "VSCodeGo_Default", + args: []string{}, + wantCmd: "serve", + }, + { + name: "GlobalFlagDefaultsToServe", + args: []string{"-v"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-v"}, + }, + { + name: "VSCodeGo_RPCTrace", + args: []string{"-rpc.trace"}, + wantCmd: "serve", + wantArgs: []string{"-rpc.trace"}, + }, + { + name: "VSCodeGo_RPCTraceServe", + args: []string{"-rpc.trace", "serve"}, + wantCmd: "serve", + wantArgs: []string{"-rpc.trace"}, + }, + { + name: "XTools_ServeDebug", + args: []string{"serve", "-debug=localhost:6060"}, + wantCmd: "serve", + wantArgs: []string{"-debug=localhost:6060"}, + }, + { + name: "GlobalFlagBeforeExplicitServe", + args: []string{"-otel=http://localhost:4318", "serve"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-otel=http://localhost:4318"}, + }, + + // ========================================================== + // Non-Serve Subcommands + // ========================================================== + { + name: "OtherCases_Version", + args: []string{"version"}, + wantCmd: "version", + }, + { + name: "XTools_Help", + args: []string{"help"}, + wantCmd: "help", + }, + { + name: "XTools_References", + args: []string{"references", "./gopls/main.go:35:8"}, + wantCmd: "references", + wantArgs: []string{"./gopls/main.go:35:8"}, + }, + { + name: "ReferencesWithRemoteDebugStrict", + args: []string{"references", "-remote.debug=:0", "./gopls/main.go:35:8"}, + wantCmd: "references", + wantArgs: []string{"-remote.debug=:0", "./gopls/main.go:35:8"}, + }, + { + name: "ModernRemoteSessionsStrict_Valid", + args: []string{"remote", "sessions", "-remote=localhost:12345"}, + wantCmd: "remote", + wantArgs: []string{"sessions", "-remote=localhost:12345"}, + }, + { + name: "ModernRemoteDebugStrict_Valid", + args: []string{"remote", "debug", "-remote=localhost:8082", "localhost:8083"}, + wantCmd: "remote", + wantArgs: []string{"debug", "-remote=localhost:8082", "localhost:8083"}, + }, + { + name: "VSCodeGo_Vulncheck", + args: []string{"vulncheck", "--", "-mode=convert", "-show=color"}, + wantCmd: "vulncheck", + wantArgs: []string{"--", "-mode=convert", "-show=color"}, + }, + { + name: "OtherCases_VerboseExecuteServe", + args: []string{"-v", "execute", "serve"}, + wantCmd: "execute", + wantGlobalArgs: []string{"-v"}, + wantArgs: []string{"serve"}, + }, + { + name: "OtherCases_UnknownAppFlag", + args: []string{"-nope", "execute"}, + wantErr: "unknown flag: -nope", + }, + + // ========================================================== + // Edge cases + // ========================================================== + { + name: "AppFlagWithDoubleDash", + args: []string{"--verbose", "check", "foo.go"}, + wantCmd: "check", + wantGlobalArgs: []string{"--verbose"}, + wantArgs: []string{"foo.go"}, + }, + { + name: "AppFlagWithTripleDash", + args: []string{"---foo"}, + wantErr: "unknown flag: ---foo", + }, + { + name: "ServeFlagWithSpaceArg", + args: []string{"-listen", "localhost:3000"}, + wantCmd: "serve", + wantArgs: []string{"-listen", "localhost:3000"}, + }, + { + name: "LogfileWithSpaceServe", + args: []string{"-logfile", "serve"}, + wantCmd: "serve", + wantArgs: []string{"-logfile", "serve"}, + }, + { + name: "GlobalAndServeFlagsHoist", + args: []string{"-listen=localhost:3000", "-v"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-v"}, + wantArgs: []string{"-listen=localhost:3000"}, + }, + { + name: "GlobalAndServeFlagsHoistMixed", + args: []string{"-listen", "localhost:3000", "-v"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-v"}, + wantArgs: []string{"-listen", "localhost:3000"}, + }, + { + name: "DoubleDashPositional", + args: []string{"--", "foo"}, + wantCmd: "serve", + wantArgs: []string{"foo"}, + }, + { + name: "DoubleDashFlag", + args: []string{"--", "-v"}, + wantCmd: "serve", + wantArgs: []string{"-v"}, + }, + { + name: "UnknownFlagInServe", + args: []string{"-unknown"}, + wantErr: "unknown flag: -unknown", + }, + { + name: "GlobalFlagWithSpaceArgBeforeCheck", + args: []string{"-otel", "http://localhost", "check", "file.go"}, + wantCmd: "check", + wantGlobalArgs: []string{"-otel", "http://localhost"}, + wantArgs: []string{"file.go"}, + }, + { + name: "ImplicitServePositional", + args: []string{"-listen=localhost:3000", "foo"}, + wantErr: `unknown command "foo"`, // consistent with gopls@v0.20.0 + }, + { + name: "RemoteFlagBeforeServeCompat", + args: []string{"-remote=auto", "serve"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-remote=auto"}, + }, + { + name: "RemoteFlagDefaultsToServe", + args: []string{"-remote=auto"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-remote=auto"}, + }, + { + name: "RemoteFlagsDefaultToServe", + args: []string{"-remote=auto", "-remote.debug=localhost:8080"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-remote=auto", "-remote.debug=localhost:8080"}, + }, + + { + name: "RepeatedSubcommandFlags", + args: []string{"-listen", "localhost:3000", "-listen", "localhost:4000"}, + wantCmd: "serve", + wantArgs: []string{"-listen", "localhost:3000", "-listen", "localhost:4000"}, + }, + { + name: "BooleanFlagInlineTrue", + args: []string{"-v=true"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-v=true"}, + }, + { + name: "GlobalFlagTrailingEqual", + args: []string{"-v="}, + wantCmd: "serve", + wantGlobalArgs: []string{"-v="}, + }, + { + name: "ServeFlagTrailingEqual", + args: []string{"-listen="}, + wantCmd: "serve", + wantArgs: []string{"-listen="}, + }, + { + name: "EmptyArgumentImplicitServe", + args: []string{""}, + wantErr: `unknown command ""`, + }, + { + name: "EmptyArgumentAsGlobalFlagValue", + args: []string{"-otel", "", "check"}, + wantCmd: "check", + wantGlobalArgs: []string{"-otel", ""}, + }, + { + name: "RepeatedExplicitServeSubcommand", + args: []string{"serve", "serve"}, + wantCmd: "serve", + wantArgs: []string{"serve"}, + }, + { + name: "GlobalHelpFlagDefaultsToServe", + args: []string{"-help"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-help"}, + }, + + // ========================================================== + // Regression Tests: Reflection-based flag separation & misplaced flags + // ========================================================== + { + name: "SeparateProfileFlagFromServeFlag", + args: []string{"-profile.cpu=cpu.prof", "-listen=localhost:8080"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-profile.cpu=cpu.prof"}, + wantArgs: []string{"-listen=localhost:8080"}, + }, + { + name: "SeparateVeryVerboseAndProfileFromServeFlags", + args: []string{"-vv", "-listen", "localhost:8080", "-profile.mem=mem.prof"}, + wantCmd: "serve", + wantGlobalArgs: []string{"-vv", "-profile.mem=mem.prof"}, + wantArgs: []string{"-listen", "localhost:8080"}, + }, + { + name: "MisplacedServeListenFlagBeforeCheck", + args: []string{"-listen=localhost:8080", "check", "file.go"}, + wantCmd: "check", + wantErr: "flag -listen belongs to subcommand serve", + }, + { + name: "MisplacedServeLogfileFlagBeforeVersion", + args: []string{"-logfile=gopls.log", "version"}, + wantCmd: "version", + wantErr: "flag -logfile belongs to subcommand serve", + }, + + // ========================================================== + // Error Scenarios + // ========================================================== + { + name: "MissingGlobalFlagValue", + args: []string{"-otel"}, + wantErr: "flag needs an argument", + }, + { + name: "MissingServeFlagValue", + args: []string{"-listen"}, + wantErr: "flag needs an argument", + }, + { + name: "SubcommandFlagBeforeSubcommandFail", + args: []string{"-listen=:0", "references", "./gopls/main.go:35:8"}, + wantCmd: "references", + wantErr: "flag -listen belongs to subcommand serve", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + app := newApplication() // Ensure test isolation + t.Logf("> gopls %v", strings.Join(tc.args, " ")) + subApp, gotGlobalArgs, gotArgs, err := normalize(app, tc.args) + if tc.wantErr != "" { + if err == nil { + t.Fatalf("expected error containing %q, got nil", tc.wantErr) + } + if !strings.Contains(err.Error(), tc.wantErr) { + t.Errorf("err = %v, want error containing %q", err, tc.wantErr) + } + if tc.wantCmd != "" && subApp != nil && subApp.Name() != tc.wantCmd { + t.Errorf("normalize() cmd = %v, want %v on error", subApp.Name(), tc.wantCmd) + } + return + } + if err != nil { + t.Fatalf("dispatch failed: %v", err) + } + if subApp.Name() != tc.wantCmd { + t.Errorf("normalize() cmd = %v, want %v", subApp.Name(), tc.wantCmd) + } + if !slices.Equal(gotGlobalArgs, tc.wantGlobalArgs) { + t.Errorf("normalize() globalArgs = %v, want %v", gotGlobalArgs, tc.wantGlobalArgs) + } + if !slices.Equal(gotArgs, tc.wantArgs) { + t.Errorf("normalize() args = %v, want %v", gotArgs, tc.wantArgs) + } + }) + } +}
diff --git a/gopls/internal/cmd/prepare_rename.go b/gopls/internal/cmd/prepare_rename.go index 72f42af..715f1fd 100644 --- a/gopls/internal/cmd/prepare_rename.go +++ b/gopls/internal/cmd/prepare_rename.go
@@ -11,12 +11,11 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // prepareRename implements the prepare_rename verb for gopls. type prepareRename struct { - app *Application + app *application } func (r *prepareRename) Name() string { return "prepare_rename" } @@ -34,13 +33,13 @@ printFlagDefaults(f) } -// ErrInvalidRenamePosition is returned when prepareRename is run at a position that +// errInvalidRenamePosition is returned when prepareRename is run at a position that // is not a candidate for renaming. -var ErrInvalidRenamePosition = errors.New("request is not valid at the given position") +var errInvalidRenamePosition = errors.New("request is not valid at the given position") func (r *prepareRename) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("prepare_rename expects 1 argument (file)") + return commandLineErrorf("prepare_rename expects 1 argument (file)") } cli, _, err := r.app.connect(ctx) @@ -66,7 +65,7 @@ return fmt.Errorf("prepare_rename failed: %w", err) } if result == nil { - return ErrInvalidRenamePosition + return errInvalidRenamePosition } s, err := file.rangeSpan(result.Range)
diff --git a/gopls/internal/cmd/references.go b/gopls/internal/cmd/references.go index a91925f..def3f0b 100644 --- a/gopls/internal/cmd/references.go +++ b/gopls/internal/cmd/references.go
@@ -11,14 +11,13 @@ "sort" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // references implements the references verb for gopls type references struct { IncludeDeclaration bool `flag:"d,declaration" help:"include the declaration of the specified identifier in the results"` - app *Application + app *application } func (r *references) Name() string { return "references" } @@ -40,7 +39,7 @@ func (r *references) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("references expects 1 argument (position)") + return commandLineErrorf("references expects 1 argument (position)") } cli, _, err := r.app.connect(ctx)
diff --git a/gopls/internal/cmd/remote.go b/gopls/internal/cmd/remote.go index c100164..8cb6f24 100644 --- a/gopls/internal/cmd/remote.go +++ b/gopls/internal/cmd/remote.go
@@ -14,15 +14,15 @@ "os" "golang.org/x/tools/gopls/internal/lsprpc" - "golang.org/x/tools/gopls/internal/protocol/command" + protocolcommand "golang.org/x/tools/gopls/internal/protocol/command" ) type remote struct { - app *Application + app *application subcommands } -func newRemote(app *Application) *remote { +func newRemote(app *application) *remote { return &remote{ app: app, subcommands: subcommands{ @@ -44,11 +44,11 @@ // listSessions is an inspect subcommand to list current sessions. type listSessions struct { - app *Application + app *application } func (c *listSessions) Name() string { return "sessions" } -func (c *listSessions) Parent() string { return c.app.Name() } +func (c *listSessions) Parent() string { return "gopls remote" } func (c *listSessions) Usage() string { return "" } func (c *listSessions) ShortHelp() string { return "print information about current gopls sessions" @@ -91,11 +91,12 @@ } type startDebugging struct { - app *Application + app *application } -func (c *startDebugging) Name() string { return "debug" } -func (c *startDebugging) Usage() string { return "[host:port]" } +func (c *startDebugging) Name() string { return "debug" } +func (c *startDebugging) Parent() string { return "gopls remote" } +func (c *startDebugging) Usage() string { return "[host:port]" } func (c *startDebugging) ShortHelp() string { return "start the debug server" } @@ -132,11 +133,11 @@ if len(args) > 0 { debugAddr = args[0] } - debugArgs := command.DebuggingArgs{ + debugArgs := protocolcommand.DebuggingArgs{ Addr: debugAddr, } - var result command.DebuggingResult - if err := lsprpc.ExecuteCommand(ctx, remote, command.StartDebugging.String(), debugArgs, &result); err != nil { + var result protocolcommand.DebuggingResult + if err := lsprpc.ExecuteCommand(ctx, remote, protocolcommand.StartDebugging.String(), debugArgs, &result); err != nil { return err } if len(result.URLs) == 0 {
diff --git a/gopls/internal/cmd/rename.go b/gopls/internal/cmd/rename.go index 1922f40..6afdedd 100644 --- a/gopls/internal/cmd/rename.go +++ b/gopls/internal/cmd/rename.go
@@ -10,13 +10,12 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // rename implements the rename verb for gopls. type rename struct { EditFlags - app *Application + app *application } func (r *rename) Name() string { return "rename" } @@ -42,7 +41,7 @@ // - otherwise, prints the new versions to stdout. func (r *rename) Run(ctx context.Context, args ...string) error { if len(args) != 2 { - return tool.CommandLineErrorf("rename expects 2 arguments (position, new name)") + return commandLineErrorf("rename expects 2 arguments (position, new name)") } r.app.editFlags = &r.EditFlags cli, _, err := r.app.connect(ctx)
diff --git a/gopls/internal/cmd/semantictokens.go b/gopls/internal/cmd/semantictokens.go index 6dd0052..c971945 100644 --- a/gopls/internal/cmd/semantictokens.go +++ b/gopls/internal/cmd/semantictokens.go
@@ -43,7 +43,7 @@ // the gopls coordinate system type semanticToken struct { - app *Application + app *application } func (c *semanticToken) Name() string { return "semtok" } @@ -114,8 +114,8 @@ // prefixes for semantic token comments const ( - SemanticLeft = "/*⇐" - SemanticRight = "/*⇒" + semanticLeft = "/*⇐" + semanticRight = "/*⇒" ) func markLine(m mark, lines [][]byte) { @@ -126,7 +126,7 @@ if m.typ == "namespace" && m.offset-1+m.len < len(l) && l[m.offset-1+m.len] == '"' { // it is the last component of an import spec // cannot put a comment inside a string - insert = fmt.Sprintf("%s%d,namespace,[]*/", SemanticLeft, length) + insert = fmt.Sprintf("%s%d,namespace,[]*/", semanticLeft, length) splitAt = m.offset + m.len } else { // be careful not to generate //* @@ -134,7 +134,7 @@ if splitAt-1 >= 0 && l[splitAt-1] == '/' { spacer = " " } - insert = fmt.Sprintf("%s%s%d,%s,%v*/", spacer, SemanticRight, length, m.typ, m.mods) + insert = fmt.Sprintf("%s%s%d,%s,%v*/", spacer, semanticRight, length, m.typ, m.mods) } x := append([]byte(insert), l[splitAt:]...) l = append(l[:splitAt], x...)
diff --git a/gopls/internal/cmd/serve.go b/gopls/internal/cmd/serve.go index b87abda..2ffe269 100644 --- a/gopls/internal/cmd/serve.go +++ b/gopls/internal/cmd/serve.go
@@ -21,14 +21,12 @@ "golang.org/x/tools/gopls/internal/lsprpc" "golang.org/x/tools/gopls/internal/mcp" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" "golang.org/x/tools/gopls/internal/util/fakenet" "golang.org/x/tools/internal/jsonrpc2" ) -// Serve is a struct that exposes the configurable parts of the LSP and MCP -// server as flags, in the right form for tool.Main to consume. -type Serve struct { +// serve defines the flags and working state of the gopls serve command. +type serve struct { Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"` Mode string `flag:"mode" help:"no effect"` Address string `flag:"listen" help:"address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used."` @@ -36,23 +34,19 @@ Trace bool `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"` Debug string `flag:"debug" help:"serve debug information on the supplied address"` - RemoteListenTimeout time.Duration `flag:"remote.listen.timeout" help:"when used with -remote=auto, the -listen.timeout value used to start the daemon"` - RemoteDebug string `flag:"remote.debug" help:"when used with -remote=auto, the -debug value used to start the daemon"` - RemoteLogfile string `flag:"remote.logfile" help:"when used with -remote=auto, the -logfile value used to start the daemon"` - // MCP Server related configurations. MCPAddress string `flag:"mcp.listen" help:"experimental: address on which to listen for model context protocol connections. If port is localhost:0, pick a random port in localhost instead."` - app *Application + app *application } -func (s *Serve) Name() string { return "serve" } -func (s *Serve) Parent() string { return s.app.Name() } -func (s *Serve) Usage() string { return "[server-flags]" } -func (s *Serve) ShortHelp() string { +func (s *serve) Name() string { return "serve" } +func (s *serve) Parent() string { return s.app.Name() } +func (s *serve) Usage() string { return "[server-flags]" } +func (s *serve) ShortHelp() string { return "run a server for Go code using the Language Server Protocol" } -func (s *Serve) DetailedHelp(f *flag.FlagSet) { +func (s *serve) DetailedHelp(f *flag.FlagSet) { fmt.Fprint(f.Output(), ` gopls [flags] [server-flags] The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as @@ -63,27 +57,11 @@ printFlagDefaults(f) } -func (s *Serve) remoteArgs(network, address string) []string { - args := []string{"serve", - "-listen", fmt.Sprintf(`%s;%s`, network, address), - } - if s.RemoteDebug != "" { - args = append(args, "-debug", s.RemoteDebug) - } - if s.RemoteListenTimeout != 0 { - args = append(args, "-listen.timeout", s.RemoteListenTimeout.String()) - } - if s.RemoteLogfile != "" { - args = append(args, "-logfile", s.RemoteLogfile) - } - return args -} - // Run configures a server based on the flags, and then runs it. // It blocks until the server shuts down. -func (s *Serve) Run(ctx context.Context, args ...string) error { +func (s *serve) Run(ctx context.Context, args ...string) error { if len(args) > 0 { - return tool.CommandLineErrorf("server does not take arguments, got %v", args) + return commandLineErrorf("server does not take arguments, got %v", args) } di := debug.GetInstance(ctx) @@ -104,7 +82,7 @@ ) if s.app.Remote != "" { var err error - ss, err = lsprpc.NewForwarder(s.app.Remote, s.remoteArgs) + ss, err = lsprpc.NewForwarder(s.app.Remote, s.app.remoteArgs) if err != nil { return fmt.Errorf("creating forwarder: %w", err) }
diff --git a/gopls/internal/cmd/signature.go b/gopls/internal/cmd/signature.go index e1c5372..9f62afd 100644 --- a/gopls/internal/cmd/signature.go +++ b/gopls/internal/cmd/signature.go
@@ -10,12 +10,11 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // signature implements the signature verb for gopls type signature struct { - app *Application + app *application } func (r *signature) Name() string { return "signature" } @@ -35,7 +34,7 @@ func (r *signature) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("signature expects 1 argument (position)") + return commandLineErrorf("signature expects 1 argument (position)") } cli, _, err := r.app.connect(ctx) @@ -65,7 +64,7 @@ } if s == nil || len(s.Signatures) == 0 { - return tool.CommandLineErrorf("%v: not a function", from) + return commandLineErrorf("%v: not a function", from) } // there is only ever one possible signature,
diff --git a/gopls/internal/cmd/stats.go b/gopls/internal/cmd/stats.go index 2fad301..af8c77a 100644 --- a/gopls/internal/cmd/stats.go +++ b/gopls/internal/cmd/stats.go
@@ -20,7 +20,7 @@ "golang.org/x/tools/gopls/internal/filecache" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/protocol/command" + protocolcommand "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" versionpkg "golang.org/x/tools/gopls/internal/version" @@ -28,7 +28,7 @@ ) type stats struct { - app *Application + app *application Anon bool `flag:"anon" help:"hide any fields that may contain user names, file names, or source code"` } @@ -55,19 +55,21 @@ } func (s *stats) Run(ctx context.Context, args ...string) error { + // stats does not work with -remote yet. + // Other sessions on the daemon may interfere with results. + // Additionally, the type assertions in below only work if progress + // notifications bypass jsonrpc2 serialization. + // TODO(hyangah): support remote mode by fetching server-side + // statistics and avoiding hangs during InitialWorkspaceLoad. if s.app.Remote != "" { - // stats does not work with -remote. - // Other sessions on the daemon may interfere with results. - // Additionally, the type assertions in below only work if progress - // notifications bypass jsonrpc2 serialization. - return fmt.Errorf("the stats subcommand does not work with -remote") + return commandLineErrorf("stats does not currently support remote mode") } if !s.app.Verbose { event.SetExporter(nil) // don't log errors to stderr } - stats := GoplsStats{ + stats := statsJSON{ GOOS: runtime.GOOS, GOARCH: runtime.GOARCH, GOPLSCACHE: os.Getenv("GOPLSCACHE"), @@ -85,6 +87,7 @@ } // do executes a timed section of the stats command. + do := func(name string, f func() error) (time.Duration, error) { start := time.Now() fmt.Fprintf(os.Stderr, "%-30s", name+"...") @@ -128,12 +131,12 @@ if _, err := do("Querying memstats", func() error { memStats, err := executeCommand(ctx, cli.server, &protocol.Command{ - Command: command.MemStats.String(), + Command: protocolcommand.MemStats.String(), }) if err != nil { return err } - stats.MemStats = memStats.(command.MemStatsResult) + stats.MemStats = memStats.(protocolcommand.MemStatsResult) return nil }); err != nil { return err @@ -141,12 +144,12 @@ if _, err := do("Querying workspace stats", func() error { wsStats, err := executeCommand(ctx, cli.server, &protocol.Command{ - Command: command.WorkspaceStats.String(), + Command: protocolcommand.WorkspaceStats.String(), }) if err != nil { return err } - stats.WorkspaceStats = wsStats.(command.WorkspaceStatsResult) + stats.WorkspaceStats = wsStats.(protocolcommand.WorkspaceStatsResult) return nil }); err != nil { return err @@ -192,13 +195,13 @@ return nil } -// GoplsStats holds information extracted from a gopls session in the current +// statsJSON holds information extracted from a gopls session in the current // workspace. // // Fields that should be printed with the -anon flag should be explicitly // marked as `anon:"ok"`. Only fields that cannot refer to user files or code // should be marked as such. -type GoplsStats struct { +type statsJSON struct { GOOS, GOARCH string `anon:"ok"` GOPLSCACHE string GoVersion string `anon:"ok"` @@ -207,9 +210,9 @@ InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form CacheDir string BugReports []bug.Bug - MemStats command.MemStatsResult `anon:"ok"` - WorkspaceStats command.WorkspaceStatsResult `anon:"ok"` - DirStats dirStats `anon:"ok"` + MemStats protocolcommand.MemStatsResult `anon:"ok"` + WorkspaceStats protocolcommand.WorkspaceStatsResult `anon:"ok"` + DirStats dirStats `anon:"ok"` } type dirStats struct {
diff --git a/gopls/internal/cmd/subcommands.go b/gopls/internal/cmd/subcommands.go index 47d577b..cc934bf 100644 --- a/gopls/internal/cmd/subcommands.go +++ b/gopls/internal/cmd/subcommands.go
@@ -9,13 +9,11 @@ "flag" "fmt" "text/tabwriter" - - "golang.org/x/tools/gopls/internal/tool" ) // subcommands is a helper that may be embedded for commands that delegate to // subcommands. -type subcommands []tool.Application +type subcommands []command func (s subcommands) DetailedHelp(f *flag.FlagSet) { w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) @@ -31,26 +29,26 @@ func (s subcommands) Run(ctx context.Context, args ...string) error { if len(args) == 0 { - return tool.CommandLineErrorf("must provide subcommand") + return commandLineErrorf("must provide subcommand") } command, args := args[0], args[1:] for _, c := range s { if c.Name() == command { - s := flag.NewFlagSet(c.Name(), flag.ExitOnError) - return tool.Run(ctx, s, c, args) + fs := parseFlags(c, args) + return c.Run(ctx, fs.Args()...) } } - return tool.CommandLineErrorf("unknown subcommand %v", command) + return commandLineErrorf("unknown subcommand %v", command) } -func (s subcommands) Commands() []tool.Application { return s } +func (s subcommands) Commands() []command { return s } -// getSubcommands returns the subcommands of a given Application. -func getSubcommands(a tool.Application) []tool.Application { - // This interface is satisfied both by tool.Applications - // that embed subcommands, and by *cmd.Application. +// getSubcommands returns the subcommands of a given command. +func getSubcommands(a command) []command { + // This interface is satisfied both by commands + // that embed subcommands, and by *cmd.application. type hasCommands interface { - Commands() []tool.Application + Commands() []command } if sub, ok := a.(hasCommands); ok { return sub.Commands()
diff --git a/gopls/internal/cmd/symbols.go b/gopls/internal/cmd/symbols.go index 4641b6a..36fc208 100644 --- a/gopls/internal/cmd/symbols.go +++ b/gopls/internal/cmd/symbols.go
@@ -12,12 +12,11 @@ "sort" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/tool" ) // symbols implements the symbols verb for gopls type symbols struct { - app *Application + app *application } func (r *symbols) Name() string { return "symbols" } @@ -33,7 +32,7 @@ } func (r *symbols) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("symbols expects 1 argument (position)") + return commandLineErrorf("symbols expects 1 argument (position)") } cli, _, err := r.app.connect(ctx)
diff --git a/gopls/internal/cmd/usage/serve.hlp b/gopls/internal/cmd/usage/serve.hlp index 4f2dc9b..830c795 100644 --- a/gopls/internal/cmd/usage/serve.hlp +++ b/gopls/internal/cmd/usage/serve.hlp
@@ -20,11 +20,5 @@ experimental: address on which to listen for model context protocol connections. If port is localhost:0, pick a random port in localhost instead. -mode=string no effect - -remote.debug=string - when used with -remote=auto, the -debug value used to start the daemon - -remote.listen.timeout=duration - when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) - -remote.logfile=string - when used with -remote=auto, the -logfile value used to start the daemon -rpc.trace print the full rpc trace in lsp inspector format
diff --git a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp index 2cf77ba..39b3c83 100644 --- a/gopls/internal/cmd/usage/usage-v.hlp +++ b/gopls/internal/cmd/usage/usage-v.hlp
@@ -2,8 +2,10 @@ gopls is a Go language server. It is typically used with an editor to provide language features. When no -command is specified, gopls will default to the 'serve' command. The language -features can also be accessed via the gopls command-line interface. +command is specified, gopls will default to the 'serve' command. To see flags +for the 'serve' command, run 'gopls help serve'. + +The language features can also be accessed via the gopls command-line interface. For documentation of all its features, see: @@ -49,18 +51,6 @@ vulncheck run vulncheck analysis (internal-use only) flags: - -debug=string - serve debug information on the supplied address - -listen=string - address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used. - -listen.timeout=duration - when used with -listen, shut down the server when there are no connected clients for this duration - -logfile=string - filename to log to. if value is "auto", then logging to a default output file is enabled - -mcp.listen=string - experimental: address on which to listen for model context protocol connections. If port is localhost:0, pick a random port in localhost instead. - -mode=string - no effect -otel=string export telemetry to specified OpenTelemetry collector address (e.g. http://localhost:4318) -profile.alloc=string @@ -81,9 +71,9 @@ when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) -remote.logfile=string when used with -remote=auto, the -logfile value used to start the daemon - -rpc.trace - print the full rpc trace in lsp inspector format -v,-verbose verbose output -vv,-veryverbose very verbose output + +For flags specific to running the language server (such as -listen, -logfile, or -debug), run 'gopls help serve'.
diff --git a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp index c117ee8..991b5dc 100644 --- a/gopls/internal/cmd/usage/usage.hlp +++ b/gopls/internal/cmd/usage/usage.hlp
@@ -2,8 +2,10 @@ gopls is a Go language server. It is typically used with an editor to provide language features. When no -command is specified, gopls will default to the 'serve' command. The language -features can also be accessed via the gopls command-line interface. +command is specified, gopls will default to the 'serve' command. To see flags +for the 'serve' command, run 'gopls help serve'. + +The language features can also be accessed via the gopls command-line interface. For documentation of all its features, see: @@ -46,18 +48,6 @@ workspace_symbol search symbols in workspace flags: - -debug=string - serve debug information on the supplied address - -listen=string - address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used. - -listen.timeout=duration - when used with -listen, shut down the server when there are no connected clients for this duration - -logfile=string - filename to log to. if value is "auto", then logging to a default output file is enabled - -mcp.listen=string - experimental: address on which to listen for model context protocol connections. If port is localhost:0, pick a random port in localhost instead. - -mode=string - no effect -otel=string export telemetry to specified OpenTelemetry collector address (e.g. http://localhost:4318) -profile.alloc=string @@ -78,9 +68,9 @@ when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) -remote.logfile=string when used with -remote=auto, the -logfile value used to start the daemon - -rpc.trace - print the full rpc trace in lsp inspector format -v,-verbose verbose output -vv,-veryverbose very verbose output + +For flags specific to running the language server (such as -listen, -logfile, or -debug), run 'gopls help serve'.
diff --git a/gopls/internal/cmd/vulncheck.go b/gopls/internal/cmd/vulncheck.go index 7babf0d..97d1aa2 100644 --- a/gopls/internal/cmd/vulncheck.go +++ b/gopls/internal/cmd/vulncheck.go
@@ -16,7 +16,7 @@ // vulncheck implements the vulncheck command. // TODO(hakim): hide from the public. type vulncheck struct { - app *Application + app *application } func (v *vulncheck) Name() string { return "vulncheck" }
diff --git a/gopls/internal/cmd/workspace_symbol.go b/gopls/internal/cmd/workspace_symbol.go index cd4ebc6..07cac18 100644 --- a/gopls/internal/cmd/workspace_symbol.go +++ b/gopls/internal/cmd/workspace_symbol.go
@@ -12,14 +12,13 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/tool" ) // workspaceSymbol implements the workspace_symbol verb for gopls. type workspaceSymbol struct { Matcher string `flag:"matcher" help:"specifies the type of matcher: fuzzy, fastfuzzy, casesensitive, or caseinsensitive.\nThe default is caseinsensitive."` - app *Application + app *application } func (r *workspaceSymbol) Name() string { return "workspace_symbol" } @@ -39,7 +38,7 @@ func (r *workspaceSymbol) Run(ctx context.Context, args ...string) error { if len(args) != 1 { - return tool.CommandLineErrorf("workspace_symbol expects 1 argument") + return commandLineErrorf("workspace_symbol expects 1 argument") } opts := r.app.options
diff --git a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go index 354e22c..4d39ee1 100644 --- a/gopls/internal/debug/serve.go +++ b/gopls/internal/debug/serve.go
@@ -9,6 +9,7 @@ "context" "errors" "fmt" + "go/types" "html/template" "io" stdlog "log" @@ -803,6 +804,18 @@ } return s }, + "objectString": func(obj types.Object) string { + // Assume we're printing objects from the package of interest. + this := obj.Pkg() + qual := func(pkg *types.Package) string { + if this == pkg { + return "" // same package; unqualified + } + return pkg.Name() + } + return types.ObjectString(obj, qual) + }, + // TODO(rfindley): re-enable option inspection. // "options": func(s *cache.Session) []sessionOption { // return showOptions(s.Options()) @@ -1040,11 +1053,21 @@ {{end}} </ul> +<h2>Package-level symbols</h2> +<ul> + {{with $scope := .Package.Types.Scope}} + {{range $name := $scope.Names}} + {{with $obj := $scope.Lookup $name}} + <li>{{objectString .}}<br/> + {{end}} + {{end}} + {{end}} +</ul> + {{end}} {{/* TODO: - link to godoc (tricky: in server package) - - show Object inventory of types.Package.Scope - show index info (xrefs, methodsets, tests) - call DiagnoseFile on each file? */}}
diff --git a/gopls/internal/debug/template_test.go b/gopls/internal/debug/template_test.go index 43bc9b6..100335c 100644 --- a/gopls/internal/debug/template_test.go +++ b/gopls/internal/debug/template_test.go
@@ -22,7 +22,7 @@ "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/debug" - "golang.org/x/tools/gopls/internal/util/moremaps" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/testenv" )
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index c01dbca..e7f2896 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json
@@ -1623,6 +1623,12 @@ "Status": "" }, { + "Name": "\"ptrtoerror\"", + "Doc": "detect inconsistent conversions of concrete types to error\n\nThe ptrtoerror analyzer detects when a concrete type E is converted\nto the error interface inconsistently, both as a value of type E\nand as a pointer of type *E. Such inconsistency defeats attempts by\nclient code to test for specific error types using type assertions\nor library functions such as [errors.As] and [errors.Is].\n\nThe analyzer also detects when both E and *E implement error but\nneither of those types is converted to error within the defining\npackage, leaving the intended error form (E or *E) ambiguous. This\ndiagnostic offers two alternative fixes to add declarations that\nmake the intent explicit.", + "Default": "true", + "Status": "" + }, + { "Name": "\"rangeint\"", "Doc": "replace 3-clause for loops with for-range over integers\n\nThe rangeint analyzer suggests replacing traditional for loops such\nas\n\n\tfor i := 0; i \u003c n; i++ { ... }\n\nwith the more idiomatic Go 1.22 style:\n\n\tfor i := range n { ... }\n\nThis transformation is applied only if (a) the loop variable is not\nmodified within the loop body and (b) the loop's limit expression\nis not modified within the loop, as `for range` evaluates its\noperand only once.", "Default": "true", @@ -1635,6 +1641,12 @@ "Status": "" }, { + "Name": "\"reflecttypeassert\"", + "Doc": "replace v.Interface().(T) with reflect.TypeAssert[T](v)\n\nThis analyzer suggests fixes to replace two-valued type assertions on\nthe result of (reflect.Value).Interface with reflect.TypeAssert,\nintroduced in go1.25, which avoids the intermediate allocation of an\ninterface value, for example:\n\n\tx, ok := v.Interface().(string) -\u003e x, ok := reflect.TypeAssert[string](v)\n\nNo fix is offered for single-valued assertions, since they panic when\nthe assertion fails whereas reflect.TypeAssert does not. Nor is a fix\noffered for a type switch.", + "Default": "true", + "Status": "" + }, + { "Name": "\"reflecttypefor\"", "Doc": "replace reflect.TypeOf(x) with TypeFor[T]()\n\nThis analyzer suggests fixes to replace uses of reflect.TypeOf(x) with\nreflect.TypeFor, introduced in go1.22, when the desired runtime type\nis known at compile time, for example:\n\n\treflect.TypeOf(uint32(0)) -\u003e reflect.TypeFor[uint32]()\n\treflect.TypeOf((*ast.File)(nil)) -\u003e reflect.TypeFor[*ast.File]()\n\nIt also offers a fix to simplify the constructions below, which use\nreflect.TypeOf to return the runtime type for an interface type,\n\n\treflect.TypeOf((*io.Reader)(nil)).Elem()\n\nor:\n\n\treflect.TypeOf([]io.Reader(nil)).Elem()\n\nto:\n\n\treflect.TypeFor[io.Reader]()\n\nNo fix is offered in cases when the runtime type is dynamic, such as:\n\n\tvar r io.Reader = ...\n\treflect.TypeOf(r)\n\nor when the operand has potential side effects.", "Default": "true", @@ -1689,6 +1701,12 @@ "Status": "" }, { + "Name": "\"slicesclip\"", + "Doc": "replace three-index slice expressions with slices.Clip\n\nThe slicesclip analyzer suggests replacing a full slice expression of\nthe form\n\n\tx[:len(x):len(x)]\n\nwhich clips the capacity of a slice to its length, with the simpler\nand more readable\n\n\tslices.Clip(x)\n\nadded in Go 1.21.", + "Default": "true", + "Status": "" + }, + { "Name": "\"slicescontains\"", "Doc": "replace loops with slices.Contains or slices.ContainsFunc\n\nThe slicescontains analyzer simplifies loops that check for the existence of\nan element in a slice. It replaces them with calls to `slices.Contains` or\n`slices.ContainsFunc`, which were added in Go 1.21.\n\nIf the expression for the target element has side effects, this\ntransformation will cause those effects to occur only once, not\nonce per tested slice element.", "Default": "true", @@ -1726,7 +1744,7 @@ }, { "Name": "\"stditerators\"", - "Doc": "use iterators instead of Len/At-style APIs\n\nThis analyzer suggests a fix to replace each loop of the form:\n\n\tfor i := 0; i \u003c x.Len(); i++ {\n\t\tuse(x.At(i))\n\t}\n\nor its \"for elem := range x.Len()\" equivalent by a range loop over an\niterator offered by the same data type:\n\n\tfor elem := range x.All() {\n\t\tuse(x.At(i)\n\t}\n\nwhere x is one of various well-known types in the standard library.", + "Doc": "use iterators instead of Len/At-style APIs\n\nThis analyzer suggests a fix to replace each loop of the form:\n\n\tfor i := 0; i \u003c x.Len(); i++ {\n\t\tuse(x.At(i))\n\t}\n\nor its \"for elem := range x.Len()\" equivalent by a range loop over an\niterator offered by the same data type:\n\n\tfor elem := range x.All() {\n\t\tuse(elem)\n\t}\n\nwhere x is one of various well-known types in the standard library.", "Default": "true", "Status": "" }, @@ -2077,7 +2095,7 @@ }, { "Name": "\"ignoredError\"", - "Doc": "`\"ignoredError\"` inlay hints for implicitly discarded errors:\n```go\n\tf.Close()« // ignore error»\n```\nThis check inserts an `// ignore error` hint following any\nstatement that is a function call whose error result is\nimplicitly ignored.\n\nTo suppress the hint, write an actual comment containing\n\"ignore error\" following the call statement, or explicitly\nassign the result to a blank variable. A handful of common\nfunctions such as `fmt.Println` are excluded from the\ncheck.\n", + "Doc": "`\"ignoredError\"` inlay hints for implicitly discarded errors:\n```go\n\tf.Close()« // ignore error»\n```\nThis check inserts an `// ignore error` hint following any\nstatement that is a function call whose error result is\nimplicitly ignored.\n\nTo suppress the hint, write an actual comment containing\none of the following strings:\n```\nignore error\ndiscard error\ncan't fail\ncannot fail\n```\nfollowing the call statement, or explicitly assign the\nresult to a blank variable.\n\nA handful of common functions such as `fmt.Println` are\nexcluded from the check.\n", "Default": "false", "Status": "" }, @@ -2277,6 +2295,20 @@ "DeprecationMessage": "" }, { + "Name": "moveDeclaration", + "Type": "bool", + "Doc": "moveDeclaration enables producing Move Declaration codeactions. The implementation\nis unfinished so we use this setting to gate its use.\n", + "EnumKeys": { + "ValueType": "", + "Keys": null + }, + "EnumValues": null, + "Default": "false", + "Status": "experimental", + "Hierarchy": "ui", + "DeprecationMessage": "" + }, + { "Name": "local", "Type": "string", "Doc": "local is the equivalent of the `goimports -local` flag, which puts\nimports beginning with this string after third-party packages. It should\nbe the prefix of the import path whose imports should be grouped\nseparately.\n\nIt is used when tidying imports (during an LSP Organize\nImports request) or when inserting new ones (for example,\nduring completion); an LSP Formatting request merely sorts the\nexisting imports.\n", @@ -3378,13 +3410,13 @@ { "Name": "any", "Doc": "replace interface{} with any\n\nThe any analyzer suggests replacing uses of the empty interface type,\n`interface{}`, with the `any` alias, which was introduced in Go 1.18.\nThis is a purely stylistic change that makes code more readable.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#any", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_any", "Default": true }, { "Name": "appendclipped", "Doc": "simplify append chains using slices.Concat\n\nThe appendclipped analyzer suggests replacing chains of append calls with a\nsingle call to slices.Concat, which was added in Go 1.21. For example,\nappend(append(s, s1...), s2...) would be simplified to slices.Concat(s, s1, s2).\n\nIn the simple case of appending to a newly allocated slice, such as\nappend([]T(nil), s...), the analyzer suggests the more concise slices.Clone(s).\nFor byte slices, it will prefer bytes.Clone if the \"bytes\" package is\nalready imported.\n\nThis fix is only applied when the base of the append tower is a\n\"clipped\" slice, meaning its length and capacity are equal (e.g.\nx[:0:0] or []T{}). This is to avoid changing program behavior by\neliminating intended side effects on the base slice's underlying\narray.\n\nThis analyzer is currently disabled by default as the\ntransformation does not preserve the nilness of the base slice in\nall cases; see https://go.dev/issue/73557.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#appendclipped", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_appendclipped", "Default": false }, { @@ -3420,13 +3452,13 @@ { "Name": "atomictypes", "Doc": "replace basic types in sync/atomic calls with atomic types\n\nThe atomictypes analyzer suggests replacing the primitive sync/atomic functions with\nthe strongly typed atomic wrapper types introduced in Go1.19 (e.g.\natomic.Int32). For example,\n\n\tvar x int32\n\tatomic.AddInt32(\u0026x, 1)\n\nwould become\n\n\tvar x atomic.Int32\n\tx.Add(1)\n\nThe atomic types are safer because they don't allow non-atomic access, which is\na common source of bugs. These types also resolve memory alignment issues that\nplagued the old atomic functions on 32-bit architectures.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#atomictypes", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_atomictypes", "Default": true }, { "Name": "bloop", "Doc": "replace for-range over b.N with b.Loop\n\nThe bloop analyzer suggests replacing benchmark loops of the form\n`for i := 0; i \u003c b.N; i++` or `for range b.N` with the more modern\n`for b.Loop()`, which was added in Go 1.24.\n\nThis change makes benchmark code more readable and also removes the need for\nmanual timer control, so any preceding calls to b.StartTimer, b.StopTimer,\nor b.ResetTimer within the same function will also be removed.\n\nCaveats: The b.Loop() method is designed to prevent the compiler from\noptimizing away the benchmark loop, which can occasionally result in\nslower execution due to increased allocations in some specific cases.\nSince its fix may change the performance of nanosecond-scale benchmarks,\nbloop is disabled by default in the `go fix` analyzer suite; see golang/go#74967.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#bloop", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_bloop", "Default": true }, { @@ -3492,7 +3524,7 @@ { "Name": "embedlit", "Doc": "simplify references to embedded fields in composite literals\n\nThe embedlit analyzer suggests removing redundant embedded field type specifiers\nfrom composite literals. Go1.27 introduced the ability to directly initialize\nfields promoted from embedded struct types without a nested literal. For\nexample, given the following structs:\n\n\ttype T struct {\n\t\tU\n\t}\n\n\ttype U struct {\n\t\tx int\n\t}\n\nA composite literal such as\n\n\tt := T{U: U{x: 1}}\n\nwould become\n\n\tt := T{x: 1}", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#embedlit", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_embedlit", "Default": true }, { @@ -3504,7 +3536,7 @@ { "Name": "errorsastype", "Doc": "replace errors.As with errors.AsType[T]\n\nThis analyzer suggests fixes to simplify uses of [errors.As] of\nthis form:\n\n\tvar myerr *MyErr\n\tif errors.As(err, \u0026myerr) {\n\t\thandle(myerr)\n\t}\n\nby using the less error-prone generic [errors.AsType] function,\nintroduced in Go 1.26:\n\n\tif myerr, ok := errors.AsType[*MyErr](err); ok {\n\t\thandle(myerr)\n\t}\n\nThe fix is only offered if the var declaration has the form shown and\nthere are no uses of myerr outside the if statement.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_errorsastype", "Default": true }, { @@ -3528,13 +3560,13 @@ { "Name": "fmtappendf", "Doc": "replace []byte(fmt.Sprintf) with fmt.Appendf\n\nThe fmtappendf analyzer suggests replacing `[]byte(fmt.Sprintf(...))` with\n`fmt.Appendf(nil, ...)`. This avoids the intermediate allocation of a string\nby Sprintf, making the code more efficient. The suggestion also applies to\nfmt.Sprint and fmt.Sprintln.\n\nSince its fix is not a Pareto improvement, fmtappendf is disabled by default in\nthe `go fix` analyzer suite; see golang/go#77581.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#fmtappendf", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_fmtappendf", "Default": true }, { "Name": "forvar", "Doc": "remove redundant re-declaration of loop variables\n\nThe forvar analyzer removes unnecessary shadowing of loop variables.\nBefore Go 1.22, it was common to write `for _, x := range s { x := x ... }`\nto create a fresh variable for each iteration. Go 1.22 changed the semantics\nof `for` loops, making this pattern redundant. This analyzer removes the\nunnecessary `x := x` statement.\n\nThis fix only applies to `range` loops.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#forvar", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_forvar", "Default": true }, { @@ -3564,7 +3596,7 @@ { "Name": "importcomment", "Doc": "remove obsolete comments specifying canonical import path\n\nThe importcomment analyzer removes comments specifying the canonical\nimport path, such as\n\n\tpackage foo // import \"example.com/foo\"\n\nThe go command enforced these comments in GOPATH mode via \"go get\", but\nignores them in module mode, so they are obsolete once the package\nbelongs to a module. The fix removes the comment.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#importcomment", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_importcomment", "Default": true }, { @@ -3600,19 +3632,19 @@ { "Name": "mapsloop", "Doc": "replace explicit loops over maps with calls to maps package\n\nThe mapsloop analyzer replaces loops of the form\n\n\tfor k, v := range x { m[k] = v }\n\nwith a single call to a function from the `maps` package, added in Go 1.23.\nDepending on the context, this could be `maps.Copy`, `maps.Insert`,\n`maps.Clone`, or `maps.Collect`.\n\nThe transformation to `maps.Clone` is applied conservatively, as it\npreserves the nilness of the source map, which may be a subtle change in\nbehavior if the original code did not handle a nil map in the same way.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#mapsloop", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_mapsloop", "Default": true }, { "Name": "minmax", "Doc": "replace if/else statements with calls to min or max\n\nThe minmax analyzer simplifies conditional assignments by suggesting the use\nof the built-in `min` and `max` functions, introduced in Go 1.21. For example,\n\n\tif a \u003c b { x = a } else { x = b }\n\nis replaced by\n\n\tx = min(a, b).\n\nThis analyzer avoids making suggestions for floating-point types,\nas the behavior of `min` and `max` with NaN values can differ from\nthe original if/else statement.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#minmax", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_minmax", "Default": true }, { "Name": "newexpr", "Doc": "simplify code by using go1.26's new(expr)\n\nThis analyzer finds declarations of functions of this form:\n\n\tfunc varOf(x int) *int { return \u0026x }\n\nand suggests a fix to turn them into inlinable wrappers around\ngo1.26's built-in new(expr) function:\n\n\t//go:fix inline\n\tfunc varOf(x int) *int { return new(x) }\n\n(The directive comment causes the 'inline' analyzer to suggest\nthat calls to such functions are inlined.)\n\nIn addition, this analyzer suggests a fix for each call\nto one of the functions before it is transformed, so that\n\n\tuse(varOf(123))\n\nis replaced by:\n\n\tuse(new(123))\n\nWrapper functions such as varOf are common when working with Go\nserialization packages such as for JSON or protobuf, where pointers\nare often used to express optionality.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#newexpr", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_newexpr", "Default": true }, { @@ -3642,13 +3674,13 @@ { "Name": "omitzero", "Doc": "suggest replacing omitempty with omitzero for struct fields\n\nThe omitzero analyzer identifies uses of the `omitempty` JSON struct\ntag on fields that are themselves structs. For struct-typed fields,\nthe `omitempty` tag has no effect on the behavior of json.Marshal and\njson.Unmarshal. The analyzer offers two suggestions: either remove the\ntag, or replace it with `omitzero` (added in Go 1.24), which correctly\nomits the field if the struct value is zero.\n\nHowever, some other serialization packages (notably kubebuilder, see\nhttps://book.kubebuilder.io/reference/markers.html) may have their own\ninterpretation of the `json:\",omitzero\"` tag, so removing it may affect\nprogram behavior. For this reason, the omitzero modernizer will not\nmake changes in any package that contains +kubebuilder annotations.\n\nReplacing `omitempty` with `omitzero` is a change in behavior. The\noriginal code would always encode the struct field, whereas the\nmodified code will omit it if it is a zero-value.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_omitzero", "Default": true }, { "Name": "plusbuild", "Doc": "remove obsolete //+build comments\n\nThe plusbuild analyzer suggests a fix to remove obsolete build tags\nof the form:\n\n\t//+build linux,amd64\n\nin files that also contain a Go 1.18-style tag such as:\n\n\t//go:build linux \u0026\u0026 amd64\n\n(It does not check that the old and new tags are consistent;\nthat is the job of the 'buildtag' analyzer in the vet suite.)", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#plusbuild", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_plusbuild", "Default": true }, { @@ -3658,9 +3690,15 @@ "Default": true }, { + "Name": "ptrtoerror", + "Doc": "detect inconsistent conversions of concrete types to error\n\nThe ptrtoerror analyzer detects when a concrete type E is converted\nto the error interface inconsistently, both as a value of type E\nand as a pointer of type *E. Such inconsistency defeats attempts by\nclient code to test for specific error types using type assertions\nor library functions such as [errors.As] and [errors.Is].\n\nThe analyzer also detects when both E and *E implement error but\nneither of those types is converted to error within the defining\npackage, leaving the intended error form (E or *E) ambiguous. This\ndiagnostic offers two alternative fixes to add declarations that\nmake the intent explicit.", + "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/ptrtoerror", + "Default": true + }, + { "Name": "rangeint", "Doc": "replace 3-clause for loops with for-range over integers\n\nThe rangeint analyzer suggests replacing traditional for loops such\nas\n\n\tfor i := 0; i \u003c n; i++ { ... }\n\nwith the more idiomatic Go 1.22 style:\n\n\tfor i := range n { ... }\n\nThis transformation is applied only if (a) the loop variable is not\nmodified within the loop body and (b) the loop's limit expression\nis not modified within the loop, as `for range` evaluates its\noperand only once.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#rangeint", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_rangeint", "Default": true }, { @@ -3670,9 +3708,15 @@ "Default": true }, { + "Name": "reflecttypeassert", + "Doc": "replace v.Interface().(T) with reflect.TypeAssert[T](v)\n\nThis analyzer suggests fixes to replace two-valued type assertions on\nthe result of (reflect.Value).Interface with reflect.TypeAssert,\nintroduced in go1.25, which avoids the intermediate allocation of an\ninterface value, for example:\n\n\tx, ok := v.Interface().(string) -\u003e x, ok := reflect.TypeAssert[string](v)\n\nNo fix is offered for single-valued assertions, since they panic when\nthe assertion fails whereas reflect.TypeAssert does not. Nor is a fix\noffered for a type switch.", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_reflecttypeassert", + "Default": true + }, + { "Name": "reflecttypefor", "Doc": "replace reflect.TypeOf(x) with TypeFor[T]()\n\nThis analyzer suggests fixes to replace uses of reflect.TypeOf(x) with\nreflect.TypeFor, introduced in go1.22, when the desired runtime type\nis known at compile time, for example:\n\n\treflect.TypeOf(uint32(0)) -\u003e reflect.TypeFor[uint32]()\n\treflect.TypeOf((*ast.File)(nil)) -\u003e reflect.TypeFor[*ast.File]()\n\nIt also offers a fix to simplify the constructions below, which use\nreflect.TypeOf to return the runtime type for an interface type,\n\n\treflect.TypeOf((*io.Reader)(nil)).Elem()\n\nor:\n\n\treflect.TypeOf([]io.Reader(nil)).Elem()\n\nto:\n\n\treflect.TypeFor[io.Reader]()\n\nNo fix is offered in cases when the runtime type is dynamic, such as:\n\n\tvar r io.Reader = ...\n\treflect.TypeOf(r)\n\nor when the operand has potential side effects.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#reflecttypefor", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_reflecttypefor", "Default": true }, { @@ -3720,25 +3764,31 @@ { "Name": "slicesbackward", "Doc": "replace backward loops over slices with slices.Backward\n\nThe slicesbackward analyzer suggests replacing manually-written backward\nloops of the form\n\n\tfor i := len(s) - 1; i \u003e= 0; i-- {\n\t use(s[i])\n\t}\n\nwith the more readable Go 1.23 style using slices.Backward:\n\n\tfor _, v := range slices.Backward(s) {\n\t use(v)\n\t}\n\nIf the loop index is needed beyond just indexing into the slice, both\nthe index and value variables are kept:\n\n\tfor i, v := range slices.Backward(s) { ... }", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesbackward", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesbackward", + "Default": true + }, + { + "Name": "slicesclip", + "Doc": "replace three-index slice expressions with slices.Clip\n\nThe slicesclip analyzer suggests replacing a full slice expression of\nthe form\n\n\tx[:len(x):len(x)]\n\nwhich clips the capacity of a slice to its length, with the simpler\nand more readable\n\n\tslices.Clip(x)\n\nadded in Go 1.21.", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesclip", "Default": true }, { "Name": "slicescontains", "Doc": "replace loops with slices.Contains or slices.ContainsFunc\n\nThe slicescontains analyzer simplifies loops that check for the existence of\nan element in a slice. It replaces them with calls to `slices.Contains` or\n`slices.ContainsFunc`, which were added in Go 1.21.\n\nIf the expression for the target element has side effects, this\ntransformation will cause those effects to occur only once, not\nonce per tested slice element.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicescontains", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicescontains", "Default": true }, { "Name": "slicesdelete", "Doc": "replace append-based slice deletion with slices.Delete\n\nThe slicesdelete analyzer suggests replacing the idiom\n\n\ts = append(s[:i], s[j:]...)\n\nwith the more explicit\n\n\ts = slices.Delete(s, i, j)\n\nintroduced in Go 1.21.\n\nThis analyzer is disabled by default. The `slices.Delete` function\nzeros the elements between the new length and the old length of the\nslice to prevent memory leaks, which is a subtle difference in\nbehavior compared to the append-based idiom; see https://go.dev/issue/73686.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesdelete", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicesdelete", "Default": false }, { "Name": "slicessort", "Doc": "replace sort.Slice with slices.Sort for basic types\n\nThe slicessort analyzer simplifies sorting slices of basic ordered\ntypes. It replaces\n\n\tsort.Slice(s, func(i, j int) bool { return s[i] \u003c s[j] })\n\nwith the simpler `slices.Sort(s)`, which was added in Go 1.21.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicessort", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_slicessort", "Default": true }, { @@ -3761,8 +3811,8 @@ }, { "Name": "stditerators", - "Doc": "use iterators instead of Len/At-style APIs\n\nThis analyzer suggests a fix to replace each loop of the form:\n\n\tfor i := 0; i \u003c x.Len(); i++ {\n\t\tuse(x.At(i))\n\t}\n\nor its \"for elem := range x.Len()\" equivalent by a range loop over an\niterator offered by the same data type:\n\n\tfor elem := range x.All() {\n\t\tuse(x.At(i)\n\t}\n\nwhere x is one of various well-known types in the standard library.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stditerators", + "Doc": "use iterators instead of Len/At-style APIs\n\nThis analyzer suggests a fix to replace each loop of the form:\n\n\tfor i := 0; i \u003c x.Len(); i++ {\n\t\tuse(x.At(i))\n\t}\n\nor its \"for elem := range x.Len()\" equivalent by a range loop over an\niterator offered by the same data type:\n\n\tfor elem := range x.All() {\n\t\tuse(elem)\n\t}\n\nwhere x is one of various well-known types in the standard library.", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stditerators", "Default": true }, { @@ -3786,25 +3836,25 @@ { "Name": "stringsbuilder", "Doc": "replace += with strings.Builder\n\nThis analyzer replaces repeated string += string concatenation\noperations with calls to Go 1.10's strings.Builder.\n\nFor example:\n\n\tvar s = \"[\"\n\tfor x := range seq {\n\t\ts += x\n\t\ts += \".\"\n\t}\n\ts += \"]\"\n\tuse(s)\n\nis replaced by:\n\n\tvar s strings.Builder\n\ts.WriteString(\"[\")\n\tfor x := range seq {\n\t\ts.WriteString(x)\n\t\ts.WriteString(\".\")\n\t}\n\ts.WriteString(\"]\")\n\tuse(s.String())\n\nThis avoids quadratic memory allocation and improves performance.\n\nNo diagnostics are issued in tests, where data sizes are often\nsmall and asymptotic performance is not a security concern.\n\nThe analyzer requires that all references to s before the final uses\nare += operations. To avoid warning about trivial cases, at least one\nmust appear within a loop. The variable s must be a local\nvariable, not a global or parameter.\n\nAll uses of the finished string must come after the last += operation.\nEach such use will be replaced by a call to strings.Builder's String method.\n(These may appear within an intervening loop or function literal, since even\nif s.String() is called repeatedly, it does not allocate memory.)\n\nOften the addend is a call to fmt.Sprintf, as in this example:\n\n\tvar s string\n\tfor x := range seq {\n\t\ts += fmt.Sprintf(\"%v\", x)\n\t}\n\nwhich, once the suggested fix is applied, becomes:\n\n\tvar s strings.Builder\n\tfor x := range seq {\n\t\ts.WriteString(fmt.Sprintf(\"%v\", x))\n\t}\n\nThe WriteString call can be further simplified to the more efficient\nfmt.Fprintf(\u0026s, \"%v\", x), avoiding the allocation of an intermediary.\nHowever, stringsbuilder does not perform this simplification;\nit requires staticcheck analyzer QF1012. (See https://go.dev/issue/76918.)", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringbuilder", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringsbuilder", "Default": true }, { "Name": "stringscut", "Doc": "replace strings.Index etc. with strings.Cut\n\nThis analyzer replaces certain patterns of use of [strings.Index] and string slicing by [strings.Cut], added in go1.18.\n\nFor example:\n\n\tidx := strings.Index(s, substr)\n\tif idx \u003e= 0 {\n\t return s[:idx]\n\t}\n\nis replaced by:\n\n\tbefore, _, ok := strings.Cut(s, substr)\n\tif ok {\n\t return before\n\t}\n\nAnd:\n\n\tidx := strings.Index(s, substr)\n\tif idx \u003e= 0 {\n\t return\n\t}\n\nis replaced by:\n\n\tfound := strings.Contains(s, substr)\n\tif found {\n\t return\n\t}\n\nIt also handles variants using [strings.IndexByte] instead of Index, or the bytes package instead of strings.\n\nFixes are offered only in cases in which there are no potential modifications of the idx, s, or substr expressions between their definition and use.\n\nIt also replaces [strings.SplitN](s, sep, 2)[0] and [strings.Split](s, sep)[0] with the \"before\" result of strings.Cut, when sep is a non-empty string constant:\n\n\tx := strings.SplitN(s, sep, 2)[0]\n\nis replaced by:\n\n\tx, _, _ := strings.Cut(s, sep)\n\nThe fix is only offered when sep is a non-empty string literal. When sep is a variable or the empty string, the semantics differ (strings.Split(s, \"\")[0] returns the first character of s, but strings.Cut(s, \"\").before is \"\"), so no fix is suggested.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscut", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringscut", "Default": true }, { "Name": "stringscutprefix", "Doc": "replace HasPrefix/TrimPrefix with CutPrefix\n\nThe stringscutprefix analyzer simplifies a common pattern where code first\nchecks for a prefix with `strings.HasPrefix` and then removes it with\n`strings.TrimPrefix`. It replaces this two-step process with a single call\nto `strings.CutPrefix`, introduced in Go 1.20. The analyzer also handles\nthe equivalent functions in the `bytes` package.\n\nFor example, this input:\n\n\tif strings.HasPrefix(s, prefix) {\n\t use(strings.TrimPrefix(s, prefix))\n\t}\n\nis fixed to:\n\n\tif after, ok := strings.CutPrefix(s, prefix); ok {\n\t use(after)\n\t}\n\nThe analyzer also offers fixes to use CutSuffix in a similar way.\nThis input:\n\n\tif strings.HasSuffix(s, suffix) {\n\t use(strings.TrimSuffix(s, suffix))\n\t}\n\nis fixed to:\n\n\tif before, ok := strings.CutSuffix(s, suffix); ok {\n\t use(before)\n\t}", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringscutprefix", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringscutprefix", "Default": true }, { "Name": "stringsseq", "Doc": "replace ranging over Split/Fields with SplitSeq/FieldsSeq\n\nThe stringsseq analyzer improves the efficiency of iterating over substrings.\nIt replaces\n\n\tfor range strings.Split(...)\n\nwith the more efficient\n\n\tfor range strings.SplitSeq(...)\n\nwhich was added in Go 1.24 and avoids allocating a slice for the\nsubstrings. The analyzer also handles strings.Fields and the\nequivalent functions in the bytes package.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#stringsseq", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_stringsseq", "Default": true }, { @@ -3816,7 +3866,7 @@ { "Name": "testingcontext", "Doc": "replace context.WithCancel with t.Context in tests\n\nThe testingcontext analyzer simplifies context management in tests. It\nreplaces the manual creation of a cancellable context,\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\nwith a single call to t.Context(), which was added in Go 1.24.\n\nThis change is only suggested if the `cancel` function is not used\nfor any other purpose.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#testingcontext", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_testingcontext", "Default": true }, { @@ -3852,7 +3902,7 @@ { "Name": "unsafefuncs", "Doc": "replace unsafe pointer arithmetic with function calls\n\nThe unsafefuncs analyzer simplifies pointer arithmetic expressions by\nreplacing them with calls to helper functions such as unsafe.Add,\nadded in Go 1.17.\n\nExample:\n\n\tunsafe.Pointer(uintptr(ptr) + uintptr(n))\n\nwhere ptr is an unsafe.Pointer, is replaced by:\n\n\tunsafe.Add(ptr, n)", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#unsafefuncs", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_unsafefuncs", "Default": true }, { @@ -3900,7 +3950,7 @@ { "Name": "waitgroupgo", "Doc": "replace wg.Add(1)/go/wg.Done() with wg.Go\n\nThe waitgroupgo analyzer simplifies goroutine management with `sync.WaitGroup`.\nIt replaces the common pattern\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\t...\n\t}()\n\nwith a single call to\n\n\twg.Go(func(){ ... })\n\nwhich was added in Go 1.25.", - "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#waitgroupgo", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_waitgroupgo", "Default": true }, { @@ -3949,7 +3999,7 @@ }, { "Name": "ignoredError", - "Doc": "`\"ignoredError\"` inlay hints for implicitly discarded errors:\n```go\n\tf.Close()« // ignore error»\n```\nThis check inserts an `// ignore error` hint following any\nstatement that is a function call whose error result is\nimplicitly ignored.\n\nTo suppress the hint, write an actual comment containing\n\"ignore error\" following the call statement, or explicitly\nassign the result to a blank variable. A handful of common\nfunctions such as `fmt.Println` are excluded from the\ncheck.\n", + "Doc": "`\"ignoredError\"` inlay hints for implicitly discarded errors:\n```go\n\tf.Close()« // ignore error»\n```\nThis check inserts an `// ignore error` hint following any\nstatement that is a function call whose error result is\nimplicitly ignored.\n\nTo suppress the hint, write an actual comment containing\none of the following strings:\n```\nignore error\ndiscard error\ncan't fail\ncannot fail\n```\nfollowing the call statement, or explicitly assign the\nresult to a blank variable.\n\nA handful of common functions such as `fmt.Println` are\nexcluded from the check.\n", "Default": false, "Status": "" },
diff --git a/gopls/internal/filecache/filecache.go b/gopls/internal/filecache/filecache.go index 184e2fb..44db48e 100644 --- a/gopls/internal/filecache/filecache.go +++ b/gopls/internal/filecache/filecache.go
@@ -38,16 +38,35 @@ "sync/atomic" "time" + "golang.org/x/telemetry/counter" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/lru" ) -// Start causes the filecache to initialize and start garbage gollection. +// Start causes the filecache to initialize and start garbage collection. // // Start is automatically called by the first call to Get, but may be called // explicitly to pre-initialize the cache. func Start() { - go getCacheDir() // ignore error + go func() { + // Force early creation of the filecache and refuse to start + // if there were unexpected errors such as ENOSPC. This + // minimizes the window of exposure to deletion of the + // executable, and ensures that all subsequent calls to + // filecache.Get cannot fail for these two reasons; + // see issue #67433. + // + // This leaves only one likely cause for later failures: + // deletion of the cache while gopls is running. If the + // problem continues, we could periodically stat the cache + // directory (for example at the start of every RPC) and + // either re-create it or just fail the RPC with an + // informative error and terminate the process. + if _, err := Get("nonesuch", [32]byte{}, Bytes); err != nil && err != ErrNotFound { + counter.Inc("gopls/nocache") + log.Fatalf("gopls cannot access its persistent index (disk full?): %v", err) + } + }() } // memCache is a 100MB in-memory LRU cache in front of filecache
diff --git a/gopls/internal/filewatcher/filewatcher_test.go b/gopls/internal/filewatcher/filewatcher_test.go index 2c8f561..f4526aa 100644 --- a/gopls/internal/filewatcher/filewatcher_test.go +++ b/gopls/internal/filewatcher/filewatcher_test.go
@@ -19,7 +19,7 @@ "golang.org/x/tools/gopls/internal/filewatcher" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/moremaps" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/robustio" "golang.org/x/tools/internal/testfiles" "golang.org/x/tools/txtar"
diff --git a/gopls/internal/filewatcher/poll_watcher.go b/gopls/internal/filewatcher/poll_watcher.go index a48b8c3..de3f962 100644 --- a/gopls/internal/filewatcher/poll_watcher.go +++ b/gopls/internal/filewatcher/poll_watcher.go
@@ -19,8 +19,8 @@ "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/frob" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/moremaps" ) /*
diff --git a/gopls/internal/goasm/definition.go b/gopls/internal/goasm/definition.go index b4605a3..632986d 100644 --- a/gopls/internal/goasm/definition.go +++ b/gopls/internal/goasm/definition.go
@@ -6,15 +6,11 @@ import ( "context" - "fmt" "go/token" "golang.org/x/tools/gopls/internal/cache" - "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/asm" - "golang.org/x/tools/gopls/internal/util/morestrings" "golang.org/x/tools/internal/event" ) @@ -23,103 +19,32 @@ ctx, done := event.Start(ctx, "goasm.Definition") defer done() - mp, err := snapshot.NarrowestMetadataForFile(ctx, fh.URI()) + res, err := resolve(ctx, snapshot, fh, rng) if err != nil { return nil, err } - // Read the file. - content, err := fh.Content() - if err != nil { - return nil, err - } - mapper := protocol.NewMapper(fh.URI(), content) - start, end, err := mapper.RangeOffsets(rng) - if err != nil { - return nil, err - } - - // Parse the assembly. - // - // TODO(adonovan): make this just another - // attribute of the type-checked cache.Package. - file := asm.Parse(fh.URI(), content) - - // Figure out the selected symbol. - // Use the selection range so that haphazard selections that - // happen to start in an identifier don't produce spurious matches. - var found *asm.Ident - for _, id := range file.Idents { - if id.Offset <= start && end <= id.End() { - found = &id - break - } - } - if found == nil { - return nil, fmt.Errorf("not an identifier") - } - - // Resolve a symbol with a "." prefix to the current package. - sym := found.Name - if sym != "" && sym[0] == '.' { - sym = string(mp.PkgPath) + sym - } - - // package-qualified symbol? - if pkgpath, name, ok := morestrings.CutLast(sym, "."); ok { - // Find declaring package among dependencies. - // - // TODO(adonovan): assembly may legally reference - // non-dependencies. For example, sync/atomic calls - // internal/runtime/atomic. Perhaps we should search - // the entire metadata graph, but that's path-dependent. - var declaring *metadata.Package - for pkg := range snapshot.MetadataGraph().ForwardReflexiveTransitiveClosure(mp.ID) { - if pkg.PkgPath == metadata.PackagePath(pkgpath) { - declaring = pkg - break - } - } - if declaring == nil { - return nil, fmt.Errorf("package %q is not a dependency", pkgpath) - } - - // Find declared symbol in syntax package. - pkgs, err := snapshot.TypeCheck(ctx, declaring.ID) + // Package-qualified symbol: jump to its Go declaration. + if res.obj != nil { + pos := res.obj.Pos() + pgf, err := res.pkg.FileEnclosing(pos) if err != nil { return nil, err } - pkg := pkgs[0] - def := pkg.Types().Scope().Lookup(name) - if def == nil { - return nil, fmt.Errorf("no symbol %q in package %q", name, pkgpath) - } - - // Map position. - pos := def.Pos() - pgf, err := pkg.FileEnclosing(pos) - if err != nil { - return nil, err - } - loc, err := pgf.PosLocation(pos, pos+token.Pos(len(name))) + loc, err := pgf.PosLocation(pos, pos+token.Pos(len(res.obj.Name()))) if err != nil { return nil, err } return []protocol.Location{loc}, nil + } - } else { - // local symbols (funcs, vars, labels) - for _, id := range file.Idents { - if id.Name == found.Name && - (id.Kind == asm.Text || id.Kind == asm.Global || id.Kind == asm.Label) { - - loc, err := mapper.OffsetLocation(id.Offset, id.End()) - if err != nil { - return nil, err - } - return []protocol.Location{loc}, nil - } + // Local symbol: jump to its definition in the assembly file. + if res.localDef != nil { + loc, err := res.file.Mapper.OffsetLocation(res.localDef.Offset, res.localDef.End()) + if err != nil { + return nil, err } + return []protocol.Location{loc}, nil } return nil, nil
diff --git a/gopls/internal/goasm/highlight.go b/gopls/internal/goasm/highlight.go new file mode 100644 index 0000000..ad0b7c5 --- /dev/null +++ b/gopls/internal/goasm/highlight.go
@@ -0,0 +1,483 @@ +// Copyright 2025 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 goasm + +import ( + "bytes" + "context" + "strings" + + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/asm" + "golang.org/x/tools/internal/event" +) + +// Highlight handles the textDocument/documentHighlight request for Go +// assembly files. +// +// If the cursor is on a symbol identifier, all occurrences of the same +// name in the file are highlighted: definitions (TEXT, GLOBL) as Write, +// references as Read. Control labels are function-scoped, so for a label +// only occurrences within the enclosing TEXT function are highlighted. +// +// If the cursor is on a machine register, all occurrences of that +// register within the enclosing TEXT function are highlighted, +// approximating its def/use chain: occurrences classified as +// definitions are Write, uses are Read. Register highlighting requires +// a GOARCH file name suffix (e.g. *_amd64.s, *_arm64.s). +func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.DocumentHighlight, error) { + ctx, done := event.Start(ctx, "goasm.Highlight") + defer done() + + content, err := fh.Content() + if err != nil { + return nil, err + } + + asmFile := asm.Parse(fh.URI(), content) + + start, end, err := asmFile.Mapper.RangeOffsets(rng) + if err != nil { + return nil, err + } + + // Identifier (symbol or label) under the cursor? + if found := asmFile.IdentAt(start, end); found != nil { + return highlightIdents(asmFile, found) + } + + // Register under the cursor? + return highlightRegister(asmFile, start) +} + +// highlightIdents highlights every identifier with the same name as +// found. Definitions are Write; references are Read. If the name denotes +// a control label (it has a label definition in the file), only +// occurrences within the enclosing TEXT function are highlighted, since +// labels are function-scoped and the same label name may be reused in +// different functions. +func highlightIdents(file *asm.File, found *asm.Ident) ([]protocol.DocumentHighlight, error) { + // Heuristic: if the name is used as a label anywhere in the file, + // assume every occurrence is a label. A label and a global symbol + // sharing a name is implausible in practice, so per-occurrence + // disambiguation is not worth the cost. + lo, hi := 0, len(file.Mapper.Content) + for _, id := range file.Idents { + if id.Kind == asm.Label && id.Name == found.Name { + lo, hi = file.FunctionRange(found.Offset) + break + } + } + + var highlights []protocol.DocumentHighlight + for _, id := range file.Idents { + if id.Name != found.Name || !(lo <= id.Offset && id.Offset < hi) { + continue + } + idRange, err := file.IdentRange(id) + if err != nil { + return nil, err + } + kind := protocol.Read + if id.Kind == asm.Text || id.Kind == asm.Global || id.Kind == asm.Label { + kind = protocol.Write + } + highlights = append(highlights, protocol.DocumentHighlight{ + Range: idRange, + Kind: kind, + }) + } + return highlights, nil +} + +// highlightRegister highlights all occurrences of the register under the +// cursor within the enclosing TEXT function. +func highlightRegister(file *asm.File, offset int) ([]protocol.DocumentHighlight, error) { + content := file.Mapper.Content + arch := fileArch(file.Mapper.URI.Base()) + if arch == "" { + return nil, nil + } + word, wordStart := wordAt(content, offset) + if word == "" || !isRegisterWord(word) || inComment(content, offset) { + return nil, nil + } + // The first word on a line is the mnemonic, not a register. + if isLineStart(content, wordStart) { + return nil, nil + } + + funcStart, funcEnd := file.FunctionRange(offset) + var highlights []protocol.DocumentHighlight + pos := funcStart + for pos < funcEnd { + i := bytes.Index(content[pos:funcEnd], []byte(word)) + if i < 0 { + break + } + absOff := pos + i + pos = absOff + len(word) + // Skip occurrences inside comments, within a larger word + // (e.g. "AX" in "MAX"), or at the start of a line (mnemonic). + if inComment(content, absOff) || + !isWordBoundary(content, absOff, absOff+len(word)) || + isLineStart(content, absOff) { + continue + } + rng, err := file.Mapper.OffsetRange(absOff, absOff+len(word)) + if err != nil { + return nil, err + } + highlights = append(highlights, protocol.DocumentHighlight{ + Range: rng, + Kind: registerKind(content, absOff, arch), + }) + } + return highlights, nil +} + +// fileArch returns the GOARCH suffix of an assembly file's base name. It +// returns "" for unsupported architectures and for file names without a +// GOARCH suffix (which select their architecture using build constraints); +// register highlighting is not yet supported for those files. +func fileArch(base string) string { + name, ok := strings.CutSuffix(base, ".s") + if !ok { + return "" + } + if i := strings.LastIndexByte(name, '_'); i >= 0 { + arch := name[i+1:] + switch arch { + case "386", "amd64", "arm64": + return arch + } + } + return "" +} + +// registerKind classifies the register occurrence at offset (a byte +// offset within content) as Read or Write, where arch is the file's +// GOARCH ("386", "amd64", or "arm64"). It follows the Plan 9 +// assembly convention that the destination operand is the last +// operand: a register in the last operand is a definition (Write), and a +// register in any earlier operand is a use (Read), with three exceptions: +// +// - A register inside parentheses is part of a memory address operand +// (as in (AX) or 8(AX)(BX*4)) and is always Read, even in the +// destination operand of a store. A parenthesized register pair, +// such as the (R4, R5) of arm64 LDP/STP, is not a memory address +// but a single operand in its own right, and is classified by its +// position like any other operand; see enclosingGroup. +// - Comparison and test instructions (x86: CMP, TEST, COMIS*, UCOMIS*, +// BT*; arm64: CMP, CMN, TST) have no destination, so all their +// operands are Read. +// - Single-operand instructions write their operand only in a few +// cases, all x86-specific: POP stores into it, INC/DEC/NEG/NOT/BSWAP +// update it in place, and SETcc sets it to 0 or 1. For all others +// (PUSH, MUL/DIV, ...) the operand is a source and is Read. +// +// Implicit register operands are not modeled — for example MUL/DIV +// clobber DX:AX, CALL may clobber CX, and the post-increment forms such +// as arm64 LDP.P update their base register — so occurrences in such +// instructions may be misclassified. +// +// TODO(golang/go#71754): model implicit operands. +// +// TODO(golang/go#71754): consider linking instruction mnemonics to +// their CPU documentation (e.g. https://www.felixcloutier.com/x86/movzx). +func registerKind(content []byte, offset int, arch string) protocol.DocumentHighlightKind { + // Find the line containing offset. + lineStart := offset + for lineStart > 0 && content[lineStart-1] != '\n' { + lineStart-- + } + lineEnd := offset + for lineEnd < len(content) && content[lineEnd] != '\n' { + lineEnd++ + } + line := content[lineStart:lineEnd] + // Strip a trailing comment so its commas and parentheses are not + // mistaken for operand syntax. + if i := bytes.Index(line, []byte("//")); i >= 0 { + line = line[:i] + } + + // A register inside parentheses is a memory address: always Read, + // unless the parentheses enclose a register pair, in which case the + // pair is classified by its position as a whole. + rel := offset - lineStart + if open, isPair := enclosingGroup(line, rel); open >= 0 { + if !isPair { + return protocol.Read + } + rel = open // classify the pair by the position of its '(' + } + + // Identify the mnemonic: the first non-space token on the line. + i := 0 + for i < len(line) && (line[i] == ' ' || line[i] == '\t') { + i++ + } + mnStart := i + for i < len(line) && line[i] != ' ' && line[i] != '\t' && line[i] != ',' { + i++ + } + mnemonic := string(line[mnStart:i]) + + if isCompareMnemonic(arch, mnemonic) { + return protocol.Read + } + + // The operand list starts after the mnemonic. Count top-level commas to + // determine which operand the occurrence is in; the last operand is the + // destination. + operandArea := line[i:] + relMatch := min(max(rel-i, 0), len(operandArea)) + commaBefore := topLevelCommas(operandArea[:relMatch]) + totalCommas := topLevelCommas(operandArea) + if totalCommas == 0 { + // Single-operand instruction. The write cases below are + // all x86-specific; on other architectures the operand is + // always a source. + if arch != "386" && arch != "amd64" { + return protocol.Read + } + m := strings.ToUpper(mnemonic) + if strings.HasPrefix(m, "SET") { // SETcc; also avoids trimSizeSuffix("SETEQ") = "SETE" + return protocol.Write + } + switch trimSizeSuffix(m) { + case "POP", "INC", "DEC", "NEG", "NOT", "BSWAP": + return protocol.Write + } + return protocol.Read + } + if commaBefore >= totalCommas { + return protocol.Write + } + return protocol.Read +} + +// enclosingGroup reports whether index rel of line is inside a +// parenthesized group, returning the index of the group's '(', or -1 if +// there is none, and whether the group is a register pair such as the +// (R4, R5) of arm64 LDP/STP. +// +// A group is taken to be a register pair if it contains a comma at its +// own nesting level and its '(' does not immediately follow an +// identifier. Memory address operands contain no comma at that level (as +// in (AX) or 8(AX)(BX*4)), and in a macro invocation the '(' is glued to +// the macro name (as in QR(V0, V4, V8, V12)). +func enclosingGroup(line []byte, rel int) (int, bool) { + // Find the innermost unclosed '(' before rel. + open := -1 + for depth, j := 0, rel-1; j >= 0 && open < 0; j-- { + switch line[j] { + case ')': + depth++ + case '(': + if depth == 0 { + open = j + } else { + depth-- + } + } + } + if open < 0 { + return -1, false + } + if open > 0 && isWordByte(line[open-1]) { + return open, false // macro invocation + } + depth := 0 + for j := open; j < len(line); j++ { + switch line[j] { + case '(': + depth++ + case ')': + if depth--; depth == 0 { + return open, false // closed with no comma of its own + } + case ',': + if depth == 1 { + return open, true + } + } + } + return open, false // unterminated +} + +// topLevelCommas counts the commas of s that are not nested within +// parentheses, that is, the operand separators of an instruction. +func topLevelCommas(s []byte) int { + n, depth := 0, 0 + for _, b := range s { + switch b { + case '(': + depth++ + case ')': + if depth > 0 { + depth-- + } + case ',': + if depth == 0 { + n++ + } + } + } + return n +} + +// isCompareMnemonic reports whether mnemonic is a comparison or test +// instruction on arch, whose operands are all reads (no destination). +// On arm64 these are CMP/CMN/TST (the prefixes cover the W width +// variants). On x86, BT (bit test) only reads its destination operand +// to set flags, so it is a comparison; BTS/BTR/BTC are read-modify- +// write and are not — their destination is classified as Write by the +// default rule. CMPXCHG/CMPXCHG8B/CMPXCHG16B are read-modify-write and +// are excluded from CMP prefix matching for the same reason. +// +// The x86 predicate comparisons CMPPD/CMPPS/CMPSD/CMPSS do have a +// destination, and it is their second operand rather than their last +// (as in CMPPD X1, X2, $7), so both this function and the default rule +// misclassify it as Read. Their mnemonics are also ambiguous: CMPSD and +// CMPSS name both these instructions and the operand-free string +// comparisons. +func isCompareMnemonic(arch, mnemonic string) bool { + m := strings.ToUpper(mnemonic) + switch arch { + case "arm64": + return strings.HasPrefix(m, "CMP") || + strings.HasPrefix(m, "CMN") || + strings.HasPrefix(m, "TST") + case "386", "amd64": + // Continue below with the x86 cases. + default: + return false + } + if trimSizeSuffix(m) == "BT" { + return true + } + // CMPXCHG has CMP prefix but is read-modify-write, not a compare. + if strings.HasPrefix(m, "CMPXCHG") { + return false + } + switch { + case strings.HasPrefix(m, "CMP"), + strings.HasPrefix(m, "TEST"), + strings.HasPrefix(m, "COM"), + strings.HasPrefix(m, "UCOM"): + return true + } + return false +} + +// trimSizeSuffix strips a single trailing size suffix (B/W/L/Q) from an +// instruction mnemonic, e.g. "CMPQ" -> "CMP", "BTB" -> "BT", "BTS" -> "BTS". +func trimSizeSuffix(m string) string { + if len(m) > 0 { + switch m[len(m)-1] { + case 'B', 'W', 'L', 'Q': + return m[:len(m)-1] + } + } + return m +} + +// inComment reports whether offset falls within a // line comment. +// Like [asm.Parse], it does not recognize /* */ block comments. +func inComment(content []byte, offset int) bool { + lineStart := offset + for lineStart > 0 && content[lineStart-1] != '\n' { + lineStart-- + } + return bytes.Contains(content[lineStart:offset], []byte("//")) +} + +// isWordBoundary reports whether content[start:end], which contains +// only word bytes and is nonempty, is a whole word: the bytes +// immediately before start and after end are not word bytes. +func isWordBoundary(content []byte, start, end int) bool { + if start > 0 && isWordByte(content[start-1]) { + return false + } + if end < len(content) && isWordByte(content[end]) { + return false + } + return true +} + +// wordAt returns the maximal run of ASCII word bytes ([A-Za-z0-9]) +// containing pos, together with its start offset. If the run is empty +// (pos is on a non-word byte whose left neighbor is also a non-word +// byte), wordAt returns ("", pos). +// +// Precondition: 0 <= pos <= len(content). +func wordAt(content []byte, pos int) (string, int) { + start := pos + for start > 0 && isWordByte(content[start-1]) { + start-- + } + end := pos + for end < len(content) && isWordByte(content[end]) { + end++ + } + return string(content[start:end]), start +} + +func isWordByte(b byte) bool { + return (b >= 'A' && b <= 'Z') || + (b >= 'a' && b <= 'z') || + (b >= '0' && b <= '9') || + b == '_' +} + +// isRegisterWord reports whether word looks like a machine register name: +// 2-3 ASCII uppercase letters/digits with at least one letter. (Requiring +// a letter excludes numeric immediates such as "123".) The pseudo- +// registers SB, SP, FP, and PC are excluded because they appear in almost +// every operand, so highlighting them would be noise rather than signal. +func isRegisterWord(word string) bool { + if len(word) < 2 || len(word) > 3 { + return false + } + switch word { + case "SB", "SP", "FP", "PC": + return false + } + hasLetter := false + for i := 0; i < len(word); i++ { + c := word[i] + switch { + case 'A' <= c && c <= 'Z': + hasLetter = true + case !('0' <= c && c <= '9'): + return false + } + } + return hasLetter +} + +// isLineStart reports whether offset begins a line, i.e. it is preceded +// only by whitespace or the start of the file. Callers use it to reject +// instruction mnemonics, which assumes each line holds at most one +// instruction and that it is not preceded by a label; neither holds for +// "label: RET" or for instructions separated by ';', though no such line +// appears in GOROOT for the architectures supported by fileArch. +func isLineStart(content []byte, offset int) bool { + for i := offset - 1; i >= 0; i-- { + switch content[i] { + case '\n': + return true + case ' ', '\t': + continue + default: + return false + } + } + return true // beginning of file +}
diff --git a/gopls/internal/goasm/hover.go b/gopls/internal/goasm/hover.go new file mode 100644 index 0000000..2669581 --- /dev/null +++ b/gopls/internal/goasm/hover.go
@@ -0,0 +1,132 @@ +// Copyright 2025 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 goasm + +import ( + "context" + "fmt" + "go/ast" + "go/doc/comment" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/typesinternal" +) + +// Hover handles the textDocument/hover request for Go assembly files. +func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) (*protocol.Hover, error) { + ctx, done := event.Start(ctx, "goasm.Hover") + defer done() + + res, err := resolve(ctx, snapshot, fh, rng) + if err != nil { + return nil, err + } + if res.obj == nil { + // The cursor is not on an identifier, or the symbol has no Go + // declaration (a label or asm-only TEXT/GLOBL): there is no + // non-obvious information to report. + return nil, nil + } + + identRange, err := res.file.IdentRange(*res.found) + if err != nil { + return nil, err + } + + format := snapshot.Options().PreferredContentFormat + + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: format, + Value: hoverObject(res.obj, res.pkg, format), + }, + Range: identRange, + }, nil +} + +// hoverObject formats hover text for a resolved Go object: its signature +// (in a fenced Go code block when markdown is preferred), followed by its +// doc comment. +func hoverObject(obj types.Object, pkg *cache.Package, format protocol.MarkupKind) string { + // Qualify other packages by name, not path: the full package path is + // almost always excessively verbose in hover text. + qual := typesinternal.NameRelativeTo(pkg.Types()) + signature := types.ObjectString(obj, qual) + + var b strings.Builder + if format == protocol.Markdown { + fmt.Fprintf(&b, "```go\n%s\n```", signature) + } else { + b.WriteString(signature) + } + + if doc := docCommentForObj(pkg, obj); doc != "" { + if format == protocol.Markdown { + b.WriteString("\n\n") + doctree := new(comment.Parser).Parse(doc) + printer := &comment.Printer{HeadingLevel: 3} + // Suppress the default {#Hdr-...} heading anchors, which + // clients display (as in golang.DocCommentToMarkdown). + printer.HeadingID = func(*comment.Heading) string { return "" } + b.Write(printer.Markdown(doctree)) + } else { + b.WriteByte('\n') + b.WriteString(doc) + } + } + return b.String() +} + +// docCommentForObj returns the text of the doc comment associated with the +// declaration of obj in pkg, or "" if there is none. +// +// Assembly symbols resolve to package-level Go declarations (functions, +// variables, constants, and types), so only those are handled here. +func docCommentForObj(pkg *cache.Package, obj types.Object) string { + pos := obj.Pos() + if pos == token.NoPos { + return "" + } + pgf, err := pkg.FileEnclosing(pos) + if err != nil { + return "" + } + for _, decl := range pgf.File.Decls { + switch d := decl.(type) { + case *ast.FuncDecl: + if d.Name != nil && d.Name.Pos() == pos { + return d.Doc.Text() + } + case *ast.GenDecl: + for _, spec := range d.Specs { + switch s := spec.(type) { + case *ast.TypeSpec: + if s.Name.Pos() == pos { + if t := s.Doc.Text(); t != "" { + return t + } + return d.Doc.Text() + } + case *ast.ValueSpec: + for _, name := range s.Names { + if name.Pos() == pos { + if t := s.Doc.Text(); t != "" { + return t + } + return d.Doc.Text() + } + } + } + } + } + } + return "" +}
diff --git a/gopls/internal/goasm/resolve.go b/gopls/internal/goasm/resolve.go new file mode 100644 index 0000000..fe9fb8e --- /dev/null +++ b/gopls/internal/goasm/resolve.go
@@ -0,0 +1,143 @@ +// Copyright 2025 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 goasm + +import ( + "context" + "go/types" + + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/metadata" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/asm" + "golang.org/x/tools/gopls/internal/util/morestrings" +) + +// A resolution is the result of resolving an assembly identifier to its +// definition, shared by Definition and Hover. +type resolution struct { + // file is the parsed assembly file. + file *asm.File + + // found is the identifier under the cursor, or nil if the cursor is + // not on an identifier. + found *asm.Ident + + // obj is the Go object for a package-qualified symbol (including a + // current-package symbol such as ·foo), or nil if the symbol has no + // Go declaration. + obj types.Object + // pkg is the type-checked package that declares obj, or nil. + pkg *cache.Package + + // localDef is the defining identifier in the assembly file for a local + // symbol — a label, a bare TEXT/GLOBL symbol, or a current-package + // symbol without a Go declaration. It is nil if none was found. For a + // label, only the enclosing TEXT function is searched. + localDef *asm.Ident +} + +// resolve resolves the assembly identifier at rng to its definition. +// +// For a package-qualified symbol (including a current-package symbol such +// as ·foo, which is rewritten to pkgpath.foo), resolve type-checks the +// declaring package and returns the Go object in res.obj. For a local +// symbol, resolve returns the defining identifier in the assembly file in +// res.localDef. +// +// res.found is nil if the cursor is not on an identifier. +func resolve(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) (res resolution, err error) { + // Package metadata is needed only to resolve package-qualified symbols + // to Go declarations. An assembly-only file with no Go package has + // none; tolerate the error and fall back to local assembly definitions. + mp, err := snapshot.NarrowestMetadataForFile(ctx, fh.URI()) + if err != nil { + mp = nil + } + + content, err := fh.Content() + if err != nil { + return res, err + } + // TODO(adonovan): make this just another + // attribute of the type-checked cache.Package. + res.file = asm.Parse(fh.URI(), content) + + start, end, err := res.file.Mapper.RangeOffsets(rng) + if err != nil { + return res, err + } + + // Find the identifier under the cursor. + // Use the selection range so that haphazard selections that + // happen to start in an identifier don't produce spurious matches. + res.found = res.file.IdentAt(start, end) + if res.found == nil { + return res, nil + } + + // Resolve a package-qualified symbol (including a current-package + // symbol such as ·foo) to its Go declaration. + if mp != nil { + sym := res.found.Name + if sym != "" && sym[0] == '.' { + sym = string(mp.PkgPath) + sym + } + if pkgpath, name, ok := morestrings.CutLast(sym, "."); ok { + // Find declaring package among dependencies. + // + // TODO(adonovan): assembly may legally reference + // non-dependencies. For example, sync/atomic calls + // internal/runtime/atomic. Perhaps we should search + // the entire metadata graph, but that's path-dependent. + var declaring *metadata.Package + for pkg := range snapshot.MetadataGraph().ForwardReflexiveTransitiveClosure(mp.ID) { + if pkg.PkgPath == metadata.PackagePath(pkgpath) { + declaring = pkg + break + } + } + if declaring != nil { + pkgs, err := snapshot.TypeCheck(ctx, declaring.ID) + if err != nil { + return res, err + } + res.pkg = pkgs[0] + res.obj = res.pkg.Types().Scope().Lookup(name) + } + // If obj is nil (no Go declaration, e.g. an asm-only + // symbol), fall through to the local-definition search. + } + } + + // Find the definition of a local symbol — a label, a bare TEXT/GLOBL + // symbol, or a package-qualified symbol without a Go declaration — in + // the assembly file. + if res.obj == nil { + // Labels are function-scoped: a label definition matches only + // within the enclosing TEXT function, so that a jump doesn't land + // on a same-named label in another function. + lo, hi := res.file.FunctionRange(res.found.Offset) + for _, id := range res.file.Idents { + if id.Name != res.found.Name { + continue + } + switch id.Kind { + case asm.Text, asm.Global: + res.localDef = &id + case asm.Label: + if lo <= id.Offset && id.Offset < hi { + res.localDef = &id + } + } + if res.localDef != nil { + break + } + } + } + + return res, nil +}
diff --git a/gopls/internal/golang/addtest.go b/gopls/internal/golang/addtest.go index cef26a4..502cb04 100644 --- a/gopls/internal/golang/addtest.go +++ b/gopls/internal/golang/addtest.go
@@ -26,9 +26,9 @@ "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" )
diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go index 00f62a9..a8d55f0 100644 --- a/gopls/internal/golang/assembly.go +++ b/gopls/internal/golang/assembly.go
@@ -116,14 +116,15 @@ // replace the "(/file.go:123)" portion with an "L0123" source link. // Skip filenames of the form "<foo>". if parts := insnRx.FindStringSubmatch(line); parts != nil { - link := " " // if unknown + linkHTML := template.HTML(" ") // if unknown if file, linenum, ok := morestrings.CutLast(parts[2], ":"); ok && !strings.HasPrefix(file, "<") { if linenum, err := strconv.Atoi(linenum); err == nil { - text := fmt.Sprintf("L%04d", linenum) - link = sourceLink(text, web.SrcURL(file, linenum, 1)) + linkHTML = sourceLinkHTML( + template.HTML(fmt.Sprintf("L%04d", linenum)), + web.SrcURL(file, linenum, 1)) } } - fmt.Fprintf(&buf, "%s\t%s\t%s", escape(parts[1]), link, escape(parts[3])) + fmt.Fprintf(&buf, "%s\t%s\t%s", escape(parts[1]), linkHTML, escape(parts[3])) } else { buf.WriteString(escape(line)) }
diff --git a/gopls/internal/golang/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go index 9378438..adb17c8 100644 --- a/gopls/internal/golang/call_hierarchy.go +++ b/gopls/internal/golang/call_hierarchy.go
@@ -17,9 +17,9 @@ "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/typesinternal" )
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 9ae120a..14465d4 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go
@@ -257,6 +257,7 @@ {kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true}, {kind: settings.RefactorInlineVariable, fn: refactorInlineVariable, needPkg: true}, {kind: settings.RefactorMoveType, fn: refactorMoveType, needPkg: true}, + {kind: settings.RefactorMoveDeclaration, fn: refactorMoveDeclaration, needPkg: true}, {kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote}, {kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true}, {kind: settings.RefactorRewriteFillSwitch, fn: refactorRewriteFillSwitch, needPkg: true}, @@ -1259,3 +1260,16 @@ } return nil } + +func refactorMoveDeclaration(_ context.Context, req *codeActionsRequest) error { + if !req.snapshot.Options().MoveDeclaration { + return nil + } + if !supportsDialog(req.snapshot.Options().ClientOptions, moveDeclarationFormFile, moveDeclarationFormString) { + return nil + } + curSel, _ := req.pgf.Cursor().FindByPos(req.start, req.end) + cmd := command.NewMoveDeclarationCommand(fmt.Sprintf("Move declaration %s", curSel.Node()), command.MoveDeclarationArgs{Location: req.loc}) + req.addCommandAction(cmd, false) + return nil +}
diff --git a/gopls/internal/golang/completion/builtin.go b/gopls/internal/golang/completion/builtin.go index 68f773e..d465b84 100644 --- a/gopls/internal/golang/completion/builtin.go +++ b/gopls/internal/golang/completion/builtin.go
@@ -91,10 +91,11 @@ break } - inf.objType = deslice(inf.objType) - - // Check if we are completing the variadic append() param. - inf.variadic = exprIdx == 1 && len(call.Args) <= 2 + if !call.Ellipsis.IsValid() { + inf.objType = deslice(inf.objType) + // Check if we are completing the variadic append() param. (TestIssue74564) + inf.variadic = exprIdx == 1 && len(call.Args) <= 2 + } // Penalize the first append() argument as a candidate. You // don't normally append a slice to itself.
diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index 411e708..ed146c0 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go
@@ -40,6 +40,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/imports" + "golang.org/x/tools/internal/stdlib" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" @@ -274,8 +275,7 @@ // [typesinternal.TooNewStdSymbols], recording for each std // package which of its exported symbols are too new for // the version of Go in force in the completion file. - // (The value is the minimum version in the form "go1.%d".) - tooNewSymbolsCache map[*types.Package]map[types.Object]string + tooNewSymbolsCache map[*types.Package]map[types.Object]stdlib.Symbol // mapper converts the positions in the file from which the completion originated. mapper *protocol.Mapper @@ -313,7 +313,7 @@ disallowed = typesinternal.TooNewStdSymbols(pkg, c.goversion) c.tooNewSymbolsCache[pkg] = disallowed } - return disallowed[obj] != "" + return disallowed[obj] != stdlib.Symbol{} } // funcInfo holds info about a function object. @@ -645,7 +645,7 @@ // default to a matcher that always matches matcher: prefixMatcher(""), methodSetCache: make(map[methodSetKey]*types.MethodSet), - tooNewSymbolsCache: make(map[*types.Package]map[types.Object]string), + tooNewSymbolsCache: make(map[*types.Package]map[types.Object]stdlib.Symbol), mapper: pgf.Mapper, startTime: startTime, scopes: scopes, @@ -1379,14 +1379,11 @@ prefix := sel.Sel.Name if c.surrounding != nil { if c.surrounding.content != sel.Sel.Name { - // the bug reports do not include the Reportf strings just the line numbers - if len(c.surrounding.content) == 0 { - bug.Reportf("surrounding is empty, should be %q", sel.Sel.Name) - } else if len(sel.Sel.Name) == 0 { - bug.Reportf("sel.Sel.Name is empty, should be %q", c.surrounding.content) - } else { + if sel.Sel.Name != "_" && sel.Sel.Name != "" { bug.Reportf("unexpected surrounding: %q != %q", c.surrounding.content, sel.Sel.Name) + } + prefix = c.surrounding.Prefix() } else { prefix = sel.Sel.Name[:c.surrounding.cursor-c.surrounding.start] } @@ -2726,7 +2723,7 @@ inf.objType = sig.Params().At(exprIdx).Type() } - if sig.Variadic() && exprIdx >= (numParams-1) { + if sig.Variadic() && exprIdx >= (numParams-1) && !node.Ellipsis.IsValid() { // If we are completing a variadic param, deslice the variadic type. inf.objType = deslice(inf.objType) // Record whether we are completing the initial variadic param.
diff --git a/gopls/internal/golang/completion/deep_completion.go b/gopls/internal/golang/completion/deep_completion.go index 6987fd7..1d6ad7f 100644 --- a/gopls/internal/golang/completion/deep_completion.go +++ b/gopls/internal/golang/completion/deep_completion.go
@@ -12,6 +12,7 @@ "golang.org/x/tools/gopls/internal/util/typesutil" "golang.org/x/tools/internal/packagepath" + "golang.org/x/tools/internal/typesinternal" ) // MaxDeepCompletions limits deep completion results because in most cases @@ -269,8 +270,9 @@ // If obj is a *types.TypeName that didn't otherwise match, check // if a literal object of this type makes a good candidate. - // We only care about named types (i.e. don't want builtin types). - if _, isNamed := obj.Type().(*types.Named); isNamed { + // We only care about named or alias types without unspecified type + // params: instantiated generic types are already handled by injectType. + if pnt, ok := obj.Type().(typesinternal.NamedOrAlias); ok && pnt.TypeParams().Len() == 0 { c.literal(ctx, obj.Type(), cand.imp) } }
diff --git a/gopls/internal/golang/completion/format.go b/gopls/internal/golang/completion/format.go index ba50c4a..583cfe6 100644 --- a/gopls/internal/golang/completion/format.go +++ b/gopls/internal/golang/completion/format.go
@@ -198,10 +198,15 @@ } } + // replacing suffix with conv.suffix was wrong (Issue 80268) + // That produced `(*[10]int)(array)` for passing an + // array to a function expecting an array pointer. But + // `(*[10]int)(array[:])` is what is wanted. + // (conv.suffix is always ")" or empty) if cand.convertTo != nil { conv := c.formatConversion(cand.convertTo) prefix = conv.prefix + prefix - suffix = conv.suffix + suffix = suffix + conv.suffix } if prefix != "" {
diff --git a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go index f28fa9b..0c6d899 100644 --- a/gopls/internal/golang/definition.go +++ b/gopls/internal/golang/definition.go
@@ -266,16 +266,18 @@ if obj.Pkg() == types.Unsafe { // package "unsafe": // parse $GOROOT/src/unsafe/unsafe.go - // - // (Strictly, we shouldn't assume that the ID of a std - // package is its PkgPath, but no Bazel+gopackagesdriver - // users have complained about this yet.) - unsafe := snapshot.Metadata("unsafe") + var unsafe *metadata.Package + if mps := snapshot.MetadataGraph().ForPackagePath[metadata.PackagePath("unsafe")]; len(mps) > 0 { + unsafe = mps[0] + } if unsafe == nil { // If the type checker somehow resolved 'unsafe', we must have metadata // for it. return nil, nil, bug.Errorf("no metadata for package 'unsafe'") } + if len(unsafe.GoFiles) == 0 { + return nil, nil, bug.Errorf("no files for package 'unsafe'") + } uri := unsafe.GoFiles[0] fh, err := snapshot.ReadFile(ctx, uri) if err != nil {
diff --git a/gopls/internal/golang/diagnostics.go b/gopls/internal/golang/diagnostics.go index 6708d32..b49c96e 100644 --- a/gopls/internal/golang/diagnostics.go +++ b/gopls/internal/golang/diagnostics.go
@@ -11,7 +11,7 @@ "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/progress" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/moremaps" + "golang.org/x/tools/internal/moremaps" ) // DiagnoseFile returns pull-based diagnostics for the given file.
diff --git a/gopls/internal/golang/extracttofile.go b/gopls/internal/golang/extracttofile.go index 9bd3d5b..054220f 100644 --- a/gopls/internal/golang/extracttofile.go +++ b/gopls/internal/golang/extracttofile.go
@@ -98,8 +98,6 @@ if !ok { return nil, fmt.Errorf("invalid selection") } - pgf.CheckPos(start) // #70553 - // Inv: start is valid wrt pgf.Tok. // select trailing empty lines offset, err := safetoken.Offset(pgf.Tok, end) @@ -109,21 +107,10 @@ rest := pgf.Src[offset:] spaces := len(rest) - len(bytes.TrimLeft(rest, " \t\n")) end += token.Pos(spaces) - pgf.CheckPos(end) // #70553 - if !(start <= end) { - bug.Reportf("start: not before end") + src, err := pgf.PosText(start, end) + if err != nil { + return nil, err } - // Inv: end is valid wrt pgf.Tok; env >= start. - fileStart := pgf.File.FileStart - pgf.CheckPos(fileStart) // #70553 - if !(0 <= start-fileStart) { - bug.Reportf("start: out of bounds") - } - if !(int(end-fileStart) <= len(pgf.Src)) { - bug.Reportf("end: out of bounds") - } - // Inv: 0 <= start-fileStart <= end-fileStart <= len(Src). - src := pgf.Src[start-fileStart : end-fileStart] replaceRange, err := pgf.PosRange(start, end) if err != nil { @@ -180,7 +167,7 @@ newFileContent, err := format.Source(buf.Bytes()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to format extracted source: %w", err) } return []protocol.DocumentChange{
diff --git a/gopls/internal/golang/freesymbols.go b/gopls/internal/golang/freesymbols.go index 3360253..8e3ad2d 100644 --- a/gopls/internal/golang/freesymbols.go +++ b/gopls/internal/golang/freesymbols.go
@@ -12,17 +12,17 @@ "go/ast" "go/token" "go/types" - "html" + "html/template" + "log" "slices" "sort" "strings" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" - "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/typesinternal" ) @@ -36,18 +36,21 @@ // -- model -- type Import struct { - Path metadata.PackagePath - Symbols []string + URL string + Path PackagePath + Symbols string } type Symbol struct { Kind string + Name template.HTML Type string - Refs []types.Object } + // model is [freeSymbolsTmpl]'s operand. var model struct { Imported []Import PkgLevel []Symbol Local []Symbol + Code template.HTML } qualifier := typesinternal.NameRelativeTo(pkg.Types()) @@ -60,8 +63,8 @@ }) // Inspect the references. - imported := make(map[string][]*freeRef) // refs to imported symbols, by package path - seen := make(map[string]bool) // to de-dup dotted paths + imported := make(map[PackagePath][]*freeRef) // refs to imported symbols + seen := make(map[string]bool) // to de-dup dotted paths for _, ref := range refs { if seen[ref.dotted] { continue // de-dup @@ -73,7 +76,7 @@ case "file": // imported symbol: group by package if pkgname, ok := ref.objects[0].(*types.PkgName); ok { - path := pkgname.Imported().Path() + path := PackagePath(pkgname.Imported().Path()) imported[path] = append(imported[path], ref) } continue @@ -110,10 +113,23 @@ typestr = "" // avoid "label L L" } + // Join name with dots. + var nameHTML template.HTML + { + var buf strings.Builder + for i, obj := range ref.objects { + if i > 0 { + buf.WriteByte('.') + } + buf.WriteString(string(objHTML(pkg.FileSet(), web, obj))) + } + nameHTML = template.HTML(buf.String()) + } + *symbols = append(*symbols, Symbol{ Kind: kind, + Name: nameHTML, Type: typestr, - Refs: ref.objects, }) } @@ -131,83 +147,20 @@ syms[max-1] = fmt.Sprintf("... (%d)", len(syms)) syms = syms[:max] } + symbols := strings.Join(syms, ", ") model.Imported = append(model.Imported, Import{ - Path: PackagePath(pkgPath), - Symbols: syms, + URL: web.PkgURL(viewID, pkgPath, ""), + Path: pkgPath, + Symbols: symbols, }) } } - // -- presentation -- - - var buf bytes.Buffer - buf.WriteString(`<!DOCTYPE html> -<html> -<head> -<style> -.col-pkg { color: #2eb007 } -.col-file { color: #a10b15 } -.col-local { color: #0cb7c9 } -li { font-family: monospace; } -p { max-width: 6in; } -</style> - <script src="/assets/common.js"></script> - <link rel="stylesheet" href="/assets/common.css"> -</head> -<body> -<h1>Free symbols</h1> -<p> - The selected code contains references to these free* symbols: -</p> -`) - - // Present the refs in three sections: imported, same package, local. - - // -- imported symbols -- - - // Show one item per package, with a list of symbols. - fmt.Fprintf(&buf, "<h2><span class='col-file'>⬤</span> Imported symbols</h2>\n") - fmt.Fprintf(&buf, "<ul>\n") - for _, imp := range model.Imported { - fmt.Fprintf(&buf, "<li>import \"<a href='%s'>%s</a>\" // for %s</li>\n", - web.PkgURL(viewID, imp.Path, ""), - html.EscapeString(string(imp.Path)), - strings.Join(imp.Symbols, ", ")) - } - if len(model.Imported) == 0 { - fmt.Fprintf(&buf, "<li>(none)</li>\n") - } - buf.WriteString("</ul>\n") - - // -- package and local symbols -- - - showSymbols := func(scope, title string, symbols []Symbol) { - fmt.Fprintf(&buf, "<h2><span class='col-%s'>⬤</span> %s</h2>\n", scope, title) - fmt.Fprintf(&buf, "<ul>\n") - pre := buf.Len() - for _, sym := range symbols { - fmt.Fprintf(&buf, "<li>%s ", sym.Kind) // of rightmost symbol in dotted path - for i, obj := range sym.Refs { - if i > 0 { - buf.WriteByte('.') - } - buf.WriteString(objHTML(pkg.FileSet(), web, obj)) - } - fmt.Fprintf(&buf, " %s</li>\n", html.EscapeString(sym.Type)) - } - if buf.Len() == pre { - fmt.Fprintf(&buf, "<li>(none)</li>\n") - } - buf.WriteString("</ul>\n") - } - showSymbols("pkg", "Package-level symbols", model.PkgLevel) - showSymbols("local", "Local symbols", model.Local) - // -- code selection -- - // Print the selection, highlighting references to free symbols. - buf.WriteString("<hr/>\n") + // Highlight references to free symbols in the selection. + var code strings.Builder // (valid HTML) sort.Slice(refs, func(i, j int) bool { return refs[i].expr.Pos() < refs[j].expr.Pos() }) @@ -216,46 +169,25 @@ if pos < end { fileStart := pgf.File.FileStart text := pgf.Mapper.Content[pos-fileStart : end-fileStart] - buf.WriteString(html.EscapeString(string(text))) + code.WriteString(template.HTMLEscapeString(string(text))) pos = end } } - buf.WriteString(`<pre>`) for _, ref := range refs { emitTo(ref.expr.Pos()) - fmt.Fprintf(&buf, `<b class='col-%s'>`, ref.scope) + fmt.Fprintf(&code, `<b class='col-%s'>`, ref.scope) // no escaping needed emitTo(ref.expr.End()) - buf.WriteString(`</b>`) + code.WriteString(`</b>`) } emitTo(end) - buf.WriteString(`</pre> -<hr> -<p> - *A symbol is "free" if it is referenced within the selection but declared - outside of it. + model.Code = template.HTML(code.String()) - The free variables are approximately the set of parameters that - would be needed if the block were extracted into its own function in - the same package. + // -- presentation -- - Free identifiers may include local types and control labels as well. - - Even when you don't intend to extract a block into a new function, - this information can help you to tell at a glance what names a block - of code depends on. -</p> -<p> - Each dotted path of identifiers (such as file.Name.Pos) is reported - as a separate item, so that you can see which parts of a complex - type are actually needed. - - The free symbols referenced by the body of a function may - reveal that only a small part (a single field of a struct, say) of - one of the function's parameters is used, allowing you to simplify - and generalize the function by choosing a different type for that - parameter. -</p> -`) + var buf bytes.Buffer + if err := freeSymbolsTmpl.Execute(&buf, model); err != nil { + log.Fatal(err) + } return buf.Bytes() } @@ -399,22 +331,103 @@ // objHTML returns HTML for obj.Name(), possibly marked up as a link // to the web server that, when visited, opens the declaration in the // client editor. -func objHTML(fset *token.FileSet, web Web, obj types.Object) string { - text := obj.Name() +func objHTML(fset *token.FileSet, web Web, obj types.Object) template.HTML { + // Go symbol names don't need escaping, but be conservative. + html := template.HTML(template.HTMLEscapeString(obj.Name())) if posn := safetoken.StartPosition(fset, obj.Pos()); posn.IsValid() { - url := web.SrcURL(posn.Filename, posn.Line, posn.Column) - return sourceLink(text, url) + return sourceLinkHTML(html, web.SrcURL(posn.Filename, posn.Line, posn.Column)) } - return text + return html } -// sourceLink returns HTML for a link to open a file in the client editor. -func sourceLink(text, url string) string { +// sourceLinkHTML returns HTML for a link to open a file in the client editor. +func sourceLinkHTML(body template.HTML, url string) template.HTML { + var buf strings.Builder + if err := sourceLinkTmpl.Execute(&buf, sourceLinkData{URL: url, Body: body}); err != nil { + panic(err) + } + return template.HTML(buf.String()) +} + +type sourceLinkData struct { + URL string + Body template.HTML +} + +var sourceLinkTmpl = template.Must(template.New("sourcelink").Parse( // The /src URL returns nothing but has the side effect // of causing the LSP client to open the requested file. // So we use onclick to prevent the browser from navigating. // We keep the href attribute as it causes the <a> to render // as a link: blue, underlined, with URL hover information. - return fmt.Sprintf(`<a href="%[1]s" onclick='return httpGET("%[1]s")'>%[2]s</a>`, - html.EscapeString(url), text) -} + `<a href='{{.URL}}' onclick='return httpGET({{.URL}})'>{{.Body}}</a>`)) + +var freeSymbolsTmpl = template.Must(template.New("freesymbols").Parse(`<!DOCTYPE html> +<html> +<head> +<style> +.col-pkg { color: #2eb007 } +.col-file { color: #a10b15 } +.col-local { color: #0cb7c9 } +li { font-family: monospace; } +p { max-width: 6in; } +</style> + <script src="/assets/common.js"></script> + <link rel="stylesheet" href="/assets/common.css"> +</head> +<body> +<h1>Free symbols</h1> +<p> + The selected code contains references to these free* symbols: +</p> + +<h2><span class='col-file'>⬤</span> Imported symbols</h2> +<ul> +{{range .Imported}}<li>import "<a href='{{.URL}}'>{{.Path}}</a>" // for {{.Symbols}}</li> +{{else}}<li>(none)</li> +{{end}}</ul> + +<h2><span class='col-pkg'>⬤</span> Package-level symbols</h2> +<ul> +{{range .PkgLevel}}<li>{{.Kind}} {{.Name}} {{.Type}}</li> +{{else}}<li>(none)</li> +{{end}}</ul> + +<h2><span class='col-local'>⬤</span> Local symbols</h2> +<ul> +{{range .Local}}<li>{{.Kind}} {{.Name}} {{.Type}}</li> +{{else}}<li>(none)</li> +{{end}} +</ul> + +<hr/> +<pre>{{.Code}}</pre> +<hr> +<p> + * A symbol is "free" if it is referenced within the selection but declared + outside of it. + + The free variables are approximately the set of parameters that + would be needed if the block were extracted into its own function in + the same package. + + Free identifiers may include local types and control labels as well. + + Even when you don't intend to extract a block into a new function, + this information can help you to tell at a glance what names a block + of code depends on. +</p> +<p> + Each dotted path of identifiers (such as file.Name.Pos) is reported + as a separate item, so that you can see which parts of a complex + type are actually needed. + + The free symbols referenced by the body of a function may + reveal that only a small part (a single field of a struct, say) of + one of the function's parameters is used, allowing you to simplify + and generalize the function by choosing a different type for that + parameter. +</p> +</body> +</html> +`))
diff --git a/gopls/internal/golang/inlay_hint.go b/gopls/internal/golang/inlay_hint.go index a9e7027..74a6634 100644 --- a/gopls/internal/golang/inlay_hint.go +++ b/gopls/internal/golang/inlay_hint.go
@@ -140,6 +140,13 @@ } func ignoredError(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur inspector.Cursor, add func(protocol.InlayHint)) { + ignoreSubstrings := []string{ + "ignore error", + "discard error", + "can't fail", + "cannot fail", + } + outer: for curCall := range cur.Preorder((*ast.ExprStmt)(nil)) { stmt := curCall.Node().(*ast.ExprStmt) @@ -171,7 +178,7 @@ continue } - // Suppress if comment on same line contains "// ignore error". + // Suppress if comment on same line contains "ignore error" (etc). line := func(pos token.Pos) int { return safetoken.Line(pgf.Tok, pos) } comments := pgf.File.Comments compare := func(cg *ast.CommentGroup, pos token.Pos) int { @@ -180,8 +187,12 @@ i, _ := slices.BinarySearchFunc(comments, stmt.End(), compare) if i >= 0 && i < len(comments) { cg := comments[i] - if line(cg.Pos()) == line(stmt.End()) && strings.Contains(cg.Text(), "ignore error") { - continue outer // suppress + if line(cg.Pos()) == line(stmt.End()) { + for _, sub := range ignoreSubstrings { + if strings.Contains(cg.Text(), sub) { + continue outer // suppress + } + } } }
diff --git a/gopls/internal/golang/inline.go b/gopls/internal/golang/inline.go index d88bce2..dff9cef 100644 --- a/gopls/internal/golang/inline.go +++ b/gopls/internal/golang/inline.go
@@ -24,6 +24,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/refactor/inline" + "golang.org/x/tools/internal/typesinternal" ) // enclosingStaticCall returns the innermost function call enclosing @@ -144,7 +145,7 @@ // initializer expression. func canInlineVariable(info *types.Info, curFile inspector.Cursor, start, end token.Pos) (_, _ inspector.Cursor, ok bool) { if curUse, ok := curFile.FindByPos(start, end); ok { - if id, ok := curUse.Node().(*ast.Ident); ok && !isLvalueUse(curUse, info) { + if id, ok := curUse.Node().(*ast.Ident); ok && !typesinternal.IsAssignedOrAddressTaken(info, curUse) { if v, ok := info.Uses[id].(*types.Var); ok && v.Kind() == types.LocalVar { if curIdent, ok := curFile.FindByPos(v.Pos(), v.Pos()); ok { curParent := curIdent.Parent() @@ -170,47 +171,6 @@ return } -// isLvalueUse reports whether the "use" identifier represented by cur -// appears in an l-value context such as: -// -// - v=... -// - v++ -// - &v -// - v.f(), when this implicitly takes the address of v. -func isLvalueUse(cur inspector.Cursor, info *types.Info) bool { - cur = unparenEnclosing(cur) - - switch cur.ParentEdgeKind() { - case edge.AssignStmt_Lhs, edge.IncDecStmt_X: - return true // v=..., v++ - - case edge.UnaryExpr_X: - return cur.Parent().Node().(*ast.UnaryExpr).Op == token.AND // &v - - case edge.SelectorExpr_X: - sel := cur.Parent().Node().(*ast.SelectorExpr) - isPointer := func(t types.Type) bool { - return is[*types.Pointer](t) - } - if seln, ok := info.Selections[sel]; ok && seln.Kind() == types.MethodVal { - // Have: recv.f() method call - methodRecv := seln.Obj().(*types.Func).Signature().Recv().Type() - return !seln.Indirect() && isPointer(methodRecv) && !isPointer(info.TypeOf(sel.X)) - } - } - - return false -} - -// unparenEnclosing removes enclosing parens from cur in -// preparation for a call to [Cursor.ParentEdge]. -func unparenEnclosing(cur inspector.Cursor) inspector.Cursor { - for cur.ParentEdgeKind() == edge.ParenExpr_X { - cur = cur.Parent() - } - return cur -} - // inlineVariableOne computes a fix to replace the selected variable by // its initialization expression. func inlineVariableOne(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) {
diff --git a/gopls/internal/golang/inline_all.go b/gopls/internal/golang/inline_all.go index c7a1dbb..a22c052 100644 --- a/gopls/internal/golang/inline_all.go +++ b/gopls/internal/golang/inline_all.go
@@ -17,9 +17,9 @@ "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/analysis/driverutil" "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor/inline" )
diff --git a/gopls/internal/golang/movedeclaration.go b/gopls/internal/golang/movedeclaration.go new file mode 100644 index 0000000..85808ce --- /dev/null +++ b/gopls/internal/golang/movedeclaration.go
@@ -0,0 +1,17 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package golang + +import ( + "context" + + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/protocol" +) + +func MoveDeclaration(ctx context.Context, fh file.Handle, snapshot *cache.Snapshot) ([]protocol.DocumentChange, protocol.Location, error) { + return nil, protocol.Location{}, nil +}
diff --git a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go index 9781a26..1394b15 100644 --- a/gopls/internal/golang/pkgdoc.go +++ b/gopls/internal/golang/pkgdoc.go
@@ -11,8 +11,7 @@ // placeholder for a more sophisticated one. // // TODO(adonovan): -// - rewrite using html/template. -// Or factor with golang.org/x/pkgsite/internal/godoc/dochtml. +// - rewrite using html/template. No, really, let's do this. // - emit breadcrumbs for parent + sibling packages. // - list promoted methods---we have type information! (golang/go#67158) // - gather Example tests, following go/doc and pkgsite. @@ -38,7 +37,7 @@ "go/format" "go/token" "go/types" - "html" + "html/template" "path/filepath" "slices" "strings" @@ -357,7 +356,7 @@ } scope := pkg.Types().Scope() - escape := html.EscapeString + escape := template.HTMLEscapeString title := fmt.Sprintf("%s package - %s - Gopls packages", pkg.Types().Name(), escape(pkg.Types().Path())) @@ -574,7 +573,7 @@ emit(n.Pos()) pos = n.End() if url := linkify(n); url != "" { - fmt.Fprintf(&buf, "<a class='id' href='%s'>%s</a>", url, escape(n.Name)) + fmt.Fprintf(&buf, "<a class='id' href='%s'>%s</a>", escape(url), escape(n.Name)) } else { buf.WriteString(escape(n.Name)) // plain } @@ -831,7 +830,9 @@ fmt.Fprintf(&buf, "<h2 id='hdr-SourceFiles'>Source files</h2>\n") for _, filename := range docpkg.Filenames { fmt.Fprintf(&buf, "<div class='comment'>%s</div>\n", - sourceLink(filepath.Base(filename), web.SrcURL(filename, 1, 1))) + sourceLinkHTML( + template.HTML(template.HTMLEscapeString(filepath.Base(filename))), + web.SrcURL(filename, 1, 1))) } fmt.Fprintf(&buf, "</main>\n")
diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index 9e04f8d..b093326 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go
@@ -611,6 +611,15 @@ targets = []objectAt{{obj, cur}} } + // A field selected from an instantiated generic type is represented by a + // synthetic *types.Var. Normalize it to the declared field so that a rename + // initiated at the selection updates the declaration too (golang/go#80542). + for i := range targets { + if field, ok := targets[i].obj.(*types.Var); ok && field.IsField() { + targets[i].obj = field.Origin() + } + } + // Pick a representative object arbitrarily. // (All share the same name, pos, and kind.) obj, node := targets[0].obj, targets[0].cur.Node() @@ -654,8 +663,6 @@ case *types.Func: obj = obj0.Origin() case *types.Var: - // TODO(adonovan): do vars need the origin treatment too? (issue #58462) - // Function parameter and result vars that are (unusually) // capitalized are technically exported, even though they // cannot be referenced, because they may affect downstream
diff --git a/gopls/internal/golang/resolve.go b/gopls/internal/golang/resolve.go index c4ad48c..20f3703 100644 --- a/gopls/internal/golang/resolve.go +++ b/gopls/internal/golang/resolve.go
@@ -50,7 +50,7 @@ { ID: "tags", Description: `comma-separated list of tags to add; e.g.. "json,xml"`, - Type: protocol.FormFieldTypeString{Kind: "string"}, + Type: protocol.FormFieldTypeString{Kind: protocol.FormFieldKindString}, Required: true, Default: "json", }, @@ -58,7 +58,7 @@ ID: "transform", Description: `transform rule for added tags, e.g., "camelcase' or 'snakecase"`, Type: protocol.FormFieldTypeEnum{ - Kind: "enum", + Kind: protocol.FormFieldKindEnum, Entries: []protocol.FormEnumEntry{ { Value: "camelcase", @@ -91,7 +91,7 @@ { ID: "tags", Description: `comma-separated list of tags to remove; e.g., "json,xml"`, - Type: protocol.FormFieldTypeString{Kind: "string"}, + Type: protocol.FormFieldTypeString{Kind: protocol.FormFieldKindString}, Required: true, Default: "json", // TODO(?): put the existing tags here? }, @@ -115,6 +115,10 @@ if err := resolveImplementInterface(options, params); err != nil { return nil, err } + case "gopls.move_declaration": + if err := resolveMoveDeclaration(options, params); err != nil { + return nil, err + } } return params, nil } @@ -198,7 +202,7 @@ ID: "interface", Description: `fully qualified interface identifier path/to/pkg.interface; e.g., "net.Error"`, Type: protocol.FormFieldTypeLazyEnum{ - Kind: "lazyEnum", + Kind: protocol.FormFieldKindLazyEnum, Source: "workspaceSymbol", Config: mustMarshal(InteractiveWorkspaceSymbolEnumConfig{ Kinds: []protocol.SymbolKind{protocol.Interface}, @@ -214,13 +218,35 @@ ID: "interface", Description: `fully qualified interface identifier path/to/pkg.interface; e.g., "net.Error"`, Type: protocol.FormFieldTypeString{ - Kind: "string", + Kind: protocol.FormFieldKindString, }, Required: true, Default: "error", }, } +var moveDeclarationFormString = []protocol.FormField{ + { + ID: "string", + Description: "destination file uri for the moved declaration, e.g. file:///path/to/file.go", + Type: protocol.FormFieldTypeFile{ + Kind: "string", + }, + Required: true, + }, +} + +var moveDeclarationFormFile = []protocol.FormField{ + { + ID: "file", + Description: "destination file for the moved declaration", + Type: protocol.FormFieldTypeFile{ + Kind: "file", + }, + Required: true, + }, +} + func resolveImplementInterface(options settings.ClientOptions, param *protocol.ExecuteCommandParams) error { var a0 command.ImplementInterfaceArgs if err := command.UnmarshalArgs(param.Arguments, &a0); err != nil { @@ -285,6 +311,39 @@ return nil } +func resolveMoveDeclaration(options settings.ClientOptions, param *protocol.ExecuteCommandParams) error { + var a0 command.MoveDeclarationArgs + if err := command.UnmarshalArgs(param.Arguments, &a0); err != nil { + return err + } + var form []protocol.FormField + if ok := options.SupportedInteractiveInputTypes[settings.InteractiveInputTypeFile]; ok { + form = moveDeclarationFormFile + } else if ok := options.SupportedInteractiveInputTypes[settings.InteractiveInputTypeString]; ok { + form = moveDeclarationFormString + } else { + // This should not happen because gopls should not offer this code action if the + // language client does not support any kind above. + return fmt.Errorf("internal error: unsupported interactive input types: %v", options.SupportedInteractiveInputTypes) + } + + // First call, return the empty form. + if len(param.FormAnswers) == 0 { + param.FormFields = form + return nil + } + + file, err := FormAnswer[string](¶m.InteractiveParams, "file") + if err != nil { + return err + } + if _, err := protocol.ParseDocumentURI(file); err != nil { + return err + } + param.FormFields = nil + return nil +} + // FormAnswer finds, validates, and returns the unique answer for id. // // It uses a linear scan since the number of answers is small (usually < 5).
diff --git a/gopls/internal/golang/splitpkg/splitpkg.go b/gopls/internal/golang/splitpkg/splitpkg.go index 32371dd..747cecf 100644 --- a/gopls/internal/golang/splitpkg/splitpkg.go +++ b/gopls/internal/golang/splitpkg/splitpkg.go
@@ -66,8 +66,8 @@ "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/filecache" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/typesinternal" )
diff --git a/gopls/internal/mcp/context.go b/gopls/internal/mcp/context.go index f7ec8ff..13b665d 100644 --- a/gopls/internal/mcp/context.go +++ b/gopls/internal/mcp/context.go
@@ -108,7 +108,7 @@ // Skip the standard library to reduce token usage, operating on // the assumption that the LLM is already familiar with its // symbols and documentation. - if packagepath.IsStdPackage(spec.Path.Value) { + if packagepath.MaybeStdPackage(spec.Path.Value) { continue } toSummarize = append(toSummarize, spec)
diff --git a/gopls/internal/mcp/mcp.go b/gopls/internal/mcp/mcp.go index 988e206..e00e92c 100644 --- a/gopls/internal/mcp/mcp.go +++ b/gopls/internal/mcp/mcp.go
@@ -23,7 +23,7 @@ "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/gopls/internal/util/moremaps" + "golang.org/x/tools/internal/moremaps" ) //go:embed instructions.md @@ -56,18 +56,22 @@ if strings.HasPrefix(address, ":") { return fmt.Errorf("address %s implicitly binds all network interfaces; please use an explicit host such as 0.0.0.0 (all interfaces) or localhost (safer)", address) } - log.Printf("Gopls MCP server: starting up on http") + listener, err := net.Listen("tcp", address) if err != nil { return err } defer listener.Close() - // TODO(hxjiang): expose the MCP server address to the LSP client. - if isDaemon { - log.Printf("Gopls MCP daemon: listening on address %s...", listener.Addr()) + { + kind := "server" + if isDaemon { + kind = "daemon" + } + + log.Printf("Gopls MCP %s: listening on %s", kind, listener.Addr()) + defer log.Printf("Gopls MCP %s: exiting", kind) } - defer log.Printf("Gopls MCP server: exiting") svr := http.Server{ Handler: HTTPHandler(sessions, isDaemon, rootsHandler), @@ -81,7 +85,6 @@ <-ctx.Done() svr.Close() // ignore error }() - log.Printf("mcp http server listening") return svr.Serve(listener) }
diff --git a/gopls/internal/mcp/references.go b/gopls/internal/mcp/references.go index d49bce1..2e82a42 100644 --- a/gopls/internal/mcp/references.go +++ b/gopls/internal/mcp/references.go
@@ -62,7 +62,7 @@ } else { continue } - fmt.Fprintf(&builder, "The reference is located on line %v, which has content `%s`\n", r.Range.Start.Line, lineContent) + fmt.Fprintf(&builder, "The reference is located on line %v, which has content `%s`\n", r.Range.Start.Line+1, lineContent) builder.WriteString("\n") } return textResult(builder.String()), nil
diff --git a/gopls/internal/mod/diagnostics.go b/gopls/internal/mod/diagnostics.go index 52f3704..b31a3d3 100644 --- a/gopls/internal/mod/diagnostics.go +++ b/gopls/internal/mod/diagnostics.go
@@ -24,6 +24,7 @@ "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/moremaps" ) // ParseDiagnostics returns diagnostics from parsing the go.mod files in the workspace. @@ -421,12 +422,9 @@ } func sortedKeys(m map[string]bool) []string { - ret := make([]string, 0, len(m)) - for k := range m { - ret = append(ret, k) - } - sort.Strings(ret) - return ret + keys := moremaps.KeySlice(m) + sort.Strings(keys) + return keys } // suggestGovulncheckAction returns a code action that suggests either run govulncheck
diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index 7d8c47c..16b5a40 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go
@@ -50,6 +50,7 @@ MemStats Command = "gopls.mem_stats" ModifyTags Command = "gopls.modify_tags" Modules Command = "gopls.modules" + MoveDeclaration Command = "gopls.move_declaration" MoveType Command = "gopls.move_type" PackageSymbols Command = "gopls.package_symbols" Packages Command = "gopls.packages" @@ -100,6 +101,7 @@ MemStats, ModifyTags, Modules, + MoveDeclaration, MoveType, PackageSymbols, Packages, @@ -276,6 +278,12 @@ return nil, err } return s.Modules(ctx, a0) + case MoveDeclaration: + var a0 MoveDeclarationArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return nil, s.MoveDeclaration(ctx, a0, ¶ms.InteractiveParams) case MoveType: var a0 MoveTypeArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -603,6 +611,14 @@ } } +func NewMoveDeclarationCommand(title string, a0 MoveDeclarationArgs) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: MoveDeclaration.String(), + Arguments: MustMarshalArgs(a0), + } +} + func NewMoveTypeCommand(title string, a0 MoveTypeArgs) *protocol.Command { return &protocol.Command{ Title: title,
diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index d1757ae..80ece52 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go
@@ -204,7 +204,7 @@ // runner. StopProfile(context.Context, StopProfileArgs) (StopProfileResult, error) - // GoVulncheck: run vulncheck synchronously. + // Vulncheck: run vulncheck synchronously. // // Run vulnerability check (`govulncheck`). // @@ -336,6 +336,9 @@ // ImplementInterface: Add methods to a type to implement an interface. ImplementInterface(context.Context, ImplementInterfaceArgs, *protocol.InteractiveParams) error + + // MoveDeclaration: Move a declaration to a different file. + MoveDeclaration(context.Context, MoveDeclarationArgs, *protocol.InteractiveParams) error } type RunTestsArgs struct { @@ -914,3 +917,9 @@ // TODO(mkalil): Determine format of the parameter that specifies where to // move the type to. } + +// MoveDeclarationArgs specifies a "move declaration" refactoring to perform. +type MoveDeclarationArgs struct { + // The location of the declaration to move. + Location protocol.Location +}
diff --git a/gopls/internal/protocol/form.go b/gopls/internal/protocol/form.go index d4871f2..da959ef 100644 --- a/gopls/internal/protocol/form.go +++ b/gopls/internal/protocol/form.go
@@ -20,12 +20,25 @@ Kinds []string `json:"kinds"` } +// FormFieldKind represents the supported input kinds for form fields. +type FormFieldKind string + +const ( + FormFieldKindString FormFieldKind = "string" + FormFieldKindFile FormFieldKind = "file" + FormFieldKindBool FormFieldKind = "bool" + FormFieldKindNumber FormFieldKind = "number" + FormFieldKindEnum FormFieldKind = "enum" + FormFieldKindLazyEnum FormFieldKind = "lazyEnum" + FormFieldKindList FormFieldKind = "list" +) + // InteractiveResolveClientCapabilities represents the client capabilities for // interactive resolve. type InteractiveResolveClientCapabilities struct { // The input types the client supports for interactive dialogs. // The presence of this field implies support for interactive refactoring. - InputTypes []string `json:"inputTypes"` + InputTypes []FormFieldKind `json:"inputTypes"` } // FormFieldTypeString defines a text input. @@ -34,7 +47,7 @@ // adding regex validation or file URI constraints. type FormFieldTypeString struct { // Kind must be "string". - Kind string `json:"kind"` + Kind FormFieldKind `json:"kind"` } // FileExistence whether the file denoted by a DocumentURI exists. @@ -73,7 +86,7 @@ // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentUri type FormFieldTypeFile struct { // Kind must be "file". - Kind string `json:"kind"` + Kind FormFieldKind `json:"kind"` // Existence constraint. Existence FileExistence `json:"existence"` @@ -93,7 +106,7 @@ // FormFieldTypeBool defines a boolean input. type FormFieldTypeBool struct { // Kind must be "bool". - Kind string `json:"kind"` + Kind FormFieldKind `json:"kind"` } // FormFieldTypeNumber defines a numeric input. @@ -102,7 +115,7 @@ // adding range constraints (min/max) or precision requirements. type FormFieldTypeNumber struct { // Kind must be "number". - Kind string `json:"kind"` + Kind FormFieldKind `json:"kind"` } // FormEnumEntry represents a single option in an enumeration. @@ -124,7 +137,7 @@ // - All options are known at the time the form is created. type FormFieldTypeEnum struct { // Kind must be "enum". - Kind string `json:"kind"` + Kind FormFieldKind `json:"kind"` // Entries is the list of allowable options. Entries []FormEnumEntry `json:"entries"` @@ -142,7 +155,7 @@ // a text input) and query the server via 'interactive/listEnum' as the user types. type FormFieldTypeLazyEnum struct { // Kind must be "lazyEnum". - Kind string `json:"kind"` + Kind FormFieldKind `json:"kind"` // TODO(hxjiang): consider make debounce configurable since fetching // cloud resources could be expensive and slow. @@ -161,7 +174,7 @@ // FormFieldTypeList defines a homogeneous list of items. type FormFieldTypeList struct { // Kind must be "list". - Kind string `json:"kind"` + Kind FormFieldKind `json:"kind"` // ElementType specifies the type of the items in the list. // It must be one of the FormFieldType* structs (e.g., FormFieldTypeString).
diff --git a/gopls/internal/protocol/semtok/semtok.go b/gopls/internal/protocol/semtok/semtok.go index 1ed0785..f384f36 100644 --- a/gopls/internal/protocol/semtok/semtok.go +++ b/gopls/internal/protocol/semtok/semtok.go
@@ -44,6 +44,9 @@ // // If you move types to above, document it in // gopls/doc/features/passive.md#semantic-tokens. + // + // Sync any changes below with the vscode-go custom token type list. + // See: extension/src/language/goSemanticTokens.ts // TokClass Type = "class" // TokDecorator Type = "decorator" // TokEnum Type = "enum" @@ -103,6 +106,9 @@ // Since the type of a symbol is orthogonal to its kind, // (e.g. a variable can have function type), // we use modifiers for the top-level type constructor. + // + // Sync any changes below with the vscode-go custom token modifier list. + // See: extension/src/language/goSemanticTokens.ts ModArray Modifier = "array" ModBool Modifier = "bool" ModChan Modifier = "chan" @@ -143,8 +149,8 @@ // Encode returns the LSP encoding of a sequence of tokens. // encodeType and encodeModifier maps control which types and modifiers are -// excluded in the response. If a type or modifier maps to false, it will be -// omitted from the output. +// included in the response. A type or modifier must exist in the map and map +// to true to be included in the output. func Encode( tokens []Token, encodeType map[Type]bool, @@ -160,18 +166,16 @@ typeMap := make(map[Type]uint32) for i, t := range Types { - if enable, ok := encodeType[t]; ok && !enable { - continue + if enable, ok := encodeType[t]; ok && enable { + typeMap[Type(t)] = uint32(i) } - typeMap[Type(t)] = uint32(i) } modMap := make(map[Modifier]uint32) for i, m := range Modifiers { - if enable, ok := encodeModifier[m]; ok && !enable { - continue + if enable, ok := encodeModifier[m]; ok && enable { + modMap[Modifier(m)] = 1 << i } - modMap[Modifier(m)] = 1 << i } // Each semantic token needs five values but some tokens might be skipped.
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index d858f17..94fe90c 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go
@@ -1954,3 +1954,15 @@ }) return err } + +func (c *commandHandler) MoveDeclaration(ctx context.Context, args command.MoveDeclarationArgs, params *protocol.InteractiveParams) error { + return c.run(ctx, commandConfig{ + forURI: args.Location.URI, + }, func(ctx context.Context, deps commandDeps) error { + changes, _, err := golang.MoveDeclaration(ctx, deps.fh, deps.snapshot) + if err != nil { + return err + } + return applyChanges(ctx, c.s.client, changes) + }) +}
diff --git a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go index 1eae797d..198bb95 100644 --- a/gopls/internal/server/diagnostics.go +++ b/gopls/internal/server/diagnostics.go
@@ -26,11 +26,11 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/template" - "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/gopls/internal/work" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/moremaps" ) // Diagnostic implements the textDocument/diagnostic LSP request, reporting
diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index 37846a0..296e9c4 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go
@@ -35,10 +35,10 @@ "golang.org/x/tools/gopls/internal/telemetry" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/goversion" - "golang.org/x/tools/gopls/internal/util/moremaps" - "golang.org/x/tools/gopls/internal/util/moreslices" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/moremaps" + "golang.org/x/tools/internal/moreslices" ) func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
diff --git a/gopls/internal/server/highlight.go b/gopls/internal/server/highlight.go index 83b2660..b5aa4bc 100644 --- a/gopls/internal/server/highlight.go +++ b/gopls/internal/server/highlight.go
@@ -8,6 +8,7 @@ "context" "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/goasm" "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" @@ -26,6 +27,8 @@ defer release() switch snapshot.FileKind(fh) { + case file.Asm: + return goasm.Highlight(ctx, snapshot, fh, params.Range) case file.Tmpl: return template.Highlight(ctx, snapshot, fh, params.Range) case file.Go:
diff --git a/gopls/internal/server/hover.go b/gopls/internal/server/hover.go index d0c2c83..7c3284d 100644 --- a/gopls/internal/server/hover.go +++ b/gopls/internal/server/hover.go
@@ -8,6 +8,7 @@ "context" "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/goasm" "golang.org/x/tools/gopls/internal/golang" "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/mod" @@ -35,6 +36,8 @@ defer release() switch snapshot.FileKind(fh) { + case file.Asm: + return goasm.Hover(ctx, snapshot, fh, params.Range) case file.Mod: return mod.Hover(ctx, snapshot, fh, params.Range) case file.Go:
diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go index 247d7b7..326776b 100644 --- a/gopls/internal/settings/analysis.go +++ b/gopls/internal/settings/analysis.go
@@ -14,10 +14,8 @@ "golang.org/x/tools/go/analysis/passes/inline" "golang.org/x/tools/go/analysis/passes/modernize" "golang.org/x/tools/go/analysis/passes/nilness" - "golang.org/x/tools/go/analysis/passes/scannererr" "golang.org/x/tools/go/analysis/passes/shadow" "golang.org/x/tools/go/analysis/passes/sortslice" - "golang.org/x/tools/go/analysis/passes/sqlrowserr" "golang.org/x/tools/go/analysis/passes/unusedwrite" "golang.org/x/tools/go/analysis/suite/fix" "golang.org/x/tools/go/analysis/suite/vet" @@ -29,6 +27,7 @@ "golang.org/x/tools/gopls/internal/analysis/maprange" "golang.org/x/tools/gopls/internal/analysis/nonewvars" "golang.org/x/tools/gopls/internal/analysis/noresultvalues" + "golang.org/x/tools/gopls/internal/analysis/ptrtoerror" "golang.org/x/tools/gopls/internal/analysis/recursiveiter" "golang.org/x/tools/gopls/internal/analysis/simplifycompositelit" "golang.org/x/tools/gopls/internal/analysis/simplifyrange" @@ -170,11 +169,10 @@ {analyzer: yield.Analyzer}, // uses go/ssa {analyzer: sortslice.Analyzer}, {analyzer: embeddirective.Analyzer}, - {analyzer: scannererr.Analyzer}, // to appear in cmd/vet@go1.28 - {analyzer: sqlrowserr.Analyzer}, // to appear in cmd/vet@go1.28 {analyzer: recursiveiter.Analyzer}, // under evaluation {analyzer: errorsastypeshadow.Analyzer}, // under evaluation {analyzer: writestring.Analyzer}, // under evaluation + {analyzer: ptrtoerror.Analyzer}, // under evaluation // disabled due to high false positives {analyzer: shadow.Analyzer, severity: protocol.SeverityHint, nonDefault: true}, // very noisy
diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index dea15f9..ba62211 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go
@@ -116,7 +116,8 @@ RefactorExtractToNewFile protocol.CodeActionKind = "refactor.extract.toNewFile" // refactor.move - RefactorMoveType protocol.CodeActionKind = "refactor.move.moveType" + RefactorMoveType protocol.CodeActionKind = "refactor.move.moveType" + RefactorMoveDeclaration protocol.CodeActionKind = "refactor.move.moveDeclaration" // Note: add new kinds to: // - the SupportedCodeActions map in default.go
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index fa9e44f..0086d5f 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go
@@ -73,6 +73,7 @@ RefactorExtractVariableAll: true, RefactorExtractToNewFile: true, RefactorMoveType: true, // gated by MoveType setting, which is off by default + RefactorMoveDeclaration: true, // gated by MoveDeclaration setting, which is off by default // Not GoTest: it must be explicit in CodeActionParams.Context.Only }, file.Mod: {
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 19bf723..987223a 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go
@@ -274,6 +274,10 @@ // MoveType enables producing Move Type codeactions. The implementation // is unfinished so we use this setting to gate its use. MoveType bool `status:"experimental"` + + // MoveDeclaration enables producing Move Declaration codeactions. The implementation + // is unfinished so we use this setting to gate its use. + MoveDeclaration bool `status:"experimental"` } // A CodeLensSource identifies an (algorithmic) source of code lenses. @@ -636,10 +640,18 @@ // implicitly ignored. // // To suppress the hint, write an actual comment containing - // "ignore error" following the call statement, or explicitly - // assign the result to a blank variable. A handful of common - // functions such as `fmt.Println` are excluded from the - // check. + // one of the following strings: + // ``` + // ignore error + // discard error + // can't fail + // cannot fail + // ``` + // following the call statement, or explicitly assign the + // result to a blank variable. + // + // A handful of common functions such as `fmt.Println` are + // excluded from the check. IgnoredError InlayHint = "ignoredError" ) @@ -1470,6 +1482,9 @@ case "moveType": return setBool(&o.MoveType, value) + case "moveDeclaration": + return setBool(&o.MoveDeclaration, value) + // deprecated and renamed settings // // These should never be deleted: there is essentially no cost @@ -1561,18 +1576,30 @@ // EnabledSemanticTokenModifiers returns a map of modifiers to boolean. func (o *Options) EnabledSemanticTokenModifiers() map[semtok.Modifier]bool { - copy := make(map[semtok.Modifier]bool, len(o.SemanticTokenModifiers)) - for k, v := range o.SemanticTokenModifiers { - copy[semtok.Modifier(k)] = v + copy := make(map[semtok.Modifier]bool, len(o.SemanticMods)) + // Enable the modifiers defined in client capabilities + for _, m := range o.SemanticMods { + if enabled, found := o.SemanticTokenModifiers[m]; found && !enabled { + // If the client capabilities enables a semantic mod, but the + // user's UI settings disables it, keep it disabled. + continue + } + copy[semtok.Modifier(m)] = true } return copy } // EnabledSemanticTokenTypes returns a map of types to boolean. func (o *Options) EnabledSemanticTokenTypes() map[semtok.Type]bool { - copy := make(map[semtok.Type]bool, len(o.SemanticTokenTypes)) - for k, v := range o.SemanticTokenTypes { - copy[semtok.Type(k)] = v + copy := make(map[semtok.Type]bool, len(o.SemanticTypes)) + // Enable the tokens defined in client capabilities + for _, t := range o.SemanticTypes { + if enabled, found := o.SemanticTokenTypes[t]; found && !enabled { + // If the client capabilities enables a semantic type, but the + // user's UI settings disables it, keep it disabled. + continue + } + copy[semtok.Type(t)] = true } if o.NoSemanticString { copy[semtok.TokString] = false
diff --git a/gopls/internal/template/implementations.go b/gopls/internal/template/implementations.go index 1a01979..e82f80b 100644 --- a/gopls/internal/template/implementations.go +++ b/gopls/internal/template/implementations.go
@@ -224,7 +224,7 @@ }) } return &protocol.SemanticTokens{ - Data: semtok.Encode(items, nil, nil), + Data: semtok.Encode(items, snapshot.Options().EnabledSemanticTokenTypes(), snapshot.Options().EnabledSemanticTokenModifiers()), // for small cache, some day. for now, the LSP client ignores this // (that is, when the LSP client starts returning these, we can cache) ResultID: fmt.Sprintf("%v", time.Now()),
diff --git a/gopls/internal/test/integration/bench/bench_test.go b/gopls/internal/test/integration/bench/bench_test.go index f42cf47..4286f48 100644 --- a/gopls/internal/test/integration/bench/bench_test.go +++ b/gopls/internal/test/integration/bench/bench_test.go
@@ -24,7 +24,6 @@ "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/test/integration" "golang.org/x/tools/gopls/internal/test/integration/fake" - "golang.org/x/tools/gopls/internal/tool" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/fakenet" "golang.org/x/tools/internal/event" @@ -57,7 +56,7 @@ func TestMain(m *testing.M) { bug.PanicOnBugs = true if os.Getenv(runAsGopls) == "true" { - tool.Main(context.Background(), cmd.New(), os.Args[1:]) + cmd.Main() os.Exit(0) } event.SetExporter(nil) // don't log to stderr
diff --git a/gopls/internal/test/integration/completion/completion_test.go b/gopls/internal/test/integration/completion/completion_test.go index 9a2e591..045b859 100644 --- a/gopls/internal/test/integration/completion/completion_test.go +++ b/gopls/internal/test/integration/completion/completion_test.go
@@ -377,6 +377,12 @@ env.RunGoCommand("mod", "tidy") env.Await(env.DoneWithChangeWatchedFiles()) + // assure that the module cache index exists + // (rather than hoping the background goroutine finishes) + if _, err := modindex.Update(filepath.Join(env.Sandbox.GOPATH(), "pkg", "mod")); err != nil { + t.Fatal(err) + } + // Trigger unimported completions for the example.com/blah package. env.OpenFile("main.go") env.Await(env.DoneWithOpen()) @@ -1446,20 +1452,8 @@ package main var _ = blah. ` - modcache := t.TempDir() - defer CleanModCache(t, modcache) - mx := fake.UnpackTxt(cache) - for k, v := range mx { - fname := filepath.Join(modcache, k) - dir := filepath.Dir(fname) - os.MkdirAll(dir, 0777) // ignore error - if err := os.WriteFile(fname, v, 0644); err != nil { - t.Fatal(err) - } - } - WithOptions( - EnvVars{"GOMODCACHE": modcache}, + CacheFiles(cache), WriteGoSum("."), NoLogsOnError(), ).Run(t, files, func(t *testing.T, env *Env) { @@ -1638,3 +1632,90 @@ } }) } + +// ensure that completion converts the array to a slice +func TestIssue80268(t *testing.T) { + const src = ` +-- go.mod -- +module mod.com +go 1.22 +-- main.go -- +package example + +func foo(p *[10]int) {} + +func Bar() { + var array [10]int + foo(arr) +} +` + Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + env.Await(env.DoneWithOpen()) + loc := env.RegexpSearch("main.go", `foo\(arr()\)`) + completions := env.Completion(loc) + if len(completions.Items) == 0 { + t.Fatal("no completions found") + } + env.AcceptCompletion(loc, completions.Items[0]) + env.Await(env.DoneWithChange()) + + fmt.Printf("New main.go content:\n%s\n", env.BufferText("main.go")) + + var diags protocol.PublishDiagnosticsParams + env.Await(ReadDiagnostics("main.go", &diags)) + if len(diags.Diagnostics) != 0 { + t.Errorf("unexpected diagnostics:\n") + for _, d := range diags.Diagnostics { + t.Errorf("Diagnostic: %d:%d: %s\n", d.Range.Start.Line, d.Range.Start.Character, d.Message) + } + for _, item := range completions.Items { + t.Errorf("Completion: %s (Kind: %v, Detail: %q)\n", item.Label, item.Kind, item.Detail) + } + } + }) +} + +// Check that the completion code no longer panics. +func TestIssue75192(t *testing.T) { + const src = ` +-- go.mod -- +module mod.com +go 1.22 +-- main.go -- +package main + +func main() { + _ = notypeinfo.(type) +} +` + Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + env.Await(env.DoneWithOpen()) + loc := env.RegexpSearch("main.go", `notypeinfo\.\(ty()pe\)`) + // this used to panic. + env.Completion(loc) + }) +} + +func TestIssue74564(t *testing.T) { + const src = ` +-- go.mod -- +module mod.com +go 1.22 +-- main.go -- +package main + +func f[T ~[]int](x T) { + var s string + _ = append(x, append(x, x...)...) +} +` + Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + env.Await(env.DoneWithOpen()) + // Place cursor inside the first 'x' of the inner append call. + loc := env.RegexpSearch("main.go", `append\(x, append\(()x,`) + env.Completion(loc) + }) +}
diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index 03f9b1e..0159380 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go
@@ -384,7 +384,8 @@ "declaration", "definition", "readonly", "static", "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", // Additional modifiers supported by this client: - "interface", "struct", "signature", "pointer", "array", "map", "slice", "chan", "string", "number", "bool", "invalid", + "interface", "struct", "signature", "pointer", "array", "map", "slice", + "chan", "string", "shadowing", "number", "bool", "invalid", "format", } // Request that the server provide its complete list of code action kinds. capabilities.TextDocument.CodeAction = protocol.CodeActionClientCapabilities{
diff --git a/gopls/internal/test/integration/fake/sandbox.go b/gopls/internal/test/integration/fake/sandbox.go index 12ce516..3446edb 100644 --- a/gopls/internal/test/integration/fake/sandbox.go +++ b/gopls/internal/test/integration/fake/sandbox.go
@@ -13,6 +13,7 @@ "strings" "golang.org/x/tools/internal/gocommand" + "golang.org/x/tools/internal/modindex" "golang.org/x/tools/internal/robustio" "golang.org/x/tools/txtar" ) @@ -54,6 +55,9 @@ // ProxyFiles holds a txtar-encoded archive of files to populate a file-based // Go proxy. ProxyFiles map[string][]byte + // CacheFiles holds a txtar-encoded archive of files to populate the sandbox's + // module cache. + CacheFiles map[string][]byte // GOPROXY is the explicit GOPROXY value that should be used for the sandbox. // // This option is incompatible with ProxyFiles. @@ -112,6 +116,25 @@ return nil, err } } + if len(config.CacheFiles) > 0 { + modcache := filepath.Join(sb.gopath, "pkg", "mod") + if err := os.MkdirAll(modcache, 0755); err != nil { + return nil, err + } + for name, content := range config.CacheFiles { + fname := filepath.Join(modcache, name) + dir := filepath.Dir(fname) + if err := os.MkdirAll(dir, 0755); err != nil { + return nil, err + } + if err := os.WriteFile(fname, content, 0644); err != nil { + return nil, err + } + } + if _, err := modindex.Update(modcache); err != nil { + return nil, fmt.Errorf("failed to create modindex: %w", err) + } + } // Short-circuit writing the workdir if we're given an absolute path, since // this is used for running in an existing directory. // TODO(findleyr): refactor this to be less of a workaround.
diff --git a/gopls/internal/test/integration/misc/imports_test.go b/gopls/internal/test/integration/misc/imports_test.go index 409fb18..52fde22 100644 --- a/gopls/internal/test/integration/misc/imports_test.go +++ b/gopls/internal/test/integration/misc/imports_test.go
@@ -300,27 +300,8 @@ return nil } ` - modcache := t.TempDir() - defer CleanModCache(t, modcache) - mx := fake.UnpackTxt(cache) - - for k, v := range mx { - fname := filepath.Join(modcache, k) - dir := filepath.Dir(fname) - os.MkdirAll(dir, 0777) // ignore error - if err := os.WriteFile(fname, v, 0644); err != nil { - t.Fatal(err) - } - if true { - // for diagnosing flakiness - t.Logf("wrote %s:%d", fname, len(v)) - } - } - // golang/go#77552 finds this test flaky, so create the index explicitly - // rather than hoping a background go routine finishes in time - modindex.Update(modcache) WithOptions( - EnvVars{"GOMODCACHE": modcache}, + CacheFiles(cache), WriteGoSum("."), NoLogsOnError(), ).Run(t, files, func(t *testing.T, env *Env) { @@ -358,26 +339,8 @@ return nil } ` - modcache := t.TempDir() - defer CleanModCache(t, modcache) - mx := fake.UnpackTxt(cache) - for k, v := range mx { - fname := filepath.Join(modcache, k) - dir := filepath.Dir(fname) - os.MkdirAll(dir, 0777) // ignore error - if err := os.WriteFile(fname, v, 0644); err != nil { - t.Fatal(err) - } - } - // create the index deterministically. When gopls is invoked - // interactively, the module index is created or updated in - // a goroutine, so we can't rely on it being present - // immediately. - if _, err := modindex.Update(modcache); err != nil { - t.Fatal(err) - } WithOptions( - EnvVars{"GOMODCACHE": modcache}, + CacheFiles(cache), WriteGoSum("."), NoLogsOnError(), ).Run(t, files, func(t *testing.T, env *Env) {
diff --git a/gopls/internal/test/integration/misc/rename_test.go b/gopls/internal/test/integration/misc/rename_test.go index 6afa183..2f13534 100644 --- a/gopls/internal/test/integration/misc/rename_test.go +++ b/gopls/internal/test/integration/misc/rename_test.go
@@ -1024,3 +1024,67 @@ } } } + +// TestRenameIssue71657 checks that a panic no longer occurs in Rename +func TestRenameIssue71657(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.18 +-- dep/dep.go -- +package dep + +type U struct { + F nonexistentType +} +-- foo.go -- +package foo + +import "mod.com/dep" + +type I interface { + M() +} + +type T struct{} +func (T) M() {} + +var _ I = T{} + +func _(u dep.U) { + _ = u.F.M +} +` + + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo.go") + // The rename operation should succeed despite type errors in dep, + // and it must not panic. + env.Rename(env.RegexpSearch("foo.go", "M"), "N") + + // Verify that M was renamed to N in the interface and struct, + // but the invalid selection u.F.M was left untouched. + want := `package foo + +import "mod.com/dep" + +type I interface { + N() +} + +type T struct{} +func (T) N() {} + +var _ I = T{} + +func _(u dep.U) { + _ = u.F.M +} +` + got := env.BufferText("foo.go") + if got != want { + t.Errorf("unexpected content of foo.go after rename:\n%s", compare.Text(want, got)) + } + }) +}
diff --git a/gopls/internal/test/integration/misc/semantictokens_test.go b/gopls/internal/test/integration/misc/semantictokens_test.go index 5d09eb1..6069fe4 100644 --- a/gopls/internal/test/integration/misc/semantictokens_test.go +++ b/gopls/internal/test/integration/misc/semantictokens_test.go
@@ -5,6 +5,7 @@ package misc import ( + "encoding/json" "fmt" "strings" "testing" @@ -319,3 +320,206 @@ } }) } + +// This sets client capability fields: +// - `textDocument.semanticTokens.tokenTypes` +// - `textDocument.semanticTokens.tokenModifiers` +func tokenClientCapabilities(t *testing.T, tokenTypes, tokenMods []string) RunOption { + t.Helper() + typeString, err := json.Marshal(tokenTypes) + if err != nil { + t.Fatalf("accepted token list failed to marshal: %v", err) + } + modString, err := json.Marshal(tokenMods) + if err != nil { + t.Fatalf("accepted mod list failed to marshal: %v", err) + } + // This will only update the specific client capabilities set. + return CapabilitiesJSON(fmt.Appendf(nil, + `{"textDocument": {"semanticTokens": {"tokenTypes": %s, "tokenModifiers": %s}}}`, + typeString, modString)) +} + +// This tests the Client Capabilities: +// - `textDocument.semanticTokens.tokenTypes` +// - `textDocument.semanticTokens.tokenModifiers` +// +// These two client capabilities determine the maximum set of types and mods +// the server can send back to the client. So the intersection of these lists +// with the lists of what the server supports is what gopls can return. +func TestSemanticTokenClientCapabilitiesTypeAndModifier(t *testing.T) { + src := ` +-- go.mod -- +module example.com + +go 1.19 +-- main.go -- +package foo + +// Comment +type A struct {} +` + tcs := []struct { + name string + tokenTypes []string + tokenModifiers []string + want []fake.SemanticToken + }{{ + name: "base case", + tokenTypes: []string{"comment", "keyword", "namespace", "type"}, + tokenModifiers: []string{"definition", "struct"}, + want: []fake.SemanticToken{ + {Token: "package", TokenType: "keyword"}, + {Token: "foo", TokenType: "namespace"}, + + {Token: "// Comment", TokenType: "comment"}, + {Token: "type", TokenType: "keyword"}, + {Token: "A", TokenType: "type", Mod: "definition struct"}, + {Token: "struct", TokenType: "keyword"}, + }, + }, { + name: "support subset of token types", + tokenTypes: []string{"comment", "type"}, + tokenModifiers: []string{"definition", "struct"}, + want: []fake.SemanticToken{ + {Token: "// Comment", TokenType: "comment"}, + {Token: "A", TokenType: "type", Mod: "definition struct"}, + }, + }, { + name: "support subset of modifiers", + tokenTypes: []string{"comment", "keyword", "namespace", "type"}, + tokenModifiers: []string{"definition"}, + want: []fake.SemanticToken{ + {Token: "package", TokenType: "keyword"}, + {Token: "foo", TokenType: "namespace"}, + + {Token: "// Comment", TokenType: "comment"}, + {Token: "type", TokenType: "keyword"}, + // "struct" modifier no longer supported + {Token: "A", TokenType: "type", Mod: "definition"}, + {Token: "struct", TokenType: "keyword"}, + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + WithOptions( + Modes(Default), + tokenClientCapabilities(t, tc.tokenTypes, tc.tokenModifiers), + Settings{"semanticTokens": true}, + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + seen := env.SemanticTokensFull("main.go") + if x := cmp.Diff(tc.want, seen); x != "" { + t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) + } + }) + }) + } +} + +// This test verifies that UI Setting's `semanticTokens` map only further +// restricts the client capabilities `tokenTypes` list. +// +// TODO(aputman): Consider moving to marker test. +func TestSemanticTokenTypesClientCapabilitiesWithUISettings(t *testing.T) { + src := ` +-- go.mod -- +module example.com + +go 1.19 +-- main.go -- +package foo + +// Comment +type A struct {} +` + // What the client capability token types are. + clientTokenTypes := []string{"comment", "keyword", "type"} + + // The UI Settings token types map + uiTokenTypes := map[string]bool{ + // Since "namespace" isn't in the clientTokenTypes list, this + // won't do anything. + "namespace": true, + "type": true, + // This will disable the keyword tokentypes even though the client + // supports them. + "keyword": false, + // Since "comment" is left out, it will be supported. + } + + clientTokenModifiers := []string{"definition", "struct"} + + // Only comment and type are left + want := []fake.SemanticToken{ + {Token: "// Comment", TokenType: "comment"}, + {Token: "A", TokenType: "type", Mod: "definition struct"}, + } + WithOptions( + Modes(Default), + tokenClientCapabilities(t, clientTokenTypes, clientTokenModifiers), + Settings{"semanticTokens": true, "semanticTokenTypes": uiTokenTypes}, + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + seen := env.SemanticTokensFull("main.go") + if x := cmp.Diff(want, seen); x != "" { + t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) + } + }) +} + +// This test verifies that UI Setting's semanticTokenModifiers map only +// further restricts the client capabilities tokenModifiers list. +// +// TODO(aputman): Consider moving to marker test. +func TestSemanticTokenModifiersClientCapabilitiesWithUISettings(t *testing.T) { + src := ` +-- go.mod -- +module example.com + +go 1.19 +-- main.go -- +package foo + +type A struct {} + +func B(l []string) {} +` + // Only these token types are returned. + clientTokenTypes := []string{"comment", "definition", "function", "parameter", "type"} + + // What the client capability token modifiers are. + clientTokenModifiers := []string{"definition", "signature", "struct"} + + // The UI Settings token mods map + uiTokenMods := map[string]bool{ + // Since "slice" isn't in the clientTokenModifiers list, this won't do anything. + "slice": true, + "signature": true, + // This will disable the definition modifiers even though the client supports them. + "definition": false, + // Since "struct" is left out, it will be supported. + } + + // Only signature and struct modifiers are left + want := []fake.SemanticToken{ + {Token: "A", TokenType: "type", Mod: "struct"}, // "definition" no longer returned + {Token: "B", TokenType: "function", Mod: "signature"}, // "definition" no longer returned + {Token: "l", TokenType: "parameter", Mod: ""}, // "slice" no longer returned + {Token: "string", TokenType: "type", Mod: ""}, + } + + WithOptions( + Modes(Default), + tokenClientCapabilities(t, clientTokenTypes, clientTokenModifiers), + Settings{"semanticTokens": true, "semanticTokenModifiers": uiTokenMods}, + ).Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("main.go") + seen := env.SemanticTokensFull("main.go") + if x := cmp.Diff(want, seen); x != "" { + t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) + } + }) +}
diff --git a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go index 73c78cd..623cd01 100644 --- a/gopls/internal/test/integration/options.go +++ b/gopls/internal/test/integration/options.go
@@ -58,6 +58,13 @@ }) } +// CacheFiles configures a module cache using the given txtar-encoded string. +func CacheFiles(txt string) RunOption { + return optionSetter(func(opts *runConfig) { + opts.sandbox.CacheFiles = fake.UnpackTxt(txt) + }) +} + // WriteGoSum causes the environment to write a go.sum file for the requested // relative directories (via `go list -mod=mod`), before starting gopls. //
diff --git a/gopls/internal/test/integration/regtest.go b/gopls/internal/test/integration/regtest.go index 25ea415..baea9df 100644 --- a/gopls/internal/test/integration/regtest.go +++ b/gopls/internal/test/integration/regtest.go
@@ -5,7 +5,6 @@ package integration import ( - "context" "flag" "fmt" "os" @@ -18,7 +17,6 @@ "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cmd" - "golang.org/x/tools/gopls/internal/tool" "golang.org/x/tools/gopls/internal/util/memoize" "golang.org/x/tools/internal/drivertest" "golang.org/x/tools/internal/gocommand" @@ -144,7 +142,7 @@ // If this magic environment variable is set, run gopls instead of the test // suite. See the documentation for runTestAsGoplsEnvvar for more details. if os.Getenv(runTestAsGoplsEnvvar) == "true" { - tool.Main(context.Background(), cmd.New(), os.Args[1:]) + cmd.Main() return 0 }
diff --git a/gopls/internal/test/integration/web/pkdoc_test.go b/gopls/internal/test/integration/web/pkdoc_test.go index 0cdeaae..0dce6f7 100644 --- a/gopls/internal/test/integration/web/pkdoc_test.go +++ b/gopls/internal/test/integration/web/pkdoc_test.go
@@ -8,6 +8,7 @@ "fmt" "html" "regexp" + "strconv" "strings" "testing" @@ -60,8 +61,13 @@ // Grab the URL in the HTML source link for NewFunc. // (We don't have a DOM or JS interpreter so we have // to know something of the document internals here.) - rx := regexp.MustCompile(`<h3 id='NewFunc'.*httpGET\("(.*)"\)`) - srcURL := html.UnescapeString(string(rx.FindSubmatch(doc2)[1])) + rx := regexp.MustCompile(`<h3 id='NewFunc'.*httpGET\((.*)\)`) + raw := string(rx.FindSubmatch(doc2)[1]) + jsUnquote := strconv.Unquote // close enough + srcURL, err := jsUnquote(html.UnescapeString(raw)) + if err != nil { + t.Fatalf("can't extract URL from httpGET(%s): %v", raw, err) + } // Fetch the document. Its result isn't important, // but it must have the side effect of another showDocument
diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_anonymous_struct.txt b/gopls/internal/test/marker/testdata/codeaction/extract_anonymous_struct.txt index f606fe2..ebe642b 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_anonymous_struct.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_anonymous_struct.txt
@@ -3,6 +3,7 @@ module mod.com go 1.12 + -- a/a.go -- package a @@ -151,6 +152,7 @@ type B struct { A } + var _ error = B{} // pacify ptrtoerror analyzer a := B{A: fmt.Errorf("test error")} //@codeaction("a", "refactor.extract.function", end=endH, err="the code refers to a local type") return a.Error() //@loc(endH, "Error()") }
diff --git a/gopls/internal/test/marker/testdata/codeaction/extract_variable-toplevel.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable-toplevel.txt index a5a5e58..d0e3fad 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract_variable-toplevel.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable-toplevel.txt
@@ -1,6 +1,9 @@ This test checks the behavior of the 'extract variable/constant' code action at top level (outside any function). See issue #70665. +-- settings.json -- +{"analyses": {"unusedfunc": false}} + -- a.go -- package a
diff --git a/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt b/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt index 5577b5e..950bde1 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extracttofile.txt
@@ -2,6 +2,7 @@ -- flags -- -ignore_extra_diags +-errors_ok -- go.mod -- module golang.org/lsptests/extracttofile @@ -349,3 +350,10 @@ -// docs -const C = "" //@codeaction("const", "refactor.extract.toNewFile", edit=copyrightandbuildconstraint) +//@codeaction("const", "refactor.extract.toNewFile", edit=copyrightandbuildconstraint) + +-- parsererror.go -- +package main + +func foo() { //@codeaction("foo", "refactor.extract.toNewFile", err=re"failed to format extracted source") + for i := struct +
diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt index a92a895..90ac890 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt
@@ -33,7 +33,7 @@ package fillswitch import ( - "golang.org/lsptests/fillswitch/data" + dataalias "golang.org/lsptests/fillswitch/data" ) type typeA int @@ -57,9 +57,9 @@ func (notificationTwo) isNotification() {} func doSwitch() { - var b data.TypeB + var b dataalias.TypeB switch b { - case data.TypeBOne: //@codeaction(":", "refactor.rewrite.fillSwitch", edit=a1) + case dataalias.TypeBOne: //@codeaction(":", "refactor.rewrite.fillSwitch", edit=a1) } var a typeA @@ -82,33 +82,67 @@ case typeAThree: //@codeaction(":", "refactor.rewrite.fillSwitch", edit=a5) } } +-- alias.go -- +package fillswitch + +import format "fmt" + +var _ = format.Sprintf + +func doAliasedFmtSwitch() { + var a typeA + switch a { + case typeAThree: //@codeaction(":", "refactor.rewrite.fillSwitch", edit=a6) + } +} -- @a1/a.go -- -@@ -31 +31,4 @@ -+ case data.TypeBThree: -+ case data.TypeBTwo: +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -31 +33,4 @@ ++ case dataalias.TypeBThree: ++ case dataalias.TypeBTwo: + default: + panic(fmt.Sprintf("unexpected data.TypeB: %#v", b)) -- @a2/a.go -- -@@ -36 +36,4 @@ +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -36 +38,4 @@ + case typeAOne: + case typeATwo: + default: + panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", a)) -- @a3/a.go -- -@@ -40 +40,4 @@ +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -40 +42,4 @@ + case notificationOne: + case notificationTwo: + default: + panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", n)) -- @a4/a.go -- -@@ -43 +43,4 @@ +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -43 +45,4 @@ + case notificationOne: + case notificationTwo: + default: + panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", nt)) -- @a5/a.go -- -@@ -51 +51,4 @@ +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -51 +53,4 @@ + case typeAOne: + case typeATwo: + default: + panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", s.a)) +-- @a6/alias.go -- +@@ -11 +11,4 @@ ++ case typeAOne: ++ case typeATwo: ++ default: ++ panic(format.Sprintf("unexpected fillswitch.typeA: %#v", a))
diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt index 39a7eae..c9e856c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt
@@ -24,7 +24,7 @@ package fillswitch import ( - "golang.org/lsptests/fillswitch/data" + dataalias "golang.org/lsptests/fillswitch/data" ) type typeA int @@ -48,9 +48,9 @@ func (notificationTwo) isNotification() {} func doSwitch() { - var b data.TypeB + var b dataalias.TypeB switch b { - case data.TypeBOne: //@codeaction(":", "refactor.rewrite.fillSwitch", edit=a1) + case dataalias.TypeBOne: //@codeaction(":", "refactor.rewrite.fillSwitch", edit=a1) } var a typeA @@ -74,31 +74,46 @@ } } -- @a1/a.go -- -@@ -31 +31,4 @@ -+ case data.TypeBThree: -+ case data.TypeBTwo: +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -31 +33,4 @@ ++ case dataalias.TypeBThree: ++ case dataalias.TypeBTwo: + default: + panic(fmt.Sprintf("unexpected data.TypeB: %#v", b)) -- @a2/a.go -- -@@ -36 +36,4 @@ +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -36 +38,4 @@ + case typeAOne: + case typeATwo: + default: + panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", a)) -- @a3/a.go -- -@@ -40 +40,4 @@ +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -40 +42,4 @@ + case notificationOne: + case notificationTwo: + default: + panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", n)) -- @a4/a.go -- -@@ -43 +43,4 @@ +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -43 +45,4 @@ + case notificationOne: + case notificationTwo: + default: + panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", nt)) -- @a5/a.go -- -@@ -51 +51,4 @@ +@@ -4 +4,2 @@ ++ "fmt" ++ +@@ -51 +53,4 @@ + case typeAOne: + case typeATwo: + default:
diff --git a/gopls/internal/test/marker/testdata/completion/aliasedfunclit.txt b/gopls/internal/test/marker/testdata/completion/aliasedfunclit.txt new file mode 100644 index 0000000..f914f6f --- /dev/null +++ b/gopls/internal/test/marker/testdata/completion/aliasedfunclit.txt
@@ -0,0 +1,27 @@ +This test checks that function-literal completion works the same for +a named func type and an aliased func type (golang/go#80357). + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module mod.test + +go 1.24 + +-- a.go -- +package a + +type NamedFunc func(i int) bool +type AliasedFunc = func(i int) bool + +func WithNamedFunc(fn NamedFunc) {} +func WithAliasedFunc(fn AliasedFunc) {} + +func _() { + WithNamedFunc(fu) //@snippet(re"()\\)", litFunc, "func(i int) bool {$0\\}") + + WithAliasedFunc(fu) //@snippet(re"()\\)", litFunc, "func(i int) bool {$0\\}") + + //@item(litFunc, "func(...) {}", "", "var") +} \ No newline at end of file
diff --git a/gopls/internal/test/marker/testdata/definition/asm.txt b/gopls/internal/test/marker/testdata/definition/asm.txt index c8fec91..9a45d84 100644 --- a/gopls/internal/test/marker/testdata/definition/asm.txt +++ b/gopls/internal/test/marker/testdata/definition/asm.txt
@@ -28,9 +28,26 @@ JMP ·ff //@ def("ff", ffgo) JMP label //@ def("label", label) label: //@ loc(label,"label") + CALL other·bar(SB) //@ def("bar") // not a dependency: no definition RET -- b/b.go -- package b func B() {} //@ loc(bB, "B") + +-- a/asm2.s -- +// Labels are function-scoped: the same label name in different TEXT +// functions denotes different labels. + +TEXT ·f1(SB), $0-0 +onlyf1: +loop: //@ loc(f1loop, "loop") + JMP loop //@ def("loop", f1loop) + RET + +TEXT ·f2(SB), $0-0 +loop: //@ loc(f2loop, "loop") + JMP loop //@ def("loop", f2loop) + JMP onlyf1 //@ def("onlyf1") // defined only in f1: no definition here + RET
diff --git a/gopls/internal/test/marker/testdata/diagnostics/stdversion.txt b/gopls/internal/test/marker/testdata/diagnostics/stdversion.txt index c6a19a7..55faa09 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/stdversion.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/stdversion.txt
@@ -24,10 +24,10 @@ var _ types.Info // ok: defined by go1.0 // new field of older type - _ = new(types.Info).FileVersions //@diag("FileVersions", re`types.FileVersions requires go1.22 or later \(module is go1.21\)`) + _ = new(types.Info).FileVersions //@diag("FileVersions", re`types.Info.FileVersions requires go1.22 or later \(module is go1.21\)`) // new method of older type - _ = new(types.Info).PkgNameOf //@diag("PkgNameOf", re`types.PkgNameOf requires go1.22 or later \(module is go1.21\)`) + _ = new(types.Info).PkgNameOf //@diag("PkgNameOf", re`types.\(\*Info\).PkgNameOf requires go1.22 or later \(module is go1.21\)`) // new package-level type var a types.Alias //@diag("Alias", re`types.Alias requires go1.22 or later \(module is go1.21\)`) @@ -51,10 +51,10 @@ var _ types.Info // ok: defined by go1.0 // new field of older type - _ = new(types.Info).FileVersions //@diag("FileVersions", re`types.FileVersions requires go1.22 or later \(module is go1.21\)`) + _ = new(types.Info).FileVersions //@diag("FileVersions", re`types.Info.FileVersions requires go1.22 or later \(module is go1.21\)`) // new method of older type - _ = new(types.Info).PkgNameOf //@diag("PkgNameOf", re`types.PkgNameOf requires go1.22 or later \(module is go1.21\)`) + _ = new(types.Info).PkgNameOf //@diag("PkgNameOf", re`types.\(\*Info\).PkgNameOf requires go1.22 or later \(module is go1.21\)`) // new package-level type var a types.Alias //@diag("Alias", re`types.Alias requires go1.22 or later \(module is go1.21\)`)
diff --git a/gopls/internal/test/marker/testdata/diagnostics/stdversion_jsonv2.txt b/gopls/internal/test/marker/testdata/diagnostics/stdversion_jsonv2.txt new file mode 100644 index 0000000..0e74a98 --- /dev/null +++ b/gopls/internal/test/marker/testdata/diagnostics/stdversion_jsonv2.txt
@@ -0,0 +1,24 @@ +This test verifies that "too new" diagnostics from the stdversion analyzer +are suppressed for jsonv2 when the go version is go1.25 or later. +See golang/go#80691 + +-- env -- +GOEXPERIMENT=jsonv2 +-- flags -- +-min_go_command=go1.25 +-max_go_command=go1.26 + +-- go.mod -- +module example.com + +go 1.25 + +-- a/a.go -- +//go:build goexperiment.jsonv2 || go1.27 +package a + +import "encoding/json/v2" + +func _() { + _ = json.Marshal // no diagnostic, see golang/go#80691 +}
diff --git a/gopls/internal/test/marker/testdata/highlight/asm.txt b/gopls/internal/test/marker/testdata/highlight/asm.txt new file mode 100644 index 0000000..ffb3aca --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/asm.txt
@@ -0,0 +1,147 @@ +Test of documentHighlight for assembly files. + +-- example/highlight_text.s -- +TEXT ·foo(SB), $0-0 //@hiloc(defFoo, "·foo", write) + CALL ·foo(SB) //@hiloc(refFoo, "·foo", read) +//@highlightall(defFoo, refFoo) + +-- example/highlight_global.s -- +GLOBL ·bar(SB), $8 //@hiloc(defBar, "·bar", write) + +TEXT ·useBar(SB), $0-0 + MOVQ ·bar(SB), AX //@hiloc(refBar, "·bar", read) +//@highlightall(defBar, refBar) + +-- example/highlight_label.s -- +TEXT ·loopDemo(SB), $0-0 +loop: //@hiloc(defLoop, "loop", write) + JMP loop //@hiloc(refLoop, "loop", read) +//@highlightall(defLoop, refLoop) + +-- example/highlight_register_amd64.s -- +TEXT ·useReg(SB), $0-0 + MOVQ AX, BX //@hiloc(ax1, "AX", read) + ADDQ CX, AX //@hiloc(ax2, "AX", write) +//@highlightall(ax1, ax2) + +-- example/highlight_register_scope_amd64.s -- +TEXT ·f1(SB), $0-0 + MOVQ AX, BX //@hiloc(axF1, "AX", read) + +TEXT ·f2(SB), $0-0 + MOVQ AX, CX +//@highlightall(axF1) + +-- example/highlight_compare_amd64.s -- +TEXT ·cmpDemo(SB), $0-0 + CMPQ AX, BX //@hiloc(cmpAX, "AX", read) +//@highlightall(cmpAX) + +-- example/highlight_label_scope.s -- +TEXT ·f1(SB), $0-0 +loop: //@hiloc(f1def, "loop", write) + JMP loop //@hiloc(f1ref, "loop", read) +//@highlightall(f1def, f1ref) + +TEXT ·f2(SB), $0-0 +loop: //@hiloc(f2def, "loop", write) + JMP loop //@hiloc(f2ref, "loop", read) +//@highlightall(f2def, f2ref) + +-- example/highlight_mem_operand_amd64.s -- +TEXT ·storeLoad(SB), $0-0 + MOVQ AX, (BX) //@hiloc(bxStore, "BX", read) + MOVQ (AX), BX //@hiloc(bxLoad, "BX", write) +//@highlightall(bxStore, bxLoad) + +-- example/highlight_stack_amd64.s -- +TEXT ·stack(SB), $0-0 + PUSHQ AX //@hiloc(axPush, "AX", read) + POPQ AX //@hiloc(axPop, "AX", write) +//@highlightall(axPush, axPop) + +-- example/highlight_register_386.s -- +TEXT ·reg386(SB), $0-0 + POPL AX //@hiloc(axPop386, "AX", write) + CMPL CX, AX //@hiloc(axCmp386, "AX", read) +//@highlightall(axPop386, axCmp386) + +-- example/highlight_immediate_amd64.s -- +TEXT ·imm(SB), $0-0 + MOVQ $123, AX //@hiloc(imm, "123", read) +//@highlight(imm) + +-- example/highlight_comment_amd64.s -- +TEXT ·c(SB), $0-0 + MOVQ AX, BX + // ADDQ AX, CX //@hiloc(commentAX, "AX", read) +//@highlight(commentAX) + +-- example/highlight_mnemonic_amd64.s -- +TEXT ·mn(SB), $0-0 + MOVQ AX, BX //@hiloc(mnemonic, "MOVQ", read) +//@highlight(mnemonic) + +-- example/highlight_bt_amd64.s -- +TEXT ·btDemo(SB), $0-0 + BTQ $3, AX //@hiloc(btAX, "AX", read) +//@highlightall(btAX) + +-- example/highlight_setcc_amd64.s -- +TEXT ·setcc(SB), $0-0 + SETEQ AL //@hiloc(alSet, "AL", write) +//@highlightall(alSet) + +-- example/highlight_bswap_amd64.s -- +TEXT ·bswap(SB), $0-0 + BSWAPL AX //@hiloc(axBswap, "AX", write) +//@highlightall(axBswap) + +-- example/highlight_cmpxchg_amd64.s -- +TEXT ·xchg(SB), $0-0 + CMPXCHGQ AX, BX //@hiloc(bxXchg, "BX", write) +//@highlightall(bxXchg) + +-- example/highlight_bare_text_amd64.s -- +TEXT ·f(SB), $0-0 + MOVQ AX, BX //@hiloc(axBare1, "AX", read) +TEXT + MOVQ AX, CX //@hiloc(axBare2, "AX", read) +//@highlightall(axBare1, axBare2) + +-- example/highlight_register_noarch.s -- +TEXT ·noArch(SB), $0-0 + MOVQ AX, BX //@hiloc(axNoArch, "AX", read) +//@highlight(axNoArch) + +-- example/highlight_arm64.s -- +TEXT ·armDemo(SB), $0-0 + MOVD R1, R0 //@hiloc(r0mov, "R0", write) + CMP R0, R1 //@hiloc(r0cmp, "R0", read) + TST $1, R0 //@hiloc(r0tst, "R0", read) +//@highlightall(r0mov, r0cmp, r0tst) + +-- example/highlight_regpair_arm64.s -- +TEXT ·pair(SB), $0-0 + LDP (0*8)(R0), (R4, R5) //@hiloc(r4load, "R4", write) + STP (R4, R5), (0*8)(R0) //@hiloc(r4store, "R4", read) +//@highlightall(r4load, r4store) + +-- example/highlight_macro_arm64.s -- +#define QR(a, b, c, d) VADD a, b, c +TEXT ·macro(SB), $0-0 + QR(V0, V4, V8, V12) //@hiloc(v12, "V12", read) +//@highlightall(v12) + +-- example/highlight_macro_amd64.s -- +#define QR(a, b, c, d) PADDL a, b +TEXT ·macro(SB), $0-0 + QR(X0, X4, X8, X12) //@hiloc(x12, "X12", read) +//@highlightall(x12) + +-- example/highlight_xmm_amd64.s -- +TEXT ·xmm(SB), $0-0 + MOVOU X0, X1 //@hiloc(x1mov, "X1", write) + PXOR X1, X0 //@hiloc(x1xor, "X1", read) +//@highlightall(x1mov, x1xor) +
diff --git a/gopls/internal/test/marker/testdata/hover/asm.txt b/gopls/internal/test/marker/testdata/hover/asm.txt new file mode 100644 index 0000000..ea718c0 --- /dev/null +++ b/gopls/internal/test/marker/testdata/hover/asm.txt
@@ -0,0 +1,81 @@ +Test of hover for assembly files. + +Hovering over an assembly symbol that has a Go declaration reports the +declaration's signature (with packages qualified by name, not path) and +its doc comment, rendered as Markdown. Labels and asm-only symbols have +no Go declaration and thus no hover. + +-- go.mod -- +module example.com +go 1.18 + +-- a/a.go -- +package a + +import "example.com/b" + +// foo is the best function. +// It returns its input unchanged. +// +// # Details +// +// There is nothing more to say. +func foo(int) int + +// x is a global variable. +var x int + +// g returns a new T. +func g() *b.T + +var _, _, _ = foo, x, g // pacify unused{func,var} analyzers + +-- a/asm.s -- +// portable assembly + +TEXT ·foo(SB), $0-8 //@hover("·foo", "·foo", foo) + MOVQ ·x(SB), R0 //@hover("·x", "·x", x) + CALL ·foo(SB) //@hover("·foo", "·foo", foo) + CALL ·g(SB) //@hover("·g", "·g", g) + CALL example·com∕b·B(SB) //@hover("B", "example·com∕b·B", bB) +loop: + JMP loop + RET + +-- b/b.go -- +package b + +// B is a helper function. +func B() {} + +// T is a helper type. +type T struct{} + +-- @foo -- +```go +func foo(int) int +``` + +foo is the best function. It returns its input unchanged. + +### Details + +There is nothing more to say. +-- @x -- +```go +var x int +``` + +x is a global variable. +-- @g -- +```go +func g() *b.T +``` + +g returns a new T. +-- @bB -- +```go +func B() +``` + +B is a helper function.
diff --git a/gopls/internal/test/marker/testdata/implementation/basic.txt b/gopls/internal/test/marker/testdata/implementation/basic.txt index be3444f..056cdf6 100644 --- a/gopls/internal/test/marker/testdata/implementation/basic.txt +++ b/gopls/internal/test/marker/testdata/implementation/basic.txt
@@ -47,6 +47,8 @@ type MyError struct {} //@implementation("MyError", StdError) +var _ error = MyError{} // pacify ptrtoerror analyzer + func (MyError) Error() string { return "bah" } -- other/other.go --
diff --git a/gopls/internal/test/marker/testdata/implementation/issue43655.txt b/gopls/internal/test/marker/testdata/implementation/issue43655.txt index 3913e3c..6e02bf8 100644 --- a/gopls/internal/test/marker/testdata/implementation/issue43655.txt +++ b/gopls/internal/test/marker/testdata/implementation/issue43655.txt
@@ -1,4 +1,4 @@ -This test verifies that we fine implementations of the built-in error interface. +This test verifies that we find implementations of the built-in error interface. -- go.mod -- module example.com @@ -8,9 +8,11 @@ package p type errA struct{ error } //@loc(errA, "errA") +var _ error = errA{} // pacify ptrtoerror analyzer type errB struct{} //@loc(errB, "errB") func (errB) Error() string{ return "" } //@loc(errBError, "Error") +var _ error = errB{} // pacify ptrtoerror analyzer type notAnError struct{} func (notAnError) Error() int { return 0 }
diff --git a/gopls/internal/test/marker/testdata/inlayhints/ignored-error.txt b/gopls/internal/test/marker/testdata/inlayhints/ignored-error.txt index 6658691..dd68d32 100644 --- a/gopls/internal/test/marker/testdata/inlayhints/ignored-error.txt +++ b/gopls/internal/test/marker/testdata/inlayhints/ignored-error.txt
@@ -36,7 +36,7 @@ func _(f *os.File) { // Allow horizontal space before comment. new(os.File).Close() // ignore error - f.Close() // ignore error + f.Close() // can't fail } -- @out -- @@ -68,6 +68,6 @@ func _(f *os.File) { // Allow horizontal space before comment. new(os.File).Close() // ignore error - f.Close() // ignore error + f.Close() // can't fail }
diff --git a/gopls/internal/test/marker/testdata/mcptools/references.txt b/gopls/internal/test/marker/testdata/mcptools/references.txt index 0172076..455e59b 100644 --- a/gopls/internal/test/marker/testdata/mcptools/references.txt +++ b/gopls/internal/test/marker/testdata/mcptools/references.txt
@@ -38,13 +38,13 @@ The object has 3 references. Their locations are listed below Reference 1 Located in the file: $WORKDIR/a/a.go -The reference is located on line 2, which has content `func Foo() {} //@loc(Foo, "Foo")` +The reference is located on line 3, which has content `func Foo() {} //@loc(Foo, "Foo")` Reference 2 Located in the file: $WORKDIR/a/a.go -The reference is located on line 5, which has content `Foo()` +The reference is located on line 6, which has content `Foo()` Reference 3 Located in the file: $WORKDIR/b/b.go -The reference is located on line 5, which has content `a.Foo()` +The reference is located on line 6, which has content `a.Foo()`
diff --git a/gopls/internal/test/marker/testdata/mcptools/symbol_references.txt b/gopls/internal/test/marker/testdata/mcptools/symbol_references.txt index b08a0a2..fdfc08b 100644 --- a/gopls/internal/test/marker/testdata/mcptools/symbol_references.txt +++ b/gopls/internal/test/marker/testdata/mcptools/symbol_references.txt
@@ -40,19 +40,19 @@ The object has 3 references. Their locations are listed below Reference 1 Located in the file: $WORKDIR/a/a.go -The reference is located on line 2, which has content `func Foo() {}` +The reference is located on line 3, which has content `func Foo() {}` Reference 2 Located in the file: $WORKDIR/a/a.go -The reference is located on line 5, which has content `Foo()` +The reference is located on line 6, which has content `Foo()` Reference 3 Located in the file: $WORKDIR/b/b.go -The reference is located on line 5, which has content `a.Foo()` +The reference is located on line 6, which has content `a.Foo()` -- @aBar -- The object has 1 references. Their locations are listed below Reference 1 Located in the file: $WORKDIR/a/a.go -The reference is located on line 10, which has content `func (T) Bar() {}` +The reference is located on line 11, which has content `func (T) Bar() {}`
diff --git a/gopls/internal/test/marker/testdata/rename/issue80542.txt b/gopls/internal/test/marker/testdata/rename/issue80542.txt new file mode 100644 index 0000000..ddf7ac5 --- /dev/null +++ b/gopls/internal/test/marker/testdata/rename/issue80542.txt
@@ -0,0 +1,63 @@ +This test verifies that a rename of a field selected from an instantiated +generic type updates the field declaration, including across packages. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module example.com + +go 1.18 + +-- a.go -- +package a + +type box[T any] struct { + value T +} + +func (b box[T]) use() { + _ = b.value //@rename("value", "renamed", valueToRenamed) +} + +var _ = box[int]{value: 1} + +-- lib/lib.go -- +package lib + +type Box[T any] struct { + Value T +} + +-- use/use.go -- +package use + +import "example.com/lib" + +func use(b lib.Box[int]) { + _ = b.Value //@rename("Value", "Renamed", crossPackage) +} + +var _ = lib.Box[int]{Value: 1} + +-- @valueToRenamed/a.go -- +@@ -4 +4 @@ +- value T ++ renamed T +@@ -8 +8 @@ +- _ = b.value //@rename("value", "renamed", valueToRenamed) ++ _ = b.renamed //@rename("value", "renamed", valueToRenamed) +@@ -11 +11 @@ +-var _ = box[int]{value: 1} ++var _ = box[int]{renamed: 1} +-- @crossPackage/lib/lib.go -- +@@ -4 +4 @@ +- Value T ++ Renamed T +-- @crossPackage/use/use.go -- +@@ -6 +6 @@ +- _ = b.Value //@rename("Value", "Renamed", crossPackage) ++ _ = b.Renamed //@rename("Value", "Renamed", crossPackage) +@@ -9 +9 @@ +-var _ = lib.Box[int]{Value: 1} ++var _ = lib.Box[int]{Renamed: 1}
diff --git a/gopls/internal/test/marker/testdata/token/format.txt b/gopls/internal/test/marker/testdata/token/format.txt index c577cc6..6acd1c9 100644 --- a/gopls/internal/test/marker/testdata/token/format.txt +++ b/gopls/internal/test/marker/testdata/token/format.txt
@@ -5,6 +5,15 @@ "semanticTokens": true } +-- capabilities.json -- +{ + "textDocument": { + "semanticTokens": { + "tokenModifiers": ["format"] + } + } + +} -- flags -- -ignore_extra_diags
diff --git a/gopls/internal/test/marker/testdata/token/shadowing.txt b/gopls/internal/test/marker/testdata/token/shadowing.txt index b6c3d18..546035c 100644 --- a/gopls/internal/test/marker/testdata/token/shadowing.txt +++ b/gopls/internal/test/marker/testdata/token/shadowing.txt
@@ -1,6 +1,17 @@ +This test tests the token modifier "shadowing", which requires adding it to the client capabilities + -- settings.json -- { - "semanticTokens": true + "semanticTokens": true, + "analyses": {"unusedfunc": false} +} +-- capabilities.json -- +{ + "textDocument": { + "semanticTokens": { + "tokenModifiers": ["definition", "number", "pointer", "readonly", "shadowing", "signature", "struct"] + } + } } -- universe/universe.go -- package p
diff --git a/gopls/internal/tool/tool.go b/gopls/internal/tool/tool.go deleted file mode 100644 index 70a6291..0000000 --- a/gopls/internal/tool/tool.go +++ /dev/null
@@ -1,297 +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. - -// Package tool is a harness for writing Go tools. -package tool - -import ( - "context" - "flag" - "fmt" - "log" - "os" - "reflect" - "runtime" - "runtime/pprof" - "runtime/trace" - "strings" - "time" -) - -// This file is a harness for writing your main function. -// -// It adds a method to the Application type -// Main(name, usage string, args []string) -// which should normally be invoked from a true main as follows: -// func main() { -// (&Application{}).Main("myapp", "non-flag-command-line-arg-help", os.Args[1:]) -// } -// It recursively scans the application object for fields with a tag containing -// `flag:"flagnames" help:"short help text"` -// uses all those fields to build command line flags. It will split flagnames on -// commas and add a flag per name. -// It expects the Application type to have a method -// Run(context.Context, args...string) error -// which it invokes only after all command line flag processing has been finished. -// If Run returns an error, the error will be printed to stderr and the -// application will quit with a non zero exit status. - -// Profile can be embedded in your application struct to automatically -// add command line arguments and handling for the common profiling methods. -type Profile struct { - CPU string `flag:"profile.cpu" help:"write CPU profile to this file"` - Memory string `flag:"profile.mem" help:"write memory profile to this file"` - Alloc string `flag:"profile.alloc" help:"write alloc profile to this file"` - Trace string `flag:"profile.trace" help:"write trace log to this file"` - Block string `flag:"profile.block" help:"write block profile to this file"` -} - -// Application is the interface that must be satisfied by an object passed to Main. -type Application interface { - // Name returns the application's name. It is used in help and error messages. - Name() string - // Most of the help usage is automatically generated, this string should only - // describe the contents of non flag arguments. - Usage() string - // ShortHelp returns the one line overview of the command. - ShortHelp() string - // DetailedHelp should print a detailed help message. It will only ever be shown - // when the ShortHelp is also printed, so there is no need to duplicate - // anything from there. - // It is passed the flag set so it can print the default values of the flags. - // It should use the flag sets configured Output to write the help to. - DetailedHelp(*flag.FlagSet) - // Run is invoked after all flag processing, and inside the profiling and - // error handling harness. - Run(ctx context.Context, args ...string) error -} - -type SubCommand interface { - Parent() string -} - -// This is the type returned by CommandLineErrorf, which causes the outer main -// to trigger printing of the command line help. -type commandLineError string - -func (e commandLineError) Error() string { return string(e) } - -// CommandLineErrorf is like fmt.Errorf except that it returns a value that -// triggers printing of the command line help. -// In general you should use this when generating command line validation errors. -func CommandLineErrorf(message string, args ...any) error { - return commandLineError(fmt.Sprintf(message, args...)) -} - -// Main should be invoked directly by main function. -// It will only return if there was no error. If an error -// was encountered it is printed to standard error and the -// application exits with an exit code of 2. -func Main(ctx context.Context, app Application, args []string) { - s := flag.NewFlagSet(app.Name(), flag.ExitOnError) - if err := Run(ctx, s, app, args); err != nil { - fmt.Fprintf(s.Output(), "%s: %v\n", app.Name(), err) - if _, printHelp := err.(commandLineError); printHelp { - // TODO(adonovan): refine this. It causes - // any command-line error to result in the full - // usage message, which typically obscures - // the actual error. - s.Usage() - } - os.Exit(2) - } -} - -// Run is the inner loop for Main; invoked by Main, recursively by -// Run, and by various tests. It runs the application and returns an -// error. -func Run(ctx context.Context, s *flag.FlagSet, app Application, args []string) (resultErr error) { - s.Usage = func() { - if app.ShortHelp() != "" { - fmt.Fprintf(s.Output(), "%s\n\nUsage:\n ", app.ShortHelp()) - if sub, ok := app.(SubCommand); ok && sub.Parent() != "" { - fmt.Fprintf(s.Output(), "%s [flags] %s", sub.Parent(), app.Name()) - } else { - fmt.Fprintf(s.Output(), "%s [flags]", app.Name()) - } - if usage := app.Usage(); usage != "" { - fmt.Fprintf(s.Output(), " %s", usage) - } - fmt.Fprint(s.Output(), "\n") - } - app.DetailedHelp(s) - } - p := addFlags(s, reflect.StructField{}, reflect.ValueOf(app)) - if err := s.Parse(args); err != nil { - return err - } - - if p != nil && p.CPU != "" { - f, err := os.Create(p.CPU) - if err != nil { - return err - } - if err := pprof.StartCPUProfile(f); err != nil { - f.Close() // ignore error - return err - } - defer func() { - pprof.StopCPUProfile() - if closeErr := f.Close(); resultErr == nil { - resultErr = closeErr - } - }() - } - - if p != nil && p.Trace != "" { - f, err := os.Create(p.Trace) - if err != nil { - return err - } - if err := trace.Start(f); err != nil { - f.Close() // ignore error - return err - } - defer func() { - trace.Stop() - if closeErr := f.Close(); resultErr == nil { - resultErr = closeErr - } - log.Printf("To view the trace, run:\n$ go tool trace view %s", p.Trace) - }() - } - - if p != nil && p.Memory != "" { - f, err := os.Create(p.Memory) - if err != nil { - return err - } - defer func() { - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.Printf("Writing memory profile: %v", err) - } - if err := f.Close(); err != nil { - log.Printf("Closing memory profile: %v", err) - } - }() - } - - if p != nil && p.Alloc != "" { - f, err := os.Create(p.Alloc) - if err != nil { - return err - } - defer func() { - if err := pprof.Lookup("allocs").WriteTo(f, 0); err != nil { - log.Printf("Writing alloc profile: %v", err) - } - if err := f.Close(); err != nil { - log.Printf("Closing alloc profile: %v", err) - } - }() - } - - if p != nil && p.Block != "" { - f, err := os.Create(p.Block) - if err != nil { - return err - } - runtime.SetBlockProfileRate(1) // record all blocking events - defer func() { - if err := pprof.Lookup("block").WriteTo(f, 0); err != nil { - log.Printf("Writing block profile: %v", err) - } - if err := f.Close(); err != nil { - log.Printf("Closing block profile: %v", err) - } - }() - } - - return app.Run(ctx, s.Args()...) -} - -// addFlags scans fields of structs recursively to find things with flag tags -// and add them to the flag set. -func addFlags(f *flag.FlagSet, field reflect.StructField, value reflect.Value) *Profile { - // is it a field we are allowed to reflect on? - if field.PkgPath != "" { - return nil - } - // now see if is actually a flag - flagNames, isFlag := field.Tag.Lookup("flag") - help := field.Tag.Get("help") - if isFlag { - nameList := strings.Split(flagNames, ",") - // add the main flag - addFlag(f, value, nameList[0], help) - if len(nameList) > 1 { - // and now add any aliases using the same flag value - fv := f.Lookup(nameList[0]).Value - for _, flagName := range nameList[1:] { - f.Var(fv, flagName, help) - } - } - return nil - } - // not a flag, but it might be a struct with flags in it - value = resolve(value.Elem()) - if value.Kind() != reflect.Struct { - return nil - } - - // TODO(adonovan): there's no need for this special treatment of Profile: - // The caller can use f.Lookup("profile.cpu") etc instead. - p, _ := value.Addr().Interface().(*Profile) - // go through all the fields of the struct - for i := 0; i < value.Type().NumField(); i++ { - child := value.Type().Field(i) - v := value.Field(i) - // make sure we have a pointer - if v.Kind() != reflect.Pointer { - v = v.Addr() - } - // check if that field is a flag or contains flags - if fp := addFlags(f, child, v); fp != nil { - p = fp - } - } - return p -} - -func addFlag(f *flag.FlagSet, value reflect.Value, flagName string, help string) { - switch v := value.Interface().(type) { - case flag.Value: - f.Var(v, flagName, help) - case *bool: - f.BoolVar(v, flagName, *v, help) - case *time.Duration: - f.DurationVar(v, flagName, *v, help) - case *float64: - f.Float64Var(v, flagName, *v, help) - case *int64: - f.Int64Var(v, flagName, *v, help) - case *int: - f.IntVar(v, flagName, *v, help) - case *string: - f.StringVar(v, flagName, *v, help) - case *uint: - f.UintVar(v, flagName, *v, help) - case *uint64: - f.Uint64Var(v, flagName, *v, help) - default: - log.Fatalf("field %q of type %T is not assignable to flag.Value", flagName, v) - } -} - -func resolve(v reflect.Value) reflect.Value { - for { - switch v.Kind() { - case reflect.Interface, reflect.Pointer: - v = v.Elem() - default: - return v - } - } -}
diff --git a/gopls/internal/util/asm/parse.go b/gopls/internal/util/asm/parse.go index 233c0c4..765eb5f 100644 --- a/gopls/internal/util/asm/parse.go +++ b/gopls/internal/util/asm/parse.go
@@ -9,6 +9,7 @@ "bufio" "bytes" "fmt" + "sort" "strings" "unicode" @@ -45,6 +46,8 @@ // A file represents a parsed file of Go assembly language. type File struct { + // Idents holds the identifiers of the file, ordered by Offset; + // [File.IdentAt] relies on that order for its binary search. Idents []Ident Mapper *protocol.Mapper @@ -63,6 +66,55 @@ return f.Mapper.OffsetLocation(ident.Offset, ident.Offset+ident.OrigLen) } +// IdentAt returns the identifier containing the byte range [start, end), +// or nil if none. Because [File.Idents] are ordered by Offset, the +// lookup uses a binary search. +func (f *File) IdentAt(start, end int) *Ident { + // Find the last identifier whose Offset <= start. + idx := sort.Search(len(f.Idents), func(i int) bool { + return f.Idents[i].Offset > start + }) + if idx == 0 { + return nil + } + id := &f.Idents[idx-1] + if end <= id.End() { + return id + } + return nil +} + +// FunctionRange returns the byte range [start, end) of the TEXT function +// enclosing offset: start is the beginning of the line containing the +// enclosing TEXT directive, end is the beginning of the line containing +// the next TEXT directive, or len(content) if there is none. If offset +// precedes the first TEXT directive, the range covers from 0 to the +// first TEXT directive. +// +// TEXT directives are taken from the parsed file rather than re-detected +// here, so that scoping stays consistent with the identifiers the parser +// reports (e.g. a bare "TEXT" line with no symbol is not a boundary). +func (f *File) FunctionRange(offset int) (int, int) { + content := f.Mapper.Content + funcStart, funcEnd := 0, len(content) + for i := range f.Idents { + id := &f.Idents[i] + if id.Kind != Text { + continue + } + lineStart := id.Offset + for lineStart > 0 && content[lineStart-1] != '\n' { + lineStart-- + } + if lineStart > offset { + funcEnd = lineStart + break + } + funcStart = lineStart + } + return funcStart, funcEnd +} + // Ident represents an identifier in an assembly file. type Ident struct { Name string // symbol name (after correcting [·âˆ•]); Name[0]='.' => current package
diff --git a/gopls/main.go b/gopls/main.go index 0a79d27..4343652 100644 --- a/gopls/main.go +++ b/gopls/main.go
@@ -11,15 +11,8 @@ package main import ( - "context" - "log" - "os" - "golang.org/x/telemetry" - "golang.org/x/telemetry/counter" "golang.org/x/tools/gopls/internal/cmd" - "golang.org/x/tools/gopls/internal/filecache" - "golang.org/x/tools/gopls/internal/tool" versionpkg "golang.org/x/tools/gopls/internal/version" ) @@ -33,24 +26,5 @@ Upload: true, }) - // Force early creation of the filecache and refuse to start - // if there were unexpected errors such as ENOSPC. This - // minimizes the window of exposure to deletion of the - // executable, and ensures that all subsequent calls to - // filecache.Get cannot fail for these two reasons; - // see issue #67433. - // - // This leaves only one likely cause for later failures: - // deletion of the cache while gopls is running. If the - // problem continues, we could periodically stat the cache - // directory (for example at the start of every RPC) and - // either re-create it or just fail the RPC with an - // informative error and terminate the process. - if _, err := filecache.Get("nonesuch", [32]byte{}, filecache.Bytes); err != nil && err != filecache.ErrNotFound { - counter.Inc("gopls/nocache") - log.Fatalf("gopls cannot access its persistent index (disk full?): %v", err) - } - - ctx := context.Background() - tool.Main(ctx, cmd.New(), os.Args[1:]) + cmd.Main() }
diff --git a/internal/analysis/analyzerutil/version.go b/internal/analysis/analyzerutil/version.go index 700d53e..60d7253 100644 --- a/internal/analysis/analyzerutil/version.go +++ b/internal/analysis/analyzerutil/version.go
@@ -38,7 +38,7 @@ // The bootstrap rule does not cover tests, // and some tests (e.g. debug/elf/file_test.go) rely on this. pkgpath := pass.Pkg.Path() - if packagepath.IsStdPackage(pkgpath) && + if packagepath.MaybeStdPackage(pkgpath) && stdlib.IsBootstrapPackage(pkgpath) && // (excludes "*_test" external test packages) !strings.HasSuffix(pass.Fset.File(file.Pos()).Name(), "_test.go") { // (excludes all tests) fileVersion = stdlib.BootstrapVersion.String() // package must bootstrap
diff --git a/gopls/internal/util/moremaps/maps.go b/internal/moremaps/maps.go similarity index 77% rename from gopls/internal/util/moremaps/maps.go rename to internal/moremaps/maps.go index 79ac2ce..a1bae07 100644 --- a/gopls/internal/util/moremaps/maps.go +++ b/internal/moremaps/maps.go
@@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package moremaps contains more functions for working with maps. package moremaps import ( @@ -89,3 +90,27 @@ delete(m, k) return pre != len(m) } + +// Entry is a key-value pair obtained from a map. +type Entry[K comparable, V any] struct { + Key K + Value V +} + +// Entries returns a new unordered array of the entries of a map. +func Entries[M ~map[K]V, K comparable, V any](m M) []Entry[K, V] { + entries := make([]Entry[K, V], 0, len(m)) + for k, v := range m { + entries = append(entries, Entry[K, V]{k, v}) + } + return entries +} + +// FromEntries returns a new map into which the entries have been inserted in order. +func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V { + m := make(map[K]V, len(entries)) + for _, e := range entries { + m[e.Key] = e.Value + } + return m +}
diff --git a/gopls/internal/util/moreslices/slices.go b/internal/moreslices/slices.go similarity index 91% rename from gopls/internal/util/moreslices/slices.go rename to internal/moreslices/slices.go index 7658cd8..bee74d8 100644 --- a/gopls/internal/util/moreslices/slices.go +++ b/internal/moreslices/slices.go
@@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +// Package moreslices contains more functions for working with slices. package moreslices // Remove removes all values equal to elem from slice.
diff --git a/internal/packagepath/packagepath.go b/internal/packagepath/packagepath.go index fa39a13..7b25340 100644 --- a/internal/packagepath/packagepath.go +++ b/internal/packagepath/packagepath.go
@@ -36,9 +36,19 @@ return true } -// IsStdPackage reports whether the specified package path belongs to a -// package in the standard library (including internal dependencies). -func IsStdPackage(path string) bool { +// MaybeStdPackage reports whether the specified package path might +// belong to a package in the standard library (including internal +// dependencies), based only on its form. +// +// It may spuriously return true, but a result of false is definitive: +// +// MaybeStdPackage("fmt") = true +// MaybeStdPackage("maybe/tomorrow") = true // false positive +// MaybeStdPackage("example.com/foo") = false +// +// For a definitive answer, use [stdlib.HasPackage], which consults a +// huge table. +func MaybeStdPackage(path string) bool { // A standard package has no dot in its first segment. // (It may yet have a dot, e.g. "vendor/golang.org/x/foo".) slash := strings.IndexByte(path, '/')
diff --git a/internal/packagepath/packagepath_test.go b/internal/packagepath/packagepath_test.go index 18ee7b1..ac7e323 100644 --- a/internal/packagepath/packagepath_test.go +++ b/internal/packagepath/packagepath_test.go
@@ -39,7 +39,7 @@ } } -func TestIsStdPackage(t *testing.T) { +func TestMaybeStdPackage(t *testing.T) { testCases := []struct { pkgpath string isStd bool @@ -49,11 +49,12 @@ {pkgpath: "vendor/golang.org/x/net/dns/dnsmessage", isStd: true}, {pkgpath: "golang.org/x/net/dns/dnsmessage", isStd: false}, {pkgpath: "testdata", isStd: false}, + {pkgpath: "myprivateapp", isStd: true}, // a false positive (go.dev/issue/80555) } for _, tc := range testCases { t.Run(tc.pkgpath, func(t *testing.T) { - got := IsStdPackage(tc.pkgpath) + got := MaybeStdPackage(tc.pkgpath) if got != tc.isStd { t.Fatalf("got %t want %t", got, tc.isStd) }
diff --git a/internal/refactor/imports.go b/internal/refactor/imports.go index 5ce70ae..046038c 100644 --- a/internal/refactor/imports.go +++ b/internal/refactor/imports.go
@@ -125,13 +125,13 @@ var pos token.Pos if gd, ok := decl0.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() { // Have existing grouped import ( ... ) decl. - if packagepath.IsStdPackage(pkgpath) && len(gd.Specs) > 0 { + if packagepath.MaybeStdPackage(pkgpath) && len(gd.Specs) > 0 { // Add spec for a std package before // first existing spec, followed by // a blank line if the next one is non-std. first := gd.Specs[0].(*ast.ImportSpec) pos = first.Pos() - if !packagepath.IsStdPackage(first.Path.Value) { + if !packagepath.MaybeStdPackage(first.Path.Value) { newText += "\n" } newText += "\n\t"
diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go index 68e2844..313e0f7 100644 --- a/internal/refactor/inline/callee.go +++ b/internal/refactor/inline/callee.go
@@ -8,6 +8,7 @@ import ( "bytes" + "cmp" "encoding/gob" "fmt" "go/ast" @@ -18,6 +19,7 @@ "strings" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/moremaps" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" ) @@ -885,6 +887,34 @@ return s } +var ( + _ gob.GobEncoder = (*shadowMap)(nil) + _ gob.GobDecoder = (*shadowMap)(nil) +) + +// GobEncode implements gob.GobEncoder, encoding the map's entries in a +// deterministic order so that serialized facts are stable. +func (s *shadowMap) GobEncode() ([]byte, error) { + entries := moremaps.Entries(*s) + slices.SortFunc(entries, func(x, y moremaps.Entry[string, int]) int { + return cmp.Compare(x.Key, y.Key) + }) + var out bytes.Buffer + if err := gob.NewEncoder(&out).Encode(entries); err != nil { + return nil, err + } + return out.Bytes(), nil +} + +func (s *shadowMap) GobDecode(data []byte) error { + var entries []moremaps.Entry[string, int] + if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&entries); err != nil { + return err + } + *s = moremaps.FromEntries(entries) + return nil +} + // fieldObjs returns a map of each types.Object defined by the given signature // to its index in the parameter list. Parameters with missing or blank name // are skipped.
diff --git a/internal/refactor/inline/calleefx.go b/internal/refactor/inline/calleefx.go index 001bf61..6dcf0b9 100644 --- a/internal/refactor/inline/calleefx.go +++ b/internal/refactor/inline/calleefx.go
@@ -10,6 +10,7 @@ "go/ast" "go/token" "go/types" + "slices" "golang.org/x/tools/internal/typesinternal" ) @@ -71,14 +72,20 @@ // unreferenced by the function body). This lets us // not bother implementing the complete traversal into // control structures. - // - // TODO(adonovan): add them in a deterministic order. - // (This is not a bug but determinism is good.) - for _, pinfo := range paramInfos { + + // Sort params by Index for determinism + sortedParams := make([]*types.Var, 0, len(paramInfos)) + for obj, pinfo := range paramInfos { if !pinfo.IsResult && len(pinfo.Refs) > 0 { - effect(pinfo.Index) + sortedParams = append(sortedParams, obj) } } + slices.SortFunc(sortedParams, func(a, b *types.Var) int { + return paramInfos[a].Index - paramInfos[b].Index + }) + for _, obj := range sortedParams { + effect(paramInfos[obj].Index) + } } var visitExpr func(n ast.Expr)
diff --git a/internal/refactor/inline/falcon.go b/internal/refactor/inline/falcon.go index 037d33b..884a807 100644 --- a/internal/refactor/inline/falcon.go +++ b/internal/refactor/inline/falcon.go
@@ -13,6 +13,7 @@ "go/format" "go/token" "go/types" + "slices" "strconv" "strings" @@ -124,11 +125,20 @@ // type mapping st.int = st.typename(types.Typ[types.Int]) st.any = "interface{}" // don't use "any" as it may be shadowed - for obj, info := range st.params { + + // Sort params by Index for determinism + sortedParams := make([]*types.Var, 0, len(st.params)) + for obj := range st.params { if isBasic(obj.Type(), types.IsConstType) { - info.FalconType = st.typename(obj.Type()) + sortedParams = append(sortedParams, obj) } } + slices.SortFunc(sortedParams, func(a, b *types.Var) int { + return st.params[a].Index - st.params[b].Index + }) + for _, obj := range sortedParams { + st.params[obj].FalconType = st.typename(obj.Type()) + } st.stmt(st.decl.Body)
diff --git a/internal/typesinternal/assignedaddress.go b/internal/typesinternal/assignedaddress.go new file mode 100644 index 0000000..020defc --- /dev/null +++ b/internal/typesinternal/assignedaddress.go
@@ -0,0 +1,128 @@ +// Copyright 2026 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/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" +) + +// IsAssignedOrAddressTaken reports whether the expression cur denotes a +// variable and appears in a context that assigns it or that takes its address, +// potentially leading to indirect assignment. +// +// These examples cause IsAssignedOrAddressTaken on the identifier for x to +// return true: +// +// x = 1 +// x++ +// x[i] = 1 (assume x is an array) +// x.a[i] = 1 (assume x.a is a non-pointer struct field) +// use(&x) +// +// whereas these cause it to return false: +// +// y = x +// f(x) +// use(x.a[i]) +// use(*x) +// +// The expression may itself be a compound, for example: +// +// use(&(*ptr)) => IsAssignedOrAddressTaken("*ptr") = true +// x.a[i] = 1 => IsAssignedOrAddressTaken("x.a") = true +// _ = x.a[i] => IsAssignedOrAddressTaken("x.a") = false +// +// A variable's declaration is not considered to be an assignment: +// +// var x int => IsAssignedOrAddressTaken(x) = false +// x := 1 => IsAssignedOrAddressTaken(x) = false +// +// TODO(adonovan): revisit the surprising behavior for declarations. +func IsAssignedOrAddressTaken(info *types.Info, cur inspector.Cursor) bool { + // Unfortunately we can't simply use info.Types[e].Assignable() + // as it is always true for a variable even when that variable is + // used only as an r-value. So we must inspect enclosing syntax. +outer: + // Ascend to outermost aggregate of which + // original cur is a part: + // x -> (x) | x.f | x[i] | x[i:j] + for cur = range cur.Enclosing() { + switch cur.ParentEdgeKind() { + case edge.ParenExpr_X: + // If x is an lvalue, then (x) is an lvalue. + case edge.SelectorExpr_X: + // If x is an lvalue, then x.f is an lvalue iff + // the selection does not traverse a pointer. + sel := cur.Parent().Node().(*ast.SelectorExpr) + if seln, ok := info.Selections[sel]; ok { + // Note: there is a bug in Indirect() where it spuriously returns true + // when both the selection receiver and parameter are pointers. However, + // it's okay in this case because there is no address taken when a + // pointer receiver method is called on a pointer type. + if seln.Indirect() { + return false + } + if seln.Kind() == types.MethodVal { + sig := seln.Obj().Type().(*types.Signature) + if is[*types.Pointer](sig.Recv().Type().Underlying()) { + t := seln.Recv() + // The receiver may be an embedded field, so we need + // to get the inner-most type (right before the method + // call in seln.Index()) + for _, idx := range seln.Index()[:len(seln.Index())-1] { + t = t.Underlying().(*types.Struct).Field(idx).Type() + } + if !is[*types.Pointer](t.Underlying()) { + return true // takes address of receiver + } + } + return false + } + } + case edge.IndexExpr_X, edge.SliceExpr_X: + // If x[i] or x[i:j] is an lvalue, + // then x is an lvalue iff x is an array. + if !is[*types.Array](info.TypeOf(cur.Node().(ast.Expr)).Underlying()) { + return false + } + default: + break outer + } + } + switch cur.ParentEdgeKind() { + case edge.AssignStmt_Lhs: + assign := cur.Parent().Node().(*ast.AssignStmt) + if assign.Tok != token.DEFINE { + return true // x = j or x += j + } + id := cur.Node().(*ast.Ident) + // Re-assigned identifiers are recorded in the Uses map. + if _, ok := info.Uses[id]; ok { + return true // reassignment of x (x, y := 1, 2) + } + case edge.RangeStmt_Key, edge.RangeStmt_Value: + rng := cur.Parent().Node().(*ast.RangeStmt) + if rng.Tok == token.ASSIGN { + return true // "for k, v = range x" is like an AssignStmt to k, v + } + case edge.IncDecStmt_X: + return true // x++, x-- + case edge.UnaryExpr_X: + if cur.Parent().Node().(*ast.UnaryExpr).Op == token.AND { + return true // &x + } + } + return false +} + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +}
diff --git a/internal/typesinternal/assignedaddress_test.go b/internal/typesinternal/assignedaddress_test.go new file mode 100644 index 0000000..c2f2dda --- /dev/null +++ b/internal/typesinternal/assignedaddress_test.go
@@ -0,0 +1,192 @@ +// Copyright 2026 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 ( + "bytes" + "go/ast" + "go/parser" + "go/printer" + "go/token" + "go/types" + "strings" + "testing" + + "golang.org/x/tools/go/ast/inspector" +) + +// TestIsAssignedOrAddressTaken tests [IsAssignedOrAddressTaken]. +// +// Each /*true*/ or /*false*/ comment in the test source asserts the result of +// IsAssignedOrAddressTaken(info, expr), where expr is the outermost expression +// ending immediately before that comment. +// There can be no space before the comment. +func TestIsAssignedOrAddressTaken(t *testing.T) { + const src = `package p + +type S struct { + f int +} + +type NestedS struct { + inner S +} + +type M struct{} + +func (M) ValRecv() {} +func (*M) PtrRecv() {} + +type EmbedPtrM struct { + *M +} + +type EmbedValM struct { + M +} + +func ptr(*int) {} +func fn(int) {} + +type StructWithArray struct { + arr [3]int +} + +func _() { + var x/*false*/ int // declarations do not count as assigned or address taken + fn(x/*false*/) + _ = x/*false*/ + _/*true*/ = 10 + x/*true*/ = 10 + x/*true*/ += 5 + (x/*true*/) = 1 // testing that IsAssignedOrAddressTaken correctly unwraps enclosing parentheses + (x)/*true*/ = 1 + x/*true*/ ++ + x/*true*/ -- + ptr(&(x/*true*/)) // targets "x", which is address-taken + ptr(&(x)/*false*/) // targets compound expression "&x", which itself is not address-taken. the value of the pointer is copied during argument passing + + y/*false*/ := 1 // declarations do not count as assigned or address taken + _ = y/*false*/ + x/*true*/, z/*false*/ := 2, 3 // x is re-assigned, z is declared + _ = z/*false*/ + + var a [3]int + a/*true*/ [0] = 1 // targets "a" + a[0]/*true*/ = 1 // targets "a[0]" + + ptr(&a/*true*/ [0]) // targets "a" + ptr(&(a[0]/*true*/)) // targets "a[0]" + ptr(&(a[0])/*false*/) // targets "&(a[0])" + + // A slice descriptor contains a pointer to the start of its underlying array. + var s []int + s/*false*/ [0] = 1 // a load of the pointer in the slice "s", not a direct assignment of "s" + s[0]/*true*/ = 1 // a store to the array element s[0] + + ptr(&s/*false*/ [0]) + ptr(&(s[0]/*true*/)) + ptr(&(s[0])/*false*/) + + var st S + st/*true*/ .f = 1 + st.f/*true*/ = 1 + + var pst *S + pst/*false*/ .f = 1 + pst.f/*true*/ = 1 + + var pst2 *NestedS + pst2/*false*/ .inner.f = 1 // indirect reference through pointer + pst2.inner/*true*/ .f = 1 + pst2.inner.f/*true*/ = 1 + + var m M + m/*true*/ .PtrRecv() // calling a method with pointer receiver on a value expression takes the address + m/*false*/ .ValRecv() + + var epm EmbedPtrM + epm/*false*/ .PtrRecv() + + var evm EmbedValM + evm/*true*/ .PtrRecv() + + var pm *M + pm/*false*/ .PtrRecv() + pm/*false*/ .ValRecv() // dereferences and passes value + + var p *int + *(p/*false*/) = 1 + *(p)/*true*/ = 1 + _ = *(p/*false*/) + ptr(&(*(p)/*true*/)) // targets "*(p)", which is address-taken + + var sa StructWithArray + sa.arr/*true*/ [0] = 1 + _ = sa.arr/*false*/ [0] // rvalue + + var mp map[int]int + mp/*false*/ [0] = 1 // targets "mp", which is a pointer to the map header and itself is not modified + mp[0]/*true*/ = 1 + + var ch chan int + ch/*false*/ <- x/*false*/ + + var k, v int + for k/*true*/, v/*true*/ = range mp {} + for k2/*false*/, v2/*false*/ := range mp { + _ = k2 + _ = v2 + } + _ = k + _ = v +} + +` + + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "p.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + + var conf types.Config + info := NewTypesInfo() // need info.Selections + _, err = conf.Check("p", fset, []*ast.File{file}, info) + if err != nil { + t.Fatal(err) + } + + inspect := inspector.New([]*ast.File{file}) + for _, cg := range file.Comments { + var ( + pos = cg.Pos() + line = fset.Position(pos).Line + text = strings.TrimSpace(cg.Text()) + ) + if text != "true" && text != "false" { + continue // skip other comments + } + + // No spaces allowed before the comment. + cur, ok := inspect.Root().FindByPos(pos, pos) + if !ok { + t.Errorf("comment %q at line %d: no cursor found", text, line) + continue + } + // Find the outermost expression ending at or before the comment position. + for cur.Parent().Node() != nil && cur.Parent().Node().End() <= pos { + cur = cur.Parent() + } + + got := IsAssignedOrAddressTaken(info, cur) + want := text == "true" + if got != want { + var buf bytes.Buffer + printer.Fprint(&buf, fset, cur.Node()) + t.Errorf("line %d: IsLValue for expression %s = %t, want %t", line, buf.String(), got, want) + } + } +}
diff --git a/internal/typesinternal/classify_call.go b/internal/typesinternal/classify_call.go index 7ebe976..d5c40a2 100644 --- a/internal/typesinternal/classify_call.go +++ b/internal/typesinternal/classify_call.go
@@ -8,7 +8,6 @@ "fmt" "go/ast" "go/types" - _ "unsafe" // for go:linkname hack ) // CallKind describes the function position of an [*ast.CallExpr]. @@ -72,11 +71,15 @@ if tv.IsBuiltin() { return CallBuiltin } - obj := info.Uses[UsedIdent(info, call.Fun)] + id := UsedIdent(info, call.Fun) + if id == nil { + return CallDynamic + } + obj := info.Uses[id] // Classify the call by the type of the object, if any. switch obj := obj.(type) { case *types.Func: - if interfaceMethod(obj) { + if isInterfaceMethod(obj) { return CallInterface } return CallStatic @@ -127,11 +130,69 @@ // Note: if e is an instantiated function or method, UsedIdent returns // the corresponding generic function or method on the generic type. func UsedIdent(info *types.Info, e ast.Expr) *ast.Ident { - return usedIdent(info, e) + if info.Types == nil || info.Uses == nil { + panic("one of info.Types or info.Uses is nil; both must be populated") + } + // Look through type instantiation if necessary. + switch d := ast.Unparen(e).(type) { + case *ast.IndexExpr: + if info.Types[d.Index].IsType() { + e = d.X + } + case *ast.IndexListExpr: + e = d.X + } + + switch e := ast.Unparen(e).(type) { + // info.Uses always has the object we want, even for selector expressions. + // We don't need info.Selections. + // See go/types/recording.go:recordSelection. + case *ast.Ident: + return e + case *ast.SelectorExpr: + return e.Sel + } + return nil } -//go:linkname usedIdent golang.org/x/tools/go/types/typeutil.usedIdent -func usedIdent(info *types.Info, e ast.Expr) *ast.Ident +// See [golang.org/x/tools/go/types/typeutil.Callee]. +func Callee(info *types.Info, call *ast.CallExpr) types.Object { + id := UsedIdent(info, call.Fun) + if id == nil { + return nil + } + obj := info.Uses[id] + if obj == nil { + return nil + } + if _, ok := obj.(*types.TypeName); ok { + return nil + } + if fn, ok := obj.(*types.Func); ok { + return fn.Origin() + } + return obj +} -//go:linkname interfaceMethod golang.org/x/tools/go/types/typeutil.interfaceMethod -func interfaceMethod(f *types.Func) bool +// See [golang.org/x/tools/go/types/typeutil.StaticCallee]. +func StaticCallee(info *types.Info, call *ast.CallExpr) *types.Func { + id := UsedIdent(info, call.Fun) + if id == nil { + return nil + } + obj := info.Uses[id] + if obj == nil { + return nil + } + fn, _ := obj.(*types.Func) + if fn == nil || isInterfaceMethod(fn) { + return nil + } + return fn.Origin() +} + +// isInterfaceMethod reports whether its argument is a method of an interface. +func isInterfaceMethod(f *types.Func) bool { + recv := f.Signature().Recv() + return recv != nil && types.IsInterface(recv.Type()) +}
diff --git a/internal/typesinternal/element.go b/internal/typesinternal/element.go index 89eeea1..bab37fb 100644 --- a/internal/typesinternal/element.go +++ b/internal/typesinternal/element.go
@@ -7,8 +7,6 @@ import ( "fmt" "go/types" - - "golang.org/x/tools/go/types/typeutil" ) // ForEachElement calls f for type T and each type reachable from its @@ -16,25 +14,24 @@ // type constructors; in addition, for each named type N, the type *N // is added to the result as it may have additional methods. // -// The caller must provide an initially empty set used to de-duplicate -// identical types, potentially across multiple calls to ForEachElement. -// (Its final value holds all the elements seen, matching the arguments -// passed to f.) +// The access argument passed to f indicates whether the type is +// inaccessible to reflection (for example, intermediate tuple types +// or underlying types of named types). // -// TODO(adonovan): share/harmonize with go/callgraph/rta. -func ForEachElement(rtypes *typeutil.Map, msets *typeutil.MethodSetCache, T types.Type, f func(types.Type)) { - var visit func(T types.Type, skip bool) - visit = func(T types.Type, skip bool) { - if !skip { - if seen, _ := rtypes.Set(T, true).(bool); seen { - return // de-dup - } - - f(T) // notify caller of new element type +// The result of f indicates whether the caller has seen this type +// already, so we can prune the traversal. +// +// methodSetOf abstracts (*typeutil.MethodSetCache).MethodSet, +// avoiding an import cycle. +func ForEachElement(methodSetOf func(types.Type) *types.MethodSet, T types.Type, f func(T types.Type, access bool) bool) { + var visit func(T types.Type, access bool) + visit = func(T types.Type, access bool) { + if f(T, access) { + return // duplicate; prune descent } // Recursion over signatures of each method. - tmset := msets.MethodSet(T) + tmset := methodSetOf(T) for method := range tmset.Methods() { sig := method.Type().(*types.Signature) if sig.TypeParams() != nil { @@ -65,13 +62,13 @@ // // 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 + visit(sig.Params(), false) // the Tuple is inaccessible + visit(sig.Results(), false) // the Tuple is inaccessible } switch T := T.(type) { case *types.Alias: - visit(types.Unalias(T), skip) // emulates the pre-Alias behavior + visit(types.Unalias(T), access) // emulates the pre-Alias behavior case *types.Basic: // nop @@ -80,49 +77,49 @@ // nop---handled by recursion over method set. case *types.Pointer: - visit(T.Elem(), false) + visit(T.Elem(), true) case *types.Slice: - visit(T.Elem(), false) + visit(T.Elem(), true) case *types.Chan: - visit(T.Elem(), false) + visit(T.Elem(), true) case *types.Map: - visit(T.Key(), false) - visit(T.Elem(), false) + visit(T.Key(), true) + visit(T.Elem(), true) 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 + visit(T.Params(), false) // the Tuple is inaccessible + visit(T.Results(), false) // the Tuple is inaccessible 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) + visit(types.NewPointer(T), true) // 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 + // so mark it inaccessible during recursion. + visit(T.Underlying(), false) // skip the unnamed type case *types.Array: - visit(T.Elem(), false) + visit(T.Elem(), true) 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) + visit(T.Field(i).Type(), true) } case *types.Tuple: for i, n := 0, T.Len(); i < n; i++ { - visit(T.At(i).Type(), false) + visit(T.At(i).Type(), true) } case *types.TypeParam, *types.Union: @@ -133,5 +130,5 @@ panic(fmt.Sprintf("ForEachElement called on unexpected type %T", T)) } } - visit(T, false) + visit(T, true) }
diff --git a/internal/typesinternal/element_test.go b/internal/typesinternal/element_test.go index 7e70d0b..1faa86c 100644 --- a/internal/typesinternal/element_test.go +++ b/internal/typesinternal/element_test.go
@@ -135,26 +135,18 @@ } got := make(map[string]bool) - set := new(typeutil.Map) // for de-duping - set2 := new(typeutil.Map) // for consistency check - typesinternal.ForEachElement(set, &msets, T, func(elem types.Type) { - got[toStr(elem)] = true - set2.Set(elem, true) + set := new(typeutil.Map) // for de-duping + typesinternal.ForEachElement(msets.MethodSet, T, func(T types.Type, access bool) bool { + if !access { + return false // inaccessible to reflection + } + seen, _ := set.Set(T, true).(bool) + if !seen { + got[toStr(T)] = true + } + return seen }) - // Assert that set==set2, meaning f(x) was - // called for each x in the de-duping map. - if set.Len() != set2.Len() { - t.Errorf("ForEachElement called f %d times yet de-dup set has %d elements", - set2.Len(), set.Len()) - } else { - set.Iterate(func(key types.Type, _ any) { - if set2.At(key) == nil { - t.Errorf("ForEachElement did not call f(%v)", key) - } - }) - } - // Assert than all expected (and no unexpected) elements were found. fail := false for _, typstr := range test.want {
diff --git a/internal/typesinternal/toonew.go b/internal/typesinternal/toonew.go index cc86487..386c59c 100644 --- a/internal/typesinternal/toonew.go +++ b/internal/typesinternal/toonew.go
@@ -13,20 +13,30 @@ // TooNewStdSymbols computes the set of package-level symbols // exported by pkg that are not available at the specified version. -// The result maps each symbol to its minimum version. // // The pkg is allowed to contain type errors. -func TooNewStdSymbols(pkg *types.Package, version string) map[types.Object]string { - disallowed := make(map[types.Object]string) +func TooNewStdSymbols(pkg *types.Package, version string) map[types.Object]stdlib.Symbol { + disallowed := make(map[types.Object]stdlib.Symbol) + + // Some symbols are accessible before their release but + // only with specific build tags unknown to us here. + // Avoid false positives in such cases. + if pkg.Path() == "testing/synctest" && versions.AtLeast(version, "go1.24") { + // requires go1.24 && goexperiment.synctest || go1.25 + return disallowed + } + if (pkg.Path() == "encoding/json/v2" || pkg.Path() == "encoding/json/jsontext") && versions.AtLeast(version, "go1.25") { + // requires go1.25 && goexperiment.jsonv2 || go1.27 + return disallowed + } // Pass 1: package-level symbols. symbols := stdlib.PackageSymbols[pkg.Path()] for _, sym := range symbols { - symver := sym.Version.String() - if versions.Before(version, symver) { + if versions.Before(version, sym.Version.String()) { switch sym.Kind { case stdlib.Func, stdlib.Var, stdlib.Const, stdlib.Type: - disallowed[pkg.Scope().Lookup(sym.Name)] = symver + disallowed[pkg.Scope().Lookup(sym.Name)] = sym } } } @@ -60,28 +70,36 @@ // spuriously cause the analyzer to report a reference to a // too-new symbol even though this expression compiles just // fine (with the fake implementation) using go1.21. + var noSym stdlib.Symbol + depth := make(map[types.Object]int) for _, sym := range symbols { - symVersion := sym.Version.String() - if !versions.Before(version, symVersion) { + if !versions.Before(version, sym.Version.String()) { continue // allowed } var obj types.Object + var indices []int switch sym.Kind { case stdlib.Field: typename, name := sym.SplitField() - if t := pkg.Scope().Lookup(typename); t != nil && disallowed[t] == "" { - obj, _, _ = types.LookupFieldOrMethod(t.Type(), false, pkg, name) + if t := pkg.Scope().Lookup(typename); t != nil && disallowed[t] == noSym { + obj, indices, _ = types.LookupFieldOrMethod(t.Type(), false, pkg, name) } case stdlib.Method: ptr, recvname, name := sym.SplitMethod() - if t := pkg.Scope().Lookup(recvname); t != nil && disallowed[t] == "" { - obj, _, _ = types.LookupFieldOrMethod(t.Type(), ptr, pkg, name) + if t := pkg.Scope().Lookup(recvname); t != nil && disallowed[t] == noSym { + obj, indices, _ = types.LookupFieldOrMethod(t.Type(), ptr, pkg, name) } } if obj != nil { - disallowed[obj] = symVersion + // In the presence of embedding, two or more "pkg.T.name" + // strings may map to the same types.Object. + // Prefer the Object with the shorter index path. + if min, ok := depth[obj]; !ok || len(indices) < min { + depth[obj] = len(indices) + disallowed[obj] = sym + } } }
diff --git a/internal/typesinternal/types.go b/internal/typesinternal/types.go index d2c0b4c..9fd48b0 100644 --- a/internal/typesinternal/types.go +++ b/internal/typesinternal/types.go
@@ -270,3 +270,11 @@ } } } + +func TupleOf(elems ...types.Type) *types.Tuple { + params := make([]*types.Var, len(elems)) + for i, elem := range elems { + params[i] = types.NewParam(token.NoPos, nil, "", elem) + } + return types.NewTuple(params...) +}
diff --git a/present/doc.go b/present/doc.go index 2c88fb9..5e8ff13 100644 --- a/present/doc.go +++ b/present/doc.go
@@ -7,6 +7,9 @@ which can be slide presentations as in golang.org/x/tools/cmd/present or articles as in golang.org/x/blog (the Go blog). +This package assumes presentation authors are trusted, +and should not be used with untrusted inputs. + # File Format Present files begin with a header giving the title of the document @@ -29,7 +32,7 @@ 2 Jan 2006 -In this case, the time will be interpreted as 10am UTC on that date. +In this case, the time will be interpreted as 11am UTC on that date. The tags line is a comma-separated list of tags that may be used to categorize the document.
diff --git a/present/parse.go b/present/parse.go index 8b41dd2..a0e1d58 100644 --- a/present/parse.go +++ b/present/parse.go
@@ -458,6 +458,9 @@ case strings.HasPrefix(text, "."): args := strings.Fields(text) if args[0] == ".background" { + if len(args) != 2 { + return nil, fmt.Errorf("%s:%d: .background expects exactly one argument", name, lines.line) + } section.Classes = append(section.Classes, "background") section.Styles = append(section.Styles, "background-image: url('"+args[1]+"')") break
diff --git a/present/parse_test.go b/present/parse_test.go index bb0fe72..064fc23 100644 --- a/present/parse_test.go +++ b/present/parse_test.go
@@ -12,9 +12,35 @@ "os/exec" "path/filepath" "runtime" + "strings" "testing" ) +func TestBackgroundErrors(t *testing.T) { + const input = `Title + +* Slide + +%s +` + const want = "test.slide:5: .background expects exactly one argument" + + for _, directive := range []string{ + ".background", + ".background a.png b.png", + } { + t.Run(directive, func(t *testing.T) { + _, err := Parse(strings.NewReader(fmt.Sprintf(input, directive)), "test.slide", 0) + if err == nil { + t.Fatalf("Parse did not return an error") + } + if got := err.Error(); got != want { + t.Errorf("Parse error = %q, want %q", got, want) + } + }) + } +} + func TestTestdata(t *testing.T) { tmpl := template.Must(Template().Parse(testTmpl)) filesP, err := filepath.Glob("testdata/*.p")
diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index 720ecc1..4299868 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go
@@ -11,7 +11,7 @@ // It requires well-typed inputs, and may panic otherwise. // // This package reimplements parts of the type checker. See -// https://go.dev/issue/70638 for a proposal to expose the the work +// https://go.dev/issue/70638 for a proposal to expose the work // already done by the type checker, which would make this package // redundant. package satisfy @@ -355,7 +355,8 @@ if e.Name == "_" { // e.g. "for _ = range x" return tInvalid } - panic("undefined ident: " + e.Name) + // There could be a missing definition, return an invalid type + return tInvalid case *ast.Ellipsis: if e.Elt != nil { @@ -374,7 +375,14 @@ case *types.Struct: for i, elem := range e.Elts { if kv, ok := elem.(*ast.KeyValueExpr); ok { - f.assign(f.info.Uses[kv.Key.(*ast.Ident)].Type(), f.expr(kv.Value)) + // in weird code, kv.Key might not be an identifier + id, ok := kv.Key.(*ast.Ident) + if !ok || f.info.Uses[id] == nil { + f.expr(kv.Value) + continue + + } + f.assign(f.info.Uses[id].Type(), f.expr(kv.Value)) } else { f.assign(T.Field(i).Type(), f.expr(elem)) } @@ -412,7 +420,10 @@ f.expr(e.X) } } else { - return f.info.Uses[e.Sel].Type() // qualified identifier + if obj, ok := f.info.Uses[e.Sel]; ok { + return obj.Type() // qualified identifier + } + return tInvalid } case *ast.IndexExpr: