diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d0485e8..0e9cebc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md
@@ -4,6 +4,24 @@ It is the work of hundreds of contributors. We appreciate your help! +## Contributing code + +Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) +before sending patches. + +Unless otherwise noted, the Go source files are distributed under +the BSD-style license found in the LICENSE file. + +### Contributing to gopls + +This repository has a multi-module structure: the root module +(`golang.org/x/tools`) and the `gopls` module +(`golang.org/x/tools/gopls`). + +If you are contributing to `gopls`, please see the specific [gopls +contribution guide](gopls/doc/contributing.md) for detailed instructions on +workspace setup, building, testing, and debugging. + ## Filing issues When [filing an issue](https://golang.org/issue/new), make sure to answer these five questions: @@ -17,10 +35,3 @@ General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker. The gophers there will answer or ask you to file an issue if you've tripped over a bug. -## Contributing code - -Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html) -before sending patches. - -Unless otherwise noted, the Go source files are distributed under -the BSD-style license found in the LICENSE file.
diff --git a/cmd/callgraph/main.go b/cmd/callgraph/main.go index e489de8..9804101 100644 --- a/cmd/callgraph/main.go +++ b/cmd/callgraph/main.go
@@ -25,8 +25,10 @@ "fmt" "go/token" "io" + "log" "os" "runtime" + "runtime/pprof" "text/template" "golang.org/x/tools/go/callgraph" @@ -52,6 +54,9 @@ "A template expression specifying how to format an edge") tagsFlag = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)") + + cpuProfile = flag.String("cpuprofile", "", "write CPU profile to this file") + memProfile = flag.String("memprofile", "", "write memory profile to this file") ) const Usage = `callgraph: display the call graph of a Go program. @@ -82,6 +87,7 @@ digraph output suitable for input to golang.org/x/tools/cmd/digraph. graphviz output in AT&T GraphViz (.dot) format. + '' output nothing (useful when profiling) All other values are interpreted using text/template syntax. The default value is: @@ -144,17 +150,42 @@ digraph succs golang.org/x/tools/cmd/callgraph.main ` -func init() { - // If $GOMAXPROCS isn't set, use the full capacity of the machine. - // For small machines, use at least 4 threads. - if os.Getenv("GOMAXPROCS") == "" { - n := max(runtime.NumCPU(), 4) - runtime.GOMAXPROCS(n) - } -} - func main() { flag.Parse() + + if *cpuProfile != "" { + f, err := os.Create(*cpuProfile) + if err != nil { + log.Fatal(err) + } + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal(err) + } + // Note: in case of error, program exits before writing profile. + defer func() { + pprof.StopCPUProfile() + log.Printf("Run: go tool pprof %s # (cpu)", *cpuProfile) + }() + } + + if *memProfile != "" { + f, err := os.Create(*memProfile) + if err != nil { + log.Fatal(err) + } + // Note: in case of error, program exits before writing profile. + defer func() { + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + log.Fatalf("Writing memory profile: %v", err) + } + if err := f.Close(); err != nil { + log.Printf("Closing memory profile: %v", err) + } + log.Printf("Run: go tool pprof -sample_index=1 %s # (alloc_space)", *memProfile) + }() + } + if err := doCallgraph("", "", *algoFlag, *formatFlag, *testFlag, flag.Args()); err != nil { fmt.Fprintf(os.Stderr, "callgraph: %s\n", err) os.Exit(1) @@ -234,6 +265,9 @@ // Pre-canned formats. switch format { + case "": + return nil + case "digraph": format = `{{printf "%q %q" .Caller .Callee}}`
diff --git a/cmd/goyacc/yacc.go b/cmd/goyacc/yacc.go index 35f772a..3729739 100644 --- a/cmd/goyacc/yacc.go +++ b/cmd/goyacc/yacc.go
@@ -107,7 +107,7 @@ ) // output parser flags -const yyFlag = -1000 +const yyFlag = math.MinInt16 // parse tokens const ( @@ -132,7 +132,7 @@ const EMPTY = 1 const WHOKNOWS = 0 const OK = 1 -const NOMORE = -1000 +const NOMORE = math.MinInt16 // macros for getting associativity and precedence levels func ASSOC(i int) int { return i & 3 } @@ -2784,7 +2784,7 @@ } arout("R2", temp1, nprod) - aryfil(temp1, nstate, -1000) + aryfil(temp1, nstate, math.MinInt16) for i = 0; i <= ntokens; i++ { for j := tstates[i]; j != 0; j = mstates[j] { temp1[j] = i @@ -3257,7 +3257,7 @@ return &$$ParserImpl{} } -const $$Flag = -1000 +const $$Flag = -32768 func $$Tokname(c int) string { if c >= 1 && c-1 < len($$Toknames) {
diff --git a/cmd/stress/stress.go b/cmd/stress/stress.go index fd68acd..7f72594 100644 --- a/cmd/stress/stress.go +++ b/cmd/stress/stress.go
@@ -143,10 +143,7 @@ n := started.Load() - int64(runs) if *flagCount > 0 { // started counts past *flagCount at end; do not count those - // TODO: n = min(n, int64(*flagCount-runs)) - if x := int64(*flagCount - runs); n > x { - n = x - } + n = min(n, int64(*flagCount-runs)) } if n > 0 { active = fmt.Sprintf(", %d active", n)
diff --git a/go.mod b/go.mod index f3f1ee0..afba373 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.35.0 - golang.org/x/net v0.53.0 - golang.org/x/sync v0.20.0 - golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa + 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 ) -require golang.org/x/sys v0.43.0 // indirect +require golang.org/x/sys v0.46.0 // indirect
diff --git a/go.sum b/go.sum index d8cc2ec..8c315fa 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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +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=
diff --git a/go/analysis/internal/checker/fix_test.go b/go/analysis/internal/checker/fix_test.go index 984a410..59efad8 100644 --- a/go/analysis/internal/checker/fix_test.go +++ b/go/analysis/internal/checker/fix_test.go
@@ -607,7 +607,7 @@ }, } -// panics asserts that f() panics with with a value whose printed form matches the regexp want. +// panics asserts that f() panics with a value whose printed form matches the regexp want. func panics(t *testing.T, want string, f func()) { defer func() { if x := recover(); x == nil {
diff --git a/go/analysis/passes/composite/composite.go b/go/analysis/passes/composite/composite.go index ed2284e..f80e393 100644 --- a/go/analysis/passes/composite/composite.go +++ b/go/analysis/passes/composite/composite.go
@@ -10,6 +10,7 @@ "fmt" "go/ast" "go/types" + "slices" "strings" "golang.org/x/tools/go/analysis" @@ -49,108 +50,88 @@ Analyzer.Flags.BoolVar(&whitelist, "whitelist", whitelist, "use composite white list; for testing only") } -// runUnkeyedLiteral checks if a composite literal is a struct literal with -// unkeyed fields. func run(pass *analysis.Pass) (any, error) { inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - nodeFilter := []ast.Node{ - (*ast.CompositeLit)(nil), - } - inspect.Preorder(nodeFilter, func(n ast.Node) { - cl := n.(*ast.CompositeLit) + for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) { + complit := curLit.Node().(*ast.CompositeLit) - typ := pass.TypesInfo.Types[cl].Type - if typ == nil { - // cannot determine composite literals' type, skip it - return + // Skip empty or partly/fully keyed literals. + if len(complit.Elts) == 0 || + slices.ContainsFunc(complit.Elts, func(e ast.Expr) bool { return is[*ast.KeyValueExpr](e) }) { + continue } + + // Find struct type. + // (For a type parameter, choose an arbitrary term.) + typ := pass.TypesInfo.Types[complit].Type + if typ == nil { + continue // no type info + } + terms, err := typeparams.NormalTerms(typ) + if err != nil || len(terms) == 0 { + continue // invalid or empty type + } + t := terms[0].Type() + strct, ok := typeparams.Deref(t).Underlying().(*types.Struct) + if !ok { + continue // not a struct literal + } + if isSamePackageType(pass, t) { + continue // allow unkeyed literals for structs in same package + } + + // Allow whitelisted types. typeName := typ.String() if whitelist && unkeyedLiteral[typeName] { - // skip whitelisted types - return - } - var structuralTypes []types.Type - switch typ := types.Unalias(typ).(type) { - case *types.TypeParam: - terms, err := typeparams.StructuralTerms(typ) - if err != nil { - return // invalid type - } - for _, term := range terms { - structuralTypes = append(structuralTypes, term.Type()) - } - default: - structuralTypes = append(structuralTypes, typ) + continue } - for _, typ := range structuralTypes { - strct, ok := typeparams.Deref(typ).Underlying().(*types.Struct) - if !ok { - // skip non-struct composite literals - continue - } - if isLocalType(pass, typ) { - // allow unkeyed locally defined composite literal - continue - } - - // check if the struct contains an unkeyed field - allKeyValue := true - var suggestedFixAvailable = len(cl.Elts) == strct.NumFields() - var missingKeys []analysis.TextEdit - for i, e := range cl.Elts { - if _, ok := e.(*ast.KeyValueExpr); !ok { - allKeyValue = false - if i >= strct.NumFields() { - break - } - field := strct.Field(i) - if !field.Exported() { - // Adding unexported field names for structs not defined - // locally will not work. - suggestedFixAvailable = false - break - } - missingKeys = append(missingKeys, analysis.TextEdit{ - Pos: e.Pos(), - End: e.Pos(), - NewText: fmt.Appendf(nil, "%s: ", field.Name()), - }) + // If there is one value per field, + // offer to fill in the field names. + var fixes []analysis.SuggestedFix + if len(complit.Elts) == strct.NumFields() { + var edits []analysis.TextEdit + for i, elt := range complit.Elts { + field := strct.Field(i) + // We cannot fill in the name of an + // exported field from another package. + if !field.Exported() { + edits = nil + break } + edits = append(edits, analysis.TextEdit{ + Pos: elt.Pos(), + End: elt.Pos(), + NewText: fmt.Appendf(nil, "%s: ", field.Name()), + }) } - if allKeyValue { - // all the struct fields are keyed - continue - } - - diag := analysis.Diagnostic{ - Pos: cl.Pos(), - End: cl.End(), - Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName), - } - if suggestedFixAvailable { - diag.SuggestedFixes = []analysis.SuggestedFix{{ + if edits != nil { + fixes = []analysis.SuggestedFix{{ Message: "Add field names to struct literal", - TextEdits: missingKeys, + TextEdits: edits, }} } - pass.Report(diag) - return } - }) + + pass.Report(analysis.Diagnostic{ + Pos: complit.Pos(), + End: complit.End(), + Message: fmt.Sprintf("%s struct literal uses unkeyed fields", typeName), + SuggestedFixes: fixes, + }) + } return nil, nil } -// isLocalType reports whether typ belongs to the same package as pass. -// TODO(adonovan): local means "internal to a function"; rename to isSamePackageType. -func isLocalType(pass *analysis.Pass, typ types.Type) bool { +// isSamePackageType reports whether typ belongs to the same package as pass. +func isSamePackageType(pass *analysis.Pass, typ types.Type) bool { switch x := types.Unalias(typ).(type) { case *types.Struct: // struct literals are local types return true case *types.Pointer: - return isLocalType(pass, x.Elem()) + return isSamePackageType(pass, x.Elem()) case interface{ Obj() *types.TypeName }: // *Named or *TypeParam (aliases were removed already) // names in package foo are local to foo_test too return x.Obj().Pkg() != nil && @@ -158,3 +139,8 @@ } return false } + +func is[T any](x any) bool { + _, ok := x.(T) + return ok +}
diff --git a/go/analysis/passes/directive/directive.go b/go/analysis/passes/directive/directive.go index 5fa2886..c8619fa 100644 --- a/go/analysis/passes/directive/directive.go +++ b/go/analysis/passes/directive/directive.go
@@ -139,7 +139,7 @@ inStar = false continue } - line, inStar = stringsCutPrefix(line, "/*") + line, inStar = strings.CutPrefix(line, "/*") if !inStar { break } @@ -194,11 +194,3 @@ } } } - -// Go 1.20 strings.CutPrefix. -func stringsCutPrefix(s, prefix string) (after string, found bool) { - if !strings.HasPrefix(s, prefix) { - return s, false - } - return s[len(prefix):], true -}
diff --git a/go/analysis/passes/errorsas/errorsas.go b/go/analysis/passes/errorsas/errorsas.go index 36076cd..eb4373d 100644 --- a/go/analysis/passes/errorsas/errorsas.go +++ b/go/analysis/passes/errorsas/errorsas.go
@@ -23,7 +23,7 @@ For example: var unwrappedErr net.DNSError - errors.As(err, unwrappedErr) // should use &unwrappedErr, DNSError.Error has a pointer reciever + errors.As(err, unwrappedErr) // should use &unwrappedErr, DNSError.Error has a pointer receiver ` var Analyzer = &analysis.Analyzer{
diff --git a/go/analysis/passes/inline/doc.go b/go/analysis/passes/inline/doc.go index 8b817a7..fea596b 100644 --- a/go/analysis/passes/inline/doc.go +++ b/go/analysis/passes/inline/doc.go
@@ -10,7 +10,12 @@ inline: apply fixes based on 'go:fix inline' comment directives -The inline analyzer inlines functions and constants that are marked for inlining. +The inline analyzer inlines functions, constants, and type aliases +that are marked for inlining. + +Use this command to apply (just) inline fixes en masse: + + $ go fix -inline ./... ## Functions @@ -61,12 +66,6 @@ func(){...}(). However, the inline analyzer discards all such "literalizations" unconditionally, again on grounds of style.) -A call to a function F from its dedicated test (TestF) is not inlined, -since the purpose of the test is to exercise F itself, even when -it's a deprecated function to which other calls should be inlined. -This is not true for type aliases; see https://go.dev/issue/79271. -See further discussion in https://go.dev/issue/79272. - ## Constants Given a constant that is marked for inlining, like this one: @@ -96,14 +95,30 @@ //go:fix inline const ( Ptr = Pointer - Val = Value + Val = Value ) -The proposal https://go.dev/issue/32816 introduces the "//go:fix inline" directives. +## Type aliases -You can use this command to apply inline fixes en masse: +Similar to named constants, a type alias can also be marked for inlining: - $ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./... + //go:fix inline + type A = newpkg.A + +The analyzer will replace all references to the annotated type +(A) by the type on the right-hand side of the declaration (newpkg.A). + +## Tests + +A use of a function, named constant, or type alias X from its +dedicated test (TestX), is not inlined, since the purpose of the test +is to exercise X itself, even if it is deprecated and other uses of it +should be inlined. +This applies to benchmarks and examples too, and follows the usual +conventions of test function naming. + +Similarly, if the symbol X is declared in a file named foo.go, any use +of it within a file named foo_test.go will also not be inlined. # Analyzer gofixdirective
diff --git a/go/analysis/passes/inline/inline.go b/go/analysis/passes/inline/inline.go index 890f4ad..d16e0d0 100644 --- a/go/analysis/passes/inline/inline.go +++ b/go/analysis/passes/inline/inline.go
@@ -150,11 +150,11 @@ a.inlineCall(n, cur) case *ast.Ident: - switch t := a.pass.TypesInfo.Uses[n].(type) { + switch obj := a.pass.TypesInfo.Uses[n].(type) { case *types.TypeName: - a.inlineAlias(t, cur) + a.inlineAlias(obj, cur) case *types.Const: - a.inlineConst(t, cur) + a.inlineConst(obj, cur) } } } @@ -252,11 +252,22 @@ } } -// withinTestOf reports whether cur is within a dedicated test -// function for the inlinable target function. +// withinTestOf reports whether curUse is within a dedicated test +// function for the inlinable target symbol. // A call within its dedicated test should not be inlined. -func (a *analyzer) withinTestOf(cur inspector.Cursor, target *types.Func) bool { - curFuncDecl, ok := moreiters.First(cur.Enclosing((*ast.FuncDecl)(nil))) +func (a *analyzer) withinTestOf(curUse inspector.Cursor, target types.Object) bool { + // x_test.go -> x + useFileBase, isTest := strings.CutSuffix(a.pass.Fset.File(curUse.Node().Pos()).Name(), "_test.go") + if !isTest { + return false // not a test file + } + + // Suppress fixes for uses in x_test.go of target symbol defined in x.go (#79272). + if useFileBase == strings.TrimSuffix(a.pass.Fset.File(target.Pos()).Name(), ".go") { + return true + } + + curFuncDecl, ok := moreiters.First(curUse.Enclosing((*ast.FuncDecl)(nil))) if !ok { return false // not in a function } @@ -267,23 +278,22 @@ if strings.TrimSuffix(a.pass.Pkg.Path(), "_test") != target.Pkg().Path() { return false // different package } - if !strings.HasSuffix(a.pass.Fset.File(funcDecl.Pos()).Name(), "_test.go") { - return false // not a test file - } - // Computed expected SYMBOL portion of "TestSYMBOL_comment" - // for the target symbol. - symbol := target.Name() - if recv := target.Signature().Recv(); recv != nil { - _, named := typesinternal.ReceiverNamed(recv) - symbol = named.Obj().Name() + "_" + symbol - } - + // Computed expected SYMBOL portion of "ExampleSYMBOL_comment" + // for the target symbol. (Strictly, this convention applies + // only to Example functions.) // TODO(adonovan): use a proper Test function parser. + symbol := target.Name() + if fn, ok := target.(*types.Func); ok { + if recv := fn.Signature().Recv(); recv != nil { + _, named := typesinternal.ReceiverNamed(recv) + symbol = named.Obj().Name() + "_" + symbol + } + } fname := funcDecl.Name.Name - for _, pre := range []string{"Test", "Example", "Bench"} { + for _, pre := range []string{"Test", "Example", "Bench", "Fuzz"} { if fname == pre+symbol || strings.HasPrefix(fname, pre+symbol+"_") { - return true + return true // use of X within TestX } } @@ -304,6 +314,10 @@ return // nope } + if a.withinTestOf(curId, tn) { + return // don't inline a type alias from within its own test + } + alias := tn.Type().(*types.Alias) // Remember the names of the alias's type params. When we check for shadowing // later, we'll ignore these because they won't appear in the replacement text. @@ -508,6 +522,10 @@ return // nope } + if a.withinTestOf(cur, con) { + return // don't inline a type alias from within its own test + } + // If n is qualified by a package identifier, we'll need the full selector expression. curFile := astutil.EnclosingFile(cur) n := cur.Node().(*ast.Ident)
diff --git a/go/analysis/passes/inline/testdata/src/issue76190.txtar b/go/analysis/passes/inline/testdata/src/issue76190.txtar index 7058911..7288253 100644 --- a/go/analysis/passes/inline/testdata/src/issue76190.txtar +++ b/go/analysis/passes/inline/testdata/src/issue76190.txtar
@@ -1,7 +1,11 @@ This test checks that calls are not inlined when the call appears in a specific test of the function. (Even deprecated functions deserve tests.) -Variants: +A test is specific if either: +- it is named for the symbol (e.g TestF -> F) (#76190), or +- its filename a_test.go matches the declaring file a.go (#79272). + +Symbol name variants: - functions (TestF) vs methods (TestT_F) - optional comment suffixes (TestSYMBOL_comment) - in-package test vs external test @@ -24,14 +28,36 @@ //go:fix inline func (T) G() { print("T.G") } // want G:`goFixInline \(a.T\).G` +//go:fix inline +type A = T // want A:`goFixInline alias` + +//go:fix inline +const K = One // want K:`goFixInline const "example.com/a".One` + +const One = 1 + -- a/a_test.go -- package a import "testing" -func TestF(t *testing.T) { - F() // not inlined - G() // want "Call of a.G should be inlined" +func TestWithAnyName(t *testing.T) { + F() // not inlined + G() // not inlined + print(K) // not inlined + var _ A // not inlined +} + +-- a/other_test.go -- +package a + +import "testing" + +func Test(t *testing.T) { + F() // want "Call of a.F should be inlined" + G() // want "Call of a.G should be inlined" + print(K) // want "Constant K should be inlined" + var _ A // want "Type alias A should be inlined" } func TestG_comment(t *testing.T) { @@ -49,6 +75,14 @@ T(0).G() // not inlined } +func TestK(t *testing.T) { + print(K) // not inlined +} + +func TestA(t *testing.T) { + var _ A // not inlined +} + -- a/a_test.go.golden -- package a @@ -74,6 +108,41 @@ T(0).G() // not inlined } +-- a/other_test.go.golden -- +package a + +import "testing" + +func Test(t *testing.T) { + print("F") // want "Call of a.F should be inlined" + print("G") // want "Call of a.G should be inlined" + print(One) // want "Constant K should be inlined" + var _ T // want "Type alias A should be inlined" +} + +func TestG_comment(t *testing.T) { + print("F") // want "Call of a.F should be inlined" + G() // not inlined +} + +func TestT_F(t *testing.T) { + T(0).F() // not inlined + print("T.G") // want `Call of \(a.T\).G should be inlined` +} + +func TestT_G(t *testing.T) { + print("T.F") // want `Call of \(a.T\).F should be inlined` + T(0).G() // not inlined +} + +func TestK(t *testing.T) { + print(K) // not inlined +} + +func TestA(t *testing.T) { + var _ A // not inlined +} + -- a/a_x_test.go -- package a_test
diff --git a/go/analysis/passes/modernize/embedlit.go b/go/analysis/passes/modernize/embedlit.go index f687239..da883d6 100644 --- a/go/analysis/passes/modernize/embedlit.go +++ b/go/analysis/passes/modernize/embedlit.go
@@ -5,9 +5,12 @@ package modernize import ( + "bytes" "fmt" "go/ast" + "go/token" "go/types" + "slices" "strings" "golang.org/x/tools/go/analysis" @@ -15,8 +18,10 @@ "golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/inspector" "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/moreiters" + "golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/versions" ) @@ -25,96 +30,369 @@ Doc: analyzerutil.MustExtractDoc(doc, "embedlit"), Requires: []*analysis.Analyzer{ inspect.Analyzer, + typeindexanalyzer.Analyzer, }, Run: runEmbedLit, URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#embedlit", } -// TODO(mkalil): Handle other patterns such as: -// t := T{...} -// t.x = x -// ... -// => -// t := T{..., x: x, ...} +// Go1.27 introduced the ability to directly access embedded struct fields. +// The embedlit modernizer suggests two types of fixes that use this feature: +// 1. Removing redundant field type specifiers in embedded struct fields. +// 2. Moving embedded struct field assignments inside of the struct literal +// initialization. func runEmbedLit(pass *analysis.Pass) (any, error) { var ( inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) info = pass.TypesInfo ) for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) { - var ( - edits []analysis.TextEdit - names []string // names of the embedded field types that can be removed - compLit = curLit.Node().(*ast.CompositeLit) - compLitType = info.TypeOf(compLit) - check func(*ast.CompositeLit) - ) - check = func(lit *ast.CompositeLit) { - for _, elt := range lit.Elts { - // Can't promote an unkeyed field; would result in a syntax error. - if kv, ok := elt.(*ast.KeyValueExpr); ok { - if innerLit := isEmbeddedFieldLit(info, compLitType, kv); innerLit != nil { - // Emit edits to delete the unnecessary embedded field type specifier - // and its closing brace. - closingPos := innerLit.Rbrace - if len(innerLit.Elts) > 0 { - // Delete any inner trailing commas or white space. Extra trailing commas - // would result in invalid code. - closingPos = innerLit.Elts[len(innerLit.Elts)-1].End() - } - file := astutil.EnclosingFile(curLit) - // Enable modernizer only for Go1.27. - if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) { - return - } - // If any comments overlap with the range to delete, don't suggest a fix. - if !moreiters.Empty(astutil.Comments(file, closingPos, innerLit.Rbrace+1)) { - continue - } - edits = append(edits, []analysis.TextEdit{ - // T{U: U{f: v, ...}} - // ----- - - { - // Delete the key and the opening brace of the inner struct literal. - Pos: kv.Pos(), - End: innerLit.Lbrace + 1, - }, - { - // Delete the corresponding closing brace, including preceding - // white space or commas. Failing to delete trailing commas may - // result in invalid code. - Pos: closingPos, - End: innerLit.Rbrace + 1, - }, - }...) - names = append(names, kv.Key.(*ast.Ident).Name) - check(innerLit) - } + if curLit.ParentEdgeKind() != edge.KeyValueExpr_Value { // non-nested comp lit + // TODO(mkalil): Figure out how to handle addition/removal of commas in + // the comp lit when we observe code where both patterns apply. (This will + // likely require a significant amount of work). For now, only apply edits + // from one pattern at a time. + if !embedlitUnnest(pass, info, curLit) { + err := embedlitCombine(pass, index, info, curLit) // calls pass.ReadFile + if err != nil { + return nil, err } } } - - if curLit.ParentEdgeKind() != edge.KeyValueExpr_Value { - compLit := curLit.Node().(*ast.CompositeLit) - check(compLit) // non-nested comp lit - } - if len(edits) > 0 { - pass.Report(analysis.Diagnostic{ - Pos: curLit.Node().Pos(), - End: curLit.Node().End(), - Message: "embedded field type can be removed from struct literal", - SuggestedFixes: []analysis.SuggestedFix{ - { - Message: fmt.Sprintf("Remove embedded field type%s %s", cond(len(names) == 1, "", "s"), strings.Join(names, ", ")), - TextEdits: edits, - }, - }, - }) - } } return nil, nil } +// Pattern A: removing unneeded embedded field type specifier from the struct +// literal. +// T{U: U{f: v, ...}} => T{f: v, ...} +// It returns true if it reported a diagnostic with edits. +func embedlitUnnest(pass *analysis.Pass, info *types.Info, curLit inspector.Cursor) bool { + var ( + edits []analysis.TextEdit + names []string // names of the embedded field types that can be removed + lit = curLit.Node().(*ast.CompositeLit) + compLitType = info.TypeOf(lit) + ) + + // checkLit determines whether any of the fields in the given struct literal can + // be promoted, and calculates the corresponding edits. + var checkLit func(lit *ast.CompositeLit) + checkLit = func(lit *ast.CompositeLit) { + for i, elt := range lit.Elts { + // Can't promote an unkeyed field; would result in a syntax error. + if kv, ok := elt.(*ast.KeyValueExpr); ok { + if innerLit := isEmbeddedFieldLit(info, compLitType, kv); innerLit != nil { + // Emit edits to delete the unnecessary embedded field type specifier + // and its closing brace. + closingPos := innerLit.Rbrace + if len(innerLit.Elts) > 0 { + // Delete any inner trailing commas or white space. Extra trailing commas + // would result in invalid code. + closingPos = innerLit.Elts[len(innerLit.Elts)-1].End() + } + file := astutil.EnclosingFile(curLit) + // Enable modernizer only for Go1.27. + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) { + return + } + // If any comments overlap with the range to delete, don't suggest a fix. + if !moreiters.Empty(astutil.Comments(file, kv.Pos(), innerLit.Lbrace+1)) || + !moreiters.Empty(astutil.Comments(file, closingPos, innerLit.Rbrace+1)) { + continue + } + // Delete starting from the key to the character right after the + // opening brace of the inner literal: + // T{U: U{f: v, ...}} + // ----- + startPos := kv.Pos() + endPos := innerLit.Lbrace + 1 + + // Delete the entire line if the key and its opening brace are + // together on their own line. This prevents leaving behind unneeded + // blank lines inside struct literals that `gofmt` will not remove. + // T{ + // U: U{ <- delete entire line + // f: v, + // } + // } + // + tokFile := pass.Fset.File(kv.Pos()) + lineOf := func(pos token.Pos) int { + return tokFile.PositionFor(pos, false).Line + } + curLine := lineOf(kv.Pos()) + var prevLine int + if i == 0 { + // First element, so the previous line is the parent lbrace. + prevLine = lineOf(lit.Lbrace) + } else { + prevLine = lineOf(lit.Elts[i-1].End()) + } + + // We can safely delete the entire line if the key value expression is + // on a different line than the previous element, and the closing + // brace of the inner literal is on a different line than its opening + // brace. + if prevLine < curLine && curLine < tokFile.LineCount() && // (1-based) + lineOf(innerLit.Lbrace) < lineOf(innerLit.Rbrace) { + lineStart := tokFile.LineStart(curLine) + nextLineStart := tokFile.LineStart(curLine + 1) + // Check that there are no comments on the line we are going to delete. + if moreiters.Empty(astutil.Comments(file, lineStart, nextLineStart)) { + startPos = tokFile.LineStart(curLine) + endPos = nextLineStart + } + } + + edits = append(edits, []analysis.TextEdit{ + // T{U: U{f: v, ...}} + // ----- - + { + // Delete the key and the opening brace of the inner struct literal. + Pos: startPos, + End: endPos, + }, + { + // Delete the corresponding closing brace, including preceding + // white space or commas. Failing to delete trailing commas may + // result in invalid code. + Pos: closingPos, + End: innerLit.Rbrace + 1, + }, + }...) + names = append(names, kv.Key.(*ast.Ident).Name) + checkLit(innerLit) + } + } + } + } + checkLit(lit) + if len(edits) > 0 { + pass.Report(analysis.Diagnostic{ + Pos: curLit.Node().Pos(), + End: curLit.Node().End(), + Message: "embedded field type can be removed from struct literal", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: fmt.Sprintf("Remove embedded field type%s %s", cond(len(names) == 1, "", "s"), strings.Join(names, ", ")), + TextEdits: edits, + }, + }, + }) + return true + } + return false +} + +// Pattern B: moving embedded field assignments inside the struct literal +// initialization. +// t := T{...}; t.x = x => t := T{..., x: x} +// (or var t = ...) +func embedlitCombine(pass *analysis.Pass, index *typeindex.Index, info *types.Info, curLit inspector.Cursor) error { + compLit := curLit.Node().(*ast.CompositeLit) + if !moreiters.Every(slices.Values(compLit.Elts), func(e ast.Expr) bool { + return is[*ast.KeyValueExpr](e) + }) { + // Promoting additional embedded fields would result in mixing keyed and + // unkeyed fields, which isn't allowed. + return nil + } + var ( + // Ident for "t" in the assignment. + lhs *ast.Ident + // The cursor representing the statement that initializes the comp lit "t". + // We use its siblings to search for field assignments and verify that there + // are no intervening statements, in case those statements observe "t". + curStmt inspector.Cursor + ) + switch curLit.ParentEdgeKind() { + 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 { + lhs = id + curStmt = curLit.Parent() + } + case edge.ValueSpec_Values: + spec := curLit.Parent().Node().(*ast.ValueSpec) + lhs = spec.Names[curLit.ParentEdgeIndex()] + if decl, ok := moreiters.First(curLit.Enclosing((*ast.DeclStmt)(nil))); ok { + curStmt = decl + } + default: + return nil + } + + if lhs == nil || !curStmt.Valid() { + return nil + } + + var ( + 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 + ) +stmtloop: + for { + var ok bool + curStmt, ok = curStmt.NextSibling() + if !ok { + break // end of (e.g.) block + } + // All embedded field value assignments must immediately follow the struct + // initialization. + assign, ok := curStmt.Node().(*ast.AssignStmt) + if !ok || len(assign.Lhs) != 1 || !(assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) { + // TODO(mkalil): handle multi-assignments like t.x, t.y = 1, 2 + break + } + expr := assign.Lhs[0] + sel, ok := expr.(*ast.SelectorExpr) + if !ok { + break + } + // Verify that sel.X refers to the same object as "t" + selXId, ok := sel.X.(*ast.Ident) + if !ok { + // TODO(mkalil): handle deeply nested expressions like t.B.x + break + } + obj := info.ObjectOf(selXId) + if obj != tObj { + break + } + rhsCur := curStmt.ChildAt(edge.AssignStmt_Rhs, 0) + if uses(index, rhsCur, tObj) { + break + } + for c := range rhsCur.Preorder((*ast.Ident)(nil)) { + id := c.Node().(*ast.Ident) + // If the rhs uses a value of t (e.g. t.x = t.y), don't suggest a fix because + // we can't evaluate t.y when constructing the new literal. + if info.ObjectOf(id) == tObj { + break stmtloop + } + // Note: we don't need to worry about expressions with side effects + // changing the behavior when moved inside the comp lit. The order of + // effects will be preserved because we preserve the order of the key + // value pairs inside the comp lit. + } + if !firstStmt.Valid() { + firstStmt = curStmt + } + lastStmt = curStmt + } + + if !firstStmt.Valid() { + return nil + } + + file := astutil.EnclosingFile(curLit) + // Enable modernizer only for Go1.27. + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) { + return nil + } + + // Read file content to determine if the struct lit has a trailing comma + // after its last element. + tokFile := pass.Fset.File(compLit.Rbrace) + filename := tokFile.Name() + src, err := pass.ReadFile(filename) + if err != nil { + return err + } + + hasTrailingComma := false + if len(compLit.Elts) > 0 { + lastElt := compLit.Elts[len(compLit.Elts)-1] + lastEltOffset := tokFile.Offset(lastElt.End()) + rbraceOffset := tokFile.Offset(compLit.Rbrace) + hasTrailingComma = bytes.Contains(src[lastEltOffset:rbraceOffset], []byte(",")) + } + var edits []analysis.TextEdit + // Emit edits to move the field assignment into the struct lit while + // removing it from its current place. + // t := T{...}; t.x = v + // ----- --- - + // t := T{..., x: v} + + // Add a trailing comma before the closing brace of compLit if one doesn't + // exist, and delete the closing brace itself. + // t := T{...}; t.x = v + // - + // t := T{..., t.x = v + if len(compLit.Elts) > 0 && !hasTrailingComma { + edits = append(edits, analysis.TextEdit{ + Pos: compLit.Rbrace, + End: compLit.Rbrace + 1, + NewText: []byte(","), + }) + } else { + edits = append(edits, analysis.TextEdit{ + Pos: compLit.Rbrace, + End: compLit.Rbrace + 1, + }) + } + + // For each assignment: + // t.x = v + // -- --- + // x : v + curStmt = firstStmt + var prevStmt inspector.Cursor + for { + assign := curStmt.Node().(*ast.AssignStmt) + expr := assign.Lhs[0] + sel := expr.(*ast.SelectorExpr) + // Delete "t." + edits = append(edits, analysis.TextEdit{ + Pos: assign.Pos(), + End: sel.Sel.Pos(), + }) + // Replace "=" with ":" + edits = append(edits, analysis.TextEdit{ + Pos: expr.End(), + End: assign.TokPos + 1, + NewText: []byte(":"), + }) + + // Add a comma after the previous assignment if this is not the first one. + if prevStmt.Valid() { + edits = append(edits, analysis.TextEdit{ + Pos: prevStmt.Node().End(), + NewText: []byte(","), + }) + } + + // For the last assignment, add the closing brace of the struct lit. + if curStmt == lastStmt { + edits = append(edits, analysis.TextEdit{ + Pos: assign.End(), + NewText: []byte("}"), + }) + break + } + prevStmt = curStmt + curStmt, _ = curStmt.NextSibling() // can't fail because we break out of the loop when we hit lastStmt + } + + pass.Report(analysis.Diagnostic{ + Pos: curLit.Node().Pos(), + End: curLit.Node().End(), + Message: "embedded field assignment can be moved to struct literal", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Move embedded field assignment to struct literal", + TextEdits: edits, + }, + }, + }) + return nil +} + // isEmbeddedFieldLit determines whether elt is a KeyValueExpr "T: T{...}" for // an embedded field for which we can safely remove its type. // If so, it returns the corresponding CompositeLit. @@ -125,7 +403,8 @@ return nil } lit, ok := kv.Value.(*ast.CompositeLit) - if !ok { + if !ok || len(lit.Elts) == 0 { + // Skip if the struct literal is empty. return nil } // We cannot remove this type if any of its nested composite elements have @@ -147,6 +426,7 @@ // type B struct { x int } // _ = T{A: A{x: 1}} // cannot be simplified to T{x: 1} because T has two different embedded fields called "x". + // We also reject composite literals with slice elements, as parentObj will be nil. parentObj, _, _ := types.LookupFieldOrMethod(topLevelType, true, obj.Pkg(), k.Name) if parentObj != obj { return nil
diff --git a/go/analysis/passes/modernize/errorsastype.go b/go/analysis/passes/modernize/errorsastype.go index c5d3063..0e3f17f 100644 --- a/go/analysis/passes/modernize/errorsastype.go +++ b/go/analysis/passes/modernize/errorsastype.go
@@ -17,6 +17,7 @@ "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/moreiters" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" @@ -31,7 +32,7 @@ Run: errorsastype, } -// errorsastype offers a fix to replace error.As with the newer +// errorsastype offers a fix to replace errors.As with the newer // errors.AsType[T] following this pattern: // // var myerr *MyErr @@ -54,22 +55,15 @@ // // because the transformation in that case would be ungainly. // +// For the negated case (!errors.As), we use !ok instead. +// // Note that the cmd/vet suite includes the "errorsas" analyzer, which // detects actual mistakes in the use of errors.As. This logic does // not belong in errorsas because the problems it fixes are merely // stylistic. // // TODO(adonovan): support more cases: -// -// - Negative cases -// var myerr E -// if !errors.As(err, &myerr) { ... } -// => -// myerr, ok := errors.AsType[E](err) -// if !ok { ... } -// // - if myerr := new(E); errors.As(err, myerr); { ... } -// // - if errors.As(err, myerr) && othercond { ... } func errorsastype(pass *analysis.Pass) (any, error) { var ( @@ -83,7 +77,7 @@ continue // spread call: errors.As(pair()) } - v, curDeclStmt := canUseErrorsAsType(info, index, curCall) + v, curDeclStmt, curIfStmt := canUseErrorsAsType(info, index, curCall) if v == nil { continue } @@ -121,64 +115,82 @@ // Choose a name for the "ok" variable. // We generate a new name only if 'ok' is already declared at // curCall and it also used within the if-statement. - curIf := curCall.Parent() - ifScope := info.Scopes[curIf.Node().(*ast.IfStmt)] - okName := freshName(info, index, ifScope, v.Pos(), curCall, curIf, token.NoPos, "ok") + ifScope := info.Scopes[curIfStmt.Node().(*ast.IfStmt)] + negated := curCall.ParentEdgeKind() == edge.UnaryExpr_X // bool => Tok==NOT + okName := freshName(info, index, ifScope, v.Pos(), curCall, curIfStmt, token.NoPos, "ok") + // Because we reject any use of v outside the if statement, any use besides + // the argument in errors.As must lie inside the if statement. + usesV := moreiters.Len(index.Uses(v)) > 1 + + edits := append( + // delete "var myerr *MyErr" + refactor.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt), + // if errors.As (err, &myerr) { ... } + // ------------- -------------- -------- ---- + // if myerr, ok := errors.AsType[*MyErr](err ); ok { ... } + analysis.TextEdit{ + // Insert "myerr, ok := " if myerr is used inside the if statement. + // Otherwise insert "_, ok := ". + Pos: call.Pos(), + End: call.Pos(), + NewText: fmt.Appendf(nil, "%s, %s := ", cond(usesV, v.Name(), "_"), okName), + }, + analysis.TextEdit{ + // replace As with AsType[T] + Pos: asIdent.Pos(), + End: asIdent.End(), + NewText: fmt.Appendf(nil, "AsType[%s]", errtype), + }, + analysis.TextEdit{ + // delete ", &myerr" + Pos: call.Args[0].End(), + End: call.Args[1].End(), + }, + analysis.TextEdit{ + // insert "; ok" for errors.AsType or "; !ok" for !errors.AsType + Pos: call.End(), + End: call.End(), + NewText: fmt.Appendf(nil, "; %s%s", cond(negated, "!", ""), okName), + }, + ) + if negated { + unaryExpr := curCall.Parent().Node().(*ast.UnaryExpr) + // delete "!" + edits = append(edits, analysis.TextEdit{ + Pos: unaryExpr.OpPos, + End: unaryExpr.X.Pos(), + }) + } pass.Report(analysis.Diagnostic{ Pos: call.Fun.Pos(), End: call.Fun.End(), Message: fmt.Sprintf("errors.As can be simplified using AsType[%s]", errtype), SuggestedFixes: []analysis.SuggestedFix{{ - Message: fmt.Sprintf("Replace errors.As with AsType[%s]", errtype), - TextEdits: append( - // delete "var myerr *MyErr" - refactor.DeleteStmt(pass.Fset.File(call.Fun.Pos()), curDeclStmt), - // if errors.As (err, &myerr) { ... } - // ------------- -------------- -------- ---- - // if myerr, ok := errors.AsType[*MyErr](err ); ok { ... } - analysis.TextEdit{ - // insert "myerr, ok := " - Pos: call.Pos(), - End: call.Pos(), - NewText: fmt.Appendf(nil, "%s, %s := ", v.Name(), okName), - }, - analysis.TextEdit{ - // replace As with AsType[T] - Pos: asIdent.Pos(), - End: asIdent.End(), - NewText: fmt.Appendf(nil, "AsType[%s]", errtype), - }, - analysis.TextEdit{ - // delete ", &myerr" - Pos: call.Args[0].End(), - End: call.Args[1].End(), - }, - analysis.TextEdit{ - // insert "; ok" - Pos: call.End(), - End: call.End(), - NewText: fmt.Appendf(nil, "; %s", okName), - }, - ), + Message: fmt.Sprintf("Replace errors.As with AsType[%s]", errtype), + TextEdits: edits, }}, }) } return nil, nil } -// canUseErrorsAsType reports whether curCall is a call to -// errors.As beneath an if statement, preceded by a -// declaration of the typed error var. The var must not be -// used outside the if statement. -func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspector.Cursor) (_ *types.Var, _ inspector.Cursor) { - if curCall.ParentEdgeKind() != edge.IfStmt_Cond { - return // not beneath if statement +// canUseErrorsAsType reports whether curCall is a call to errors.As beneath an +// if statement, preceded by a declaration of the typed error var. The var must +// not be used outside the if statement. +// If the conditions are met, it returns the error var, the cursor for its +// DeclStmt, and the cursor for the IfStmt that contains the call to errors.As. +// Otherwise it returns a nil error var. +func canUseErrorsAsType(info *types.Info, index *typeindex.Index, curCall inspector.Cursor) (_ *types.Var, curDeclStmt, curIfStmt inspector.Cursor) { + curCond := curCall + if curCond.ParentEdgeKind() == edge.UnaryExpr_X { // if !errors.As(err, &v) + curCond = curCond.Parent() } - var ( - curIfStmt = curCall.Parent() - ifStmt = curIfStmt.Node().(*ast.IfStmt) - ) + if curCond.ParentEdgeKind() != edge.IfStmt_Cond { + return // not beneath if or unaryexpr + } + curIfStmt = curCond.Parent() + ifStmt := curIfStmt.Node().(*ast.IfStmt) if ifStmt.Init != nil { return // if statement already has an init part } @@ -216,11 +228,16 @@ len(curDecl.Node().(*ast.GenDecl).Specs) != 1 { return // not a simple "var v T" decl } + // AsType requires that its type argument implements error. + // Reject if v does not implement error. + if !types.AssignableTo(v.Type(), errorType) { + return + } // Have: // var v *MyErr // ... // if errors.As(err, &v) { ... } // with no uses of v outside the IfStmt. - return v, curDecl.Parent() // DeclStmt + return v, curDecl.Parent(), curIfStmt // curDecl.Parent() is a DeclStmt }
diff --git a/go/analysis/passes/modernize/export_test.go b/go/analysis/passes/modernize/export_test.go new file mode 100644 index 0000000..0823331 --- /dev/null +++ b/go/analysis/passes/modernize/export_test.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. + +// This file exposes yet-unpublished analyzers to the tests. + +package modernize + +var ( + SlicesBackwardAnalyzer = slicesBackwardAnalyzer + UnsafeFuncsAnalyzer = unsafeFuncsAnalyzer +)
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go index 8049127..d7dfe55 100644 --- a/go/analysis/passes/modernize/modernize.go +++ b/go/analysis/passes/modernize/modernize.go
@@ -35,11 +35,8 @@ var Suite = []*analysis.Analyzer{ AnyAnalyzer, AtomicTypesAnalyzer, - // AppendClippedAnalyzer, // not nil-preserving! - // BLoopAnalyzer, // may skew benchmark results, see golang/go#74967 EmbedLitAnalyzer, ErrorsAsTypeAnalyzer, - // FmtAppendfAnalyzer, // makes code less clear, see golang/go#77581 ForVarAnalyzer, MapsLoopAnalyzer, MinMaxAnalyzer, @@ -48,9 +45,8 @@ PlusBuildAnalyzer, RangeIntAnalyzer, ReflectTypeForAnalyzer, - slicesBackwardAnalyzer, + slicesBackwardAnalyzer, // awaiting public symbol SlicesContainsAnalyzer, - // SlicesDeleteAnalyzer, // not nil-preserving! SlicesSortAnalyzer, StdIteratorsAnalyzer, StringsCutAnalyzer, @@ -58,8 +54,15 @@ StringsSeqAnalyzer, StringsBuilderAnalyzer, TestingContextAnalyzer, - unsafeFuncsAnalyzer, + unsafeFuncsAnalyzer, // awaiting public symbol WaitGroupGoAnalyzer, + + // Not included: + // + // AppendClippedAnalyzer, // not nil-preserving + // BLoopAnalyzer, // may skew benchmark results, see golang/go#74967 + // FmtAppendfAnalyzer, // makes code less clear, see golang/go#77581 + // SlicesDeleteAnalyzer, // not nil-preserving } // -- helpers -- @@ -136,6 +139,7 @@ builtinTrue = types.Universe.Lookup("true") byteSliceType = types.NewSlice(types.Typ[types.Byte]) omitemptyRegex = regexp.MustCompile(`(?:^json| json):"[^"]*(,omitempty)(?:"|,[^"]*")\s?`) + errorType = types.Universe.Lookup("error").Type() ) // lookup returns the symbol denoted by name at the position of the cursor.
diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go index 9532c0c..bc8ecfb 100644 --- a/go/analysis/passes/modernize/modernize_test.go +++ b/go/analysis/passes/modernize/modernize_test.go
@@ -9,7 +9,6 @@ . "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/modernize" - "golang.org/x/tools/internal/goplsexport" "golang.org/x/tools/internal/testenv" ) @@ -94,7 +93,7 @@ func TestSlicesBackward(t *testing.T) { testenv.NeedsGo1Point(t, 23) - RunWithSuggestedFixes(t, TestData(), goplsexport.SlicesBackwardModernizer, "slicesbackward") + RunWithSuggestedFixes(t, TestData(), modernize.SlicesBackwardAnalyzer, "slicesbackward") } func TestSlicesContains(t *testing.T) { @@ -132,7 +131,7 @@ } func TestUnsafeFuncs(t *testing.T) { - RunWithSuggestedFixes(t, TestData(), goplsexport.UnsafeFuncsModernizer, "unsafefuncs") + RunWithSuggestedFixes(t, TestData(), modernize.UnsafeFuncsAnalyzer, "unsafefuncs") } func TestWaitGroupGo(t *testing.T) {
diff --git a/go/analysis/passes/modernize/reflect.go b/go/analysis/passes/modernize/reflect.go index 959939b..10fbdf8 100644 --- a/go/analysis/passes/modernize/reflect.go +++ b/go/analysis/passes/modernize/reflect.go
@@ -17,7 +17,6 @@ "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" @@ -92,11 +91,20 @@ continue } + // Don't offer the fix if it would erase a reference to a + // (non-type) symbol, as this may break intended coupling. + // Examples: + // TypeOf(0) -> TypeFor[int]() // ok + // TypeOf(pkg.Var) -> TypeFor[int]() // bad: loses connection to pkg.Var + // TypeOf(uint(0)) -> TypeFor[uint]() // ok: (most) type symbols are preserved + if usesNonTypeSymbol(info, expr) { + continue + } + file := astutil.EnclosingFile(curCall) if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_22) { continue // TypeFor requires go1.22 } - tokFile := pass.Fset.File(file.Pos()) // Format the type as valid Go syntax. // TODO(adonovan): FileQualifier needs to respect @@ -127,14 +135,6 @@ continue } - // If the call argument contains the last use - // of a variable, as in: - // var zero T - // reflect.TypeOf(zero) - // remove the declaration of that variable. - curArg0 := curCall.ChildAt(edge.CallExpr_Args, 0) - edits = append(edits, refactor.DeleteUnusedVars(index, info, tokFile, curArg0)...) - pass.Report(analysis.Diagnostic{ Pos: call.Fun.Pos(), End: call.Fun.End(), @@ -163,6 +163,33 @@ return nil, nil } +// usesNonTypeSymbol reports whether expr uses a non-type symbol: +// a value-level object (a var, const, or func) or any other named entity +// whose identifier would disappear in a TypeFor replacement. We suppress +// the fix in that case so the rewrite does not erase a symbol the author +// named on purpose, for example reflect.TypeOf(f), reflect.TypeOf(x.Field), +// or a named array length such as [arrayLen]byte. +// +// Type names, package names, nil, and builtins are not such symbols: they +// either reappear in the type argument (e.g. T in reflect.TypeOf(T{})) or +// are irrelevant to it, so the rewrite is allowed. +func usesNonTypeSymbol(info *types.Info, expr ast.Expr) bool { + for n := range ast.Preorder(expr) { + id, ok := n.(*ast.Ident) + if !ok { + continue + } + switch info.Uses[id].(type) { + case *types.TypeName, *types.PkgName, *types.Nil, *types.Builtin: + // Type-level names reappear in (or are irrelevant to) the + // type argument, so they may be erased safely. + default: + return true + } + } + return false +} + // isComplicatedType reports whether type t is complicated, e.g. it is or contains an // unnamed struct, interface, or function signature. func isComplicatedType(t types.Type) bool {
diff --git a/go/analysis/passes/modernize/slicesbackward.go b/go/analysis/passes/modernize/slicesbackward.go index 305b5e5..02cd30a 100644 --- a/go/analysis/passes/modernize/slicesbackward.go +++ b/go/analysis/passes/modernize/slicesbackward.go
@@ -18,12 +18,12 @@ "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/goplsexport" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/versions" ) +// TODO(adonovan): needs a proposal for a public symbol. var slicesBackwardAnalyzer = &analysis.Analyzer{ Name: "slicesbackward", Doc: analyzerutil.MustExtractDoc(doc, "slicesbackward"), @@ -35,11 +35,6 @@ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesbackward", } -func init() { - // Export to gopls until this is a published modernizer. - goplsexport.SlicesBackwardModernizer = slicesBackwardAnalyzer -} - // slicesbackward offers a fix to replace a manually-written backward loop: // // for i := len(s) - 1; i >= 0; i-- {
diff --git a/go/analysis/passes/modernize/slicescontains.go b/go/analysis/passes/modernize/slicescontains.go index c1d4218..ed75e05 100644 --- a/go/analysis/passes/modernize/slicescontains.go +++ b/go/analysis/passes/modernize/slicescontains.go
@@ -19,6 +19,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typeparams" + "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" "golang.org/x/tools/internal/versions" ) @@ -58,12 +59,8 @@ // statement is "found = false" (or vice versa), the // loop becomes "found = [!]slices.Contains(...)". // -// It may change cardinality of effects of the "needle" expression. -// (Mostly this appears to be a desirable optimization, avoiding -// redundantly repeated evaluation.) -// -// TODO(adonovan): Add a check that needle/predicate expression from -// if-statement has no effects. Now the program behavior may change. +// It rejects candidates whose needle/predicate expression from the if-statement +// has side effects to avoid changes in program behavior. func slicescontains(pass *analysis.Pass) (any, error) { // Skip the analyzer in packages where its // fixes would create an import cycle. @@ -174,6 +171,11 @@ return } + // Reject if needle/predicate expression has side effects. + if !typesinternal.NoEffects(info, arg2) { + return + } + // Reject if the body, needle or predicate references either range variable. usesRangeVar := func(n ast.Node) bool { cur, ok := curRange.FindNode(n)
diff --git a/go/analysis/passes/modernize/stringscutprefix.go b/go/analysis/passes/modernize/stringscutprefix.go index ae63454..11d3359 100644 --- a/go/analysis/passes/modernize/stringscutprefix.go +++ b/go/analysis/passes/modernize/stringscutprefix.go
@@ -16,6 +16,7 @@ "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/moreiters" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/typesinternal/typeindex" @@ -217,34 +218,47 @@ call.Pos(), ) + // if x := strings.TrimPrefix(s, pre); x != s ... + // ---- ---------- ------ + // if x, ok := strings.CutPrefix (s, pre); ok ... + // (ditto Suffix) + edits := append(importEdits, []analysis.TextEdit{ + { + Pos: assign.Lhs[0].End(), + End: assign.Lhs[0].End(), + NewText: fmt.Appendf(nil, ", %s", okVarName), + }, + { + Pos: call.Fun.Pos(), + End: call.Fun.End(), + NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName), + }, + { + Pos: ifStmt.Cond.Pos(), + End: ifStmt.Cond.End(), + NewText: []byte(okVarName), + }, + }...) + + // Replace the "after" variable with "_" if is unused inside the if statement. + if id, ok := lhs.(*ast.Ident); ok { + if obj := info.ObjectOf(id); obj != nil && moreiters.Len(index.Uses(obj)) < 2 { + edits = append(edits, analysis.TextEdit{ + Pos: assign.Lhs[0].Pos(), + End: assign.Lhs[0].End(), + NewText: []byte("_"), + }) + } + } + pass.Report(analysis.Diagnostic{ // highlight from the init and the condition end. Pos: ifStmt.Init.Pos(), End: ifStmt.Cond.End(), Message: message, SuggestedFixes: []analysis.SuggestedFix{{ - Message: fixMessage, - // if x := strings.TrimPrefix(s, pre); x != s ... - // ---- ---------- ------ - // if x, ok := strings.CutPrefix (s, pre); ok ... - // (ditto Suffix) - TextEdits: append(importEdits, []analysis.TextEdit{ - { - Pos: assign.Lhs[0].End(), - End: assign.Lhs[0].End(), - NewText: fmt.Appendf(nil, ", %s", okVarName), - }, - { - Pos: call.Fun.Pos(), - End: call.Fun.End(), - NewText: fmt.Appendf(nil, "%s%s", prefix, cutFuncName), - }, - { - Pos: ifStmt.Cond.Pos(), - End: ifStmt.Cond.End(), - NewText: []byte(okVarName), - }, - }...), + Message: fixMessage, + TextEdits: edits, }}, }) }
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 f79de53..0b97443 100644 --- a/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go +++ b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go
@@ -55,6 +55,16 @@ type K struct{ L } type L []int +type T struct { + a int + b int + U +} + +type U struct { + x int +} + var ( _ = A{B: B{b: 1}} // want "embedded field type can be removed from struct literal" _ = A{a: 1, B: B{b: 1, C: C{c: 1}}} // want "embedded field type can be removed from struct literal" @@ -70,8 +80,9 @@ b: 1, }, } - // empty composite lit - _ = A{a: 1, B: B{}} // want "embedded field type can be removed from struct literal" + + _ = A{a: 1, B: B{}} // nope: empty composite lit + // don't suggest a fix if it's too tricky to preserve comments _ = A{ // nope: comments within range to delete B: B{ // one @@ -90,6 +101,108 @@ }, a: 2, } + _ = A{B: /* comment */ B{b: 1}} // nope: comment in range to delete _ = A{B: B{b: 1} /* comment */, a: 2} // want "embedded field type can be removed from struct literal" _ = K{L: L{zero: 0}} // nope: cannot promote slice elements + _ = K{L: L{0: 100}} // nope: cannot promote slice elements + + _ = A{ // want "embedded field type can be removed from struct literal" + B: B{ + C: C{ + c: 1, + }, + b: 2, + }, + a: 3, + } + + _ = A{B: B{C: C{c: 1}}} // want "embedded field type can be removed from struct literal" + + _ = A{B: B{ // want "embedded field type can be removed from struct literal" + C: C{ + c: 1, + }, + }} + + _ = A{ // want "embedded field type can be removed from struct literal" + B: B{C: C{c: 1}}, + } + + _ = A{ // want "embedded field type can be removed from struct literal" + B: B{ // comment here + C: C{ + c: 1, + }, + }, + } ) + +func _() { + t1 := A{a: 1} // want "embedded field assignment can be moved to struct literal" + t1.b = 2 + + var t2 A + t2 = A{a: 1} // want "embedded field assignment can be moved to struct literal" + t2.b = 2 + + var t3 = A{a: 1} // want "embedded field assignment can be moved to struct literal" + t3.b = 2 + + t4 := T{1, 2, U{x: 3}} // nope: can't mix keyed and unkeyed elements + t4.x = 4 + + t5 := A{a: 1} + _ = t5 // nope: intervening statement + t5.b = 2 + + t6 := A{a: 1} // nope: value assigned depends on t6 itself + t6.b = t6.a + 1 + + t7 := A{a: 1} // want "embedded field assignment can be moved to struct literal" + t7.b = foo() + + // Only apply edits from pattern A first even though both patterns apply. + t8 := A{ // want "embedded field type can be removed from struct literal" + B: B{b: 1}, + } + t8.a = 2 + + t9 := A{} // nope: multiple assignments not yet supported + t9.a, t9.b = 1, 2 + + t10 := A{} // want "embedded field assignment can be moved to struct literal" + t10.a = 1 // this comment is preserved + t10.b = 2 // this one too + + t11 := A{a: 1} // want "embedded field assignment can be moved to struct literal" + t11.b = 2 + t11.c = 3 + + t12 := A{} // want "embedded field assignment can be moved to struct literal" + t12.B = B{ + b: 1, // this comment is preserved + } + + t13 := A{} // want "embedded field assignment can be moved to struct literal" + t13.a = 1 + // comment between assignments + t13.b = 2 + + t14 := A{} // want "embedded field assignment can be moved to struct literal" + t14.a = 1 + t14.b = + foo() + + 1 + + t15 := A{a: 1} // nope: += in field assignment + t15.b += 2 + + t16 := A{ // want "embedded field assignment can be moved to struct literal" + a: 1, // comment, with a comma + } + t16.b = 2 +} + +func foo() int { + return 0 +}
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 d20da95..4bad37a 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
@@ -55,6 +55,16 @@ type K struct{ L } type L []int +type T struct { + a int + b int + U +} + +type U struct { + x int +} + var ( _ = A{b: 1} // want "embedded field type can be removed from struct literal" _ = A{a: 1, b: 1, c: 1} // want "embedded field type can be removed from struct literal" @@ -66,11 +76,11 @@ _ = J{i: 1, F: F{f: 1}, G: G{f: 1}} // want "embedded field type can be removed from struct literal" // multi-line with commas _ = A{ // want "embedded field type can be removed from struct literal" - b: 1, } - // empty composite lit - _ = A{a: 1} // want "embedded field type can be removed from struct literal" + + _ = A{a: 1, B: B{}} // nope: empty composite lit + // don't suggest a fix if it's too tricky to preserve comments _ = A{ // nope: comments within range to delete B: B{ // one @@ -89,6 +99,98 @@ }, a: 2, } + _ = A{B: /* comment */ B{b: 1}} // nope: comment in range to delete _ = A{b: 1 /* comment */, a: 2} // want "embedded field type can be removed from struct literal" - _ = K{L: L{zero: 0}} // nope: cannot promote slice elements + _ = K{L: L{zero: 0}} // nope: cannot promote slice elements + _ = K{L: L{0: 100}} // nope: cannot promote slice elements + + _ = A{ // want "embedded field type can be removed from struct literal" + c: 1, + b: 2, + a: 3, + } + + _ = A{c: 1} // want "embedded field type can be removed from struct literal" + + _ = A{ // want "embedded field type can be removed from struct literal" + c: 1} + + _ = A{ // want "embedded field type can be removed from struct literal" + c: 1, + } + + _ = A{ // want "embedded field type can be removed from struct literal" + // comment here + c: 1, + } ) + +func _() { + t1 := A{a: 1, // want "embedded field assignment can be moved to struct literal" + b: 2} + + var t2 A + t2 = A{a: 1, // want "embedded field assignment can be moved to struct literal" + b: 2} + + var t3 = A{a: 1, // want "embedded field assignment can be moved to struct literal" + b: 2} + + t4 := T{1, 2, U{x: 3}} // nope: can't mix keyed and unkeyed elements + t4.x = 4 + + t5 := A{a: 1} + _ = t5 // nope: intervening statement + t5.b = 2 + + t6 := A{a: 1} // nope: value assigned depends on t6 itself + t6.b = t6.a + 1 + + t7 := A{a: 1, // want "embedded field assignment can be moved to struct literal" + b: foo()} + + // Only apply edits from pattern A first even though both patterns apply. + t8 := A{ // want "embedded field type can be removed from struct literal" + b: 1, + } + t8.a = 2 + + t9 := A{} // nope: multiple assignments not yet supported + t9.a, t9.b = 1, 2 + + t10 := A{ // want "embedded field assignment can be moved to struct literal" + a: 1, // this comment is preserved + b: 2} // this one too + + t11 := A{a: 1, // want "embedded field assignment can be moved to struct literal" + b: 2, + c: 3} + + t12 := A{ // want "embedded field assignment can be moved to struct literal" + B: B{ + b: 1, // this comment is preserved + }} + + t13 := A{ // want "embedded field assignment can be moved to struct literal" + a: 1, + // comment between assignments + b: 2} + + t14 := A{ // want "embedded field assignment can be moved to struct literal" + a: 1, + b: foo() + + 1} + + t15 := A{a: 1} // nope: += in field assignment + t15.b += 2 + + t16 := A{ // want "embedded field assignment can be moved to struct literal" + a: 1, // comment, with a comma + + b: 2} +} + +func foo() int { + return 0 +} +
diff --git a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go index c3cf830..2e840b7 100644 --- a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go +++ b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go
@@ -22,6 +22,12 @@ } { var patherr *os.PathError + if errors.As(err, &patherr) { // want `errors.As can be simplified using AsType\[\*os.PathError\]` + print("not a use of patherr") + } + } + { + var patherr *os.PathError print(patherr) if errors.As(err, &patherr) { // nope: patherr is used outside scope of if print(patherr) @@ -49,4 +55,53 @@ print(patherr, ok) } } + // Negated case. + { + var patherr *os.PathError + if !errors.As(err, &patherr) { // want `errors.As can be simplified using AsType\[\*os.PathError\]` + print(patherr) + } + } + { + var patherr *os.PathError + var linkerr *os.LinkError + if errors.As(err, &patherr) { // want `errors.As can be simplified using AsType\[\*os.PathError\]` + print(patherr) + } else if !errors.As(err, &linkerr) { // want `errors.As can be simplified using AsType\[\*os.LinkError\]` + print("not a use of linkerr") + } + } + { + var patherr *os.PathError + if !errors.As(err, &patherr) { // want `errors.As can be simplified using AsType\[\*os.PathError\]` + print("not a use of patherr") + } else { + print(patherr) + } + } + { + var patherr *os.PathError = &os.PathError{} + if !errors.As(err, &patherr) { // nope: would change the value of patherr observed by the print statement + print(patherr) + } + } + { + type Foo interface { + Bar() string + } + var target Foo + if errors.As(err, &target) { // nope: target doesn't satisfy error + print(target) + } + } + { + type FooError interface { + Bar() string + error + } + var target FooError + if errors.As(err, &target) { // want `errors.As can be simplified using AsType\[FooError\]` + print(target) + } + } }
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 3e8a06b..35a1728 100644 --- a/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden +++ b/go/analysis/passes/modernize/testdata/src/errorsastype/errorsastype.go.golden
@@ -19,6 +19,11 @@ print("also not a use of patherr") } { + if _, ok := errors.AsType[*os.PathError](err); ok { // want `errors.As can be simplified using AsType\[\*os.PathError\]` + print("not a use of patherr") + } + } + { var patherr *os.PathError print(patherr) if errors.As(err, &patherr) { // nope: patherr is used outside scope of if @@ -45,4 +50,48 @@ print(patherr, ok) } } + // Negated case. + { + if patherr, ok := errors.AsType[*os.PathError](err); !ok { // want `errors.As can be simplified using AsType\[\*os.PathError\]` + print(patherr) + } + } + { + if patherr, ok := errors.AsType[*os.PathError](err); ok { // want `errors.As can be simplified using AsType\[\*os.PathError\]` + print(patherr) + } else if _, ok := errors.AsType[*os.LinkError](err); !ok { // want `errors.As can be simplified using AsType\[\*os.LinkError\]` + print("not a use of linkerr") + } + } + { + if patherr, ok := errors.AsType[*os.PathError](err); !ok { // want `errors.As can be simplified using AsType\[\*os.PathError\]` + print("not a use of patherr") + } else { + print(patherr) + } + } + { + var patherr *os.PathError = &os.PathError{} + if !errors.As(err, &patherr) { // nope: would change the value of patherr observed by the print statement + print(patherr) + } + } + { + type Foo interface { + Bar() string + } + var target Foo + if errors.As(err, &target) { // nope: target doesn't satisfy error + print(target) + } + } + { + type FooError interface { + Bar() string + error + } + if target, ok := errors.AsType[FooError](err); ok { // want `errors.As can be simplified using AsType\[FooError\]` + print(target) + } + } }
diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go index 1b77e34..f4155ea 100644 --- a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go +++ b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go
@@ -10,47 +10,73 @@ type B[T any] int +type tokenKind int + +type namedStruct struct{ field int } + +type Fn func([4]byte) + +const arrayLen = 4 + +func helper(a int, b string) {} + var ( - x any - a A - b B[int] - _ = reflect.TypeOf(x) // nope (dynamic) - _ = reflect.TypeOf(0) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(nil) // nope (likely a mistake) - _ = reflect.TypeOf(uint(0)) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(error(nil)) // nope (likely a mistake) - _ = reflect.TypeOf((*error)(nil)) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(io.Reader(nil)) // nope (likely a mistake) - _ = reflect.TypeOf((io.Reader)(nil)) // nope (likely a mistake) - _ = reflect.TypeOf((*io.Reader)(nil)) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(*new(time.Time)) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(time.Time{}) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(time.Duration(0)) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(&a) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(&b) // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf([]io.Reader(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf([]*io.Reader(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf((*io.Reader)(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf([0]io.Reader{}).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf([1]io.Reader{nil}).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf([...]io.Reader{}).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf([...]io.Reader{nil}).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(chan int(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(map[string]int(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + x any + a A + b B[int] + token tokenKind + named = namedStruct{field: 1} + ptr = &named + _ = reflect.TypeOf(x) // nope (dynamic) + _ = reflect.TypeOf(0) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(nil) // nope (likely a mistake) + _ = reflect.TypeOf(uint(0)) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(error(nil)) // nope (likely a mistake) + _ = reflect.TypeOf((*error)(nil)) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(io.Reader(nil)) // nope (likely a mistake) + _ = reflect.TypeOf((io.Reader)(nil)) // nope (likely a mistake) + _ = reflect.TypeOf((*io.Reader)(nil)) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(*new(time.Time)) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(time.Time{}) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(time.Duration(0)) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(&a) // nope (mentions symbol a) + _ = reflect.TypeOf(&b) // nope (mentions symbol b) + _ = reflect.TypeOf(tokenKind(0)) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(namedStruct{}) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf([4]byte{}) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf((*[4]byte)(nil)) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(any([4]byte{}).([4]byte)) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(helper) // nope (mentions symbol helper) + _ = reflect.TypeOf(token) // nope (mentions symbol token) + _ = reflect.TypeOf(named.field) // nope (mentions symbols named and field) + _ = reflect.TypeOf(ptr).Elem() // nope (mentions symbol ptr) + _ = reflect.TypeOf([arrayLen]byte{}) // nope (mentions symbol arrayLen) + _ = reflect.TypeOf((*[arrayLen]byte)(nil)) // nope (mentions symbol arrayLen) + _ = reflect.TypeOf(any([4]byte{}).([arrayLen]byte)) // nope (mentions symbol arrayLen) + _ = reflect.TypeOf(Fn(func([arrayLen]byte) {})) // nope (mentions symbol arrayLen, in a func literal) + _ = reflect.TypeOf([]io.Reader(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf([]*io.Reader(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf((*io.Reader)(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf([0]io.Reader{}).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf([1]io.Reader{nil}).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf([...]io.Reader{}).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf([...]io.Reader{nil}).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(chan int(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(map[string]int(nil)).Elem() // want "reflect.TypeOf call can be simplified using TypeFor" ) -// Eliminate local var if we deleted its last use. +// Preserve local variables used as symbolic stand-ins. func _() { // Test for shadowed nil nil := "nil" - _ = reflect.TypeOf(nil) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(nil) // nope (mentions symbol nil) _ = nil // shadowed nil has multiple uses var zero string - _ = reflect.TypeOf(zero) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(zero) // nope (mentions symbol zero) var z2 string - _ = reflect.TypeOf(z2) // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(z2) // nope (mentions symbol z2) _ = z2 // z2 has multiple uses }
diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden index 1301bff..6b0ad44 100644 --- a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden +++ b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden
@@ -10,46 +10,73 @@ type B[T any] int +type tokenKind int + +type namedStruct struct{ field int } + +type Fn func([4]byte) + +const arrayLen = 4 + +func helper(a int, b string) {} + var ( - x any - a A - b B[int] - _ = reflect.TypeOf(x) // nope (dynamic) - _ = reflect.TypeFor[int]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(nil) // nope (likely a mistake) - _ = reflect.TypeFor[uint]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(error(nil)) // nope (likely a mistake) - _ = reflect.TypeFor[*error]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeOf(io.Reader(nil)) // nope (likely a mistake) - _ = reflect.TypeOf((io.Reader)(nil)) // nope (likely a mistake) - _ = reflect.TypeFor[*io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[time.Time]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[time.Time]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[time.Duration]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[*A]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[*B[int]]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[*io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[int]() // want "reflect.TypeOf call can be simplified using TypeFor" - _ = reflect.TypeFor[int]() // want "reflect.TypeOf call can be simplified using TypeFor" + x any + a A + b B[int] + token tokenKind + named = namedStruct{field: 1} + ptr = &named + _ = reflect.TypeOf(x) // nope (dynamic) + _ = reflect.TypeFor[int]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(nil) // nope (likely a mistake) + _ = reflect.TypeFor[uint]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(error(nil)) // nope (likely a mistake) + _ = reflect.TypeFor[*error]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(io.Reader(nil)) // nope (likely a mistake) + _ = reflect.TypeOf((io.Reader)(nil)) // nope (likely a mistake) + _ = reflect.TypeFor[*io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[time.Time]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[time.Time]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[time.Duration]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(&a) // nope (mentions symbol a) + _ = reflect.TypeOf(&b) // nope (mentions symbol b) + _ = reflect.TypeFor[tokenKind]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[namedStruct]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[[4]byte]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[*[4]byte]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[[4]byte]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(helper) // nope (mentions symbol helper) + _ = reflect.TypeOf(token) // nope (mentions symbol token) + _ = reflect.TypeOf(named.field) // nope (mentions symbols named and field) + _ = reflect.TypeOf(ptr).Elem() // nope (mentions symbol ptr) + _ = reflect.TypeOf([arrayLen]byte{}) // nope (mentions symbol arrayLen) + _ = reflect.TypeOf((*[arrayLen]byte)(nil)) // nope (mentions symbol arrayLen) + _ = reflect.TypeOf(any([4]byte{}).([arrayLen]byte)) // nope (mentions symbol arrayLen) + _ = reflect.TypeOf(Fn(func([arrayLen]byte) {})) // nope (mentions symbol arrayLen, in a func literal) + _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[*io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[io.Reader]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[int]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeFor[int]() // want "reflect.TypeOf call can be simplified using TypeFor" ) -// Eliminate local var if we deleted its last use. +// Preserve local variables used as symbolic stand-ins. func _() { // Test for shadowed nil nil := "nil" - _ = reflect.TypeFor[string]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(nil) // nope (mentions symbol nil) _ = nil // shadowed nil has multiple uses - - _ = reflect.TypeFor[string]() // want "reflect.TypeOf call can be simplified using TypeFor" + + var zero string + _ = reflect.TypeOf(zero) // nope (mentions symbol zero) var z2 string - _ = reflect.TypeFor[string]() // want "reflect.TypeOf call can be simplified using TypeFor" + _ = reflect.TypeOf(z2) // nope (mentions symbol z2) _ = z2 // z2 has multiple uses }
diff --git a/go/analysis/passes/modernize/testdata/src/slicescontains/slicescontains.go b/go/analysis/passes/modernize/testdata/src/slicescontains/slicescontains.go index b9734e4..8e3c840 100644 --- a/go/analysis/passes/modernize/testdata/src/slicescontains/slicescontains.go +++ b/go/analysis/passes/modernize/testdata/src/slicescontains/slicescontains.go
@@ -251,3 +251,14 @@ } o.Print() } + +func issue77564needlesideeffects(slice []int, f func() int) bool { + found := false + for _, elem := range slice { // nope: needle f() may have side effects + if elem == f() { + found = true + break + } + } + return found +}
diff --git a/go/analysis/passes/modernize/testdata/src/slicescontains/slicescontains.go.golden b/go/analysis/passes/modernize/testdata/src/slicescontains/slicescontains.go.golden index 6b9275d..58d9629 100644 --- a/go/analysis/passes/modernize/testdata/src/slicescontains/slicescontains.go.golden +++ b/go/analysis/passes/modernize/testdata/src/slicescontains/slicescontains.go.golden
@@ -190,3 +190,14 @@ } o.Print() } + +func issue77564needlesideeffects(slice []int, f func() int) bool { + found := false + for _, elem := range slice { // nope: needle f() may have side effects + if elem == f() { + found = true + break + } + } + return found +}
diff --git a/go/analysis/passes/modernize/testdata/src/stringscutprefix/stringscutprefix.go.golden b/go/analysis/passes/modernize/testdata/src/stringscutprefix/stringscutprefix.go.golden index b1931aa..9699cbe 100644 --- a/go/analysis/passes/modernize/testdata/src/stringscutprefix/stringscutprefix.go.golden +++ b/go/analysis/passes/modernize/testdata/src/stringscutprefix/stringscutprefix.go.golden
@@ -94,7 +94,7 @@ if after, ok := strings.CutPrefix(s, pre); ok { // want "TrimPrefix can be simplified to CutPrefix" println(after) } - if after, ok := strings.CutPrefix(s, pre); ok { // want "TrimPrefix can be simplified to CutPrefix" + if _, ok := strings.CutPrefix(s, pre); ok { // want "TrimPrefix can be simplified to CutPrefix" println(strings.TrimPrefix(s, pre)) // noop here } if after, ok := strings.CutPrefix(s, ""); ok { // want "TrimPrefix can be simplified to CutPrefix"
diff --git a/go/analysis/passes/modernize/unsafefuncs.go b/go/analysis/passes/modernize/unsafefuncs.go index 91c2f37..34c135c 100644 --- a/go/analysis/passes/modernize/unsafefuncs.go +++ b/go/analysis/passes/modernize/unsafefuncs.go
@@ -16,7 +16,6 @@ "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/analysis/analyzerutil" "golang.org/x/tools/internal/astutil" - "golang.org/x/tools/internal/goplsexport" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" @@ -29,6 +28,7 @@ // func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType // func SliceData(slice []ArbitraryType) *ArbitraryType +// TODO(adonovan): needs a proposal for a public symbol. var unsafeFuncsAnalyzer = &analysis.Analyzer{ Name: "unsafefuncs", Doc: analyzerutil.MustExtractDoc(doc, "unsafefuncs"), @@ -37,11 +37,6 @@ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#unsafefuncs", } -func init() { - // Export to gopls until this is a published modernizer. - goplsexport.UnsafeFuncsModernizer = unsafeFuncsAnalyzer -} - func unsafefuncs(pass *analysis.Pass) (any, error) { // Short circuit if the package doesn't use unsafe. // (In theory one could use some imported alias of unsafe.Pointer,
diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 1306531..f82d2ea 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go
@@ -416,7 +416,7 @@ } // propagate propagates changes in wrapper (non-None) kind information backwards -// through through the wrapper.callers graph of well-formed forwarding calls. +// through the wrapper.callers graph of well-formed forwarding calls. func propagate(pass *analysis.Pass, w *wrapper, call *ast.CallExpr, kind Kind, res *Result) { // Check correct call forwarding. // @@ -1061,7 +1061,10 @@ e = u.X // strip off & from &r } if id, ok := e.(*ast.Ident); ok { - if pass.TypesInfo.Uses[id] == sig.Recv() { + // Uses refers to the receiver Var for the declared method, but looking up the String + // method on the instantiated receiver type may return an instantiated Signature with + // distinct parameter variables. Therefore we must compare against the Origin. + if pass.TypesInfo.Uses[id] == sig.Recv().Origin() { return method.FullName(), true } }
diff --git a/go/analysis/passes/printf/testdata/src/a/a.go b/go/analysis/passes/printf/testdata/src/a/a.go index a66f0ac..37961c4 100644 --- a/go/analysis/passes/printf/testdata/src/a/a.go +++ b/go/analysis/passes/printf/testdata/src/a/a.go
@@ -154,7 +154,7 @@ fmt.Println("%v", "hi") // want "fmt.Println call has possible Printf formatting directive %v" fmt.Println("%T", "hi") // want "fmt.Println call has possible Printf formatting directive %T" fmt.Println("%s"+" there", "hi") // want "fmt.Println call has possible Printf formatting directive %s" - fmt.Println("http://foo.com?q%2Fabc") // no diagnostic: %XX is excepted + fmt.Println("http://foo.com?q%2Fabc") // no diagnostic: %XX is expected fmt.Println("http://foo.com?q%2Fabc-%s") // want"fmt.Println call has possible Printf formatting directive %s" fmt.Println("0.0%") // correct (trailing % couldn't be a formatting directive) fmt.Printf("%s", "hi", 3) // want "fmt.Printf call needs 1 arg but has 2 args"
diff --git a/go/analysis/passes/scannererr/scannererr.go b/go/analysis/passes/scannererr/scannererr.go index 40d6c47..059b162 100644 --- a/go/analysis/passes/scannererr/scannererr.go +++ b/go/analysis/passes/scannererr/scannererr.go
@@ -111,10 +111,8 @@ for curUse := range index.Uses(sc) { // If the var sc is used in a context other than sc.Method(...), // assume conservatively that it may escape, and reject this candidate. - switch curUse.ParentEdgeKind() { - case edge.SelectorExpr_X, edge.CallExpr_Fun: - // ok - default: + if curUse.ParentEdgeKind() != edge.SelectorExpr_X || + curUse.Parent().ParentEdgeKind() != edge.CallExpr_Fun { continue callLoop }
diff --git a/go/analysis/passes/sqlrowserr/main.go b/go/analysis/passes/sqlrowserr/main.go new file mode 100644 index 0000000..3bf356a --- /dev/null +++ b/go/analysis/passes/sqlrowserr/main.go
@@ -0,0 +1,14 @@ +// 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 + +package main + +import ( + "golang.org/x/tools/go/analysis/passes/sqlrowserr" + "golang.org/x/tools/go/analysis/singlechecker" +) + +func main() { singlechecker.Main(sqlrowserr.Analyzer) }
diff --git a/go/analysis/passes/sqlrowserr/sqlrowserr.go b/go/analysis/passes/sqlrowserr/sqlrowserr.go new file mode 100644 index 0000000..4ae1529 --- /dev/null +++ b/go/analysis/passes/sqlrowserr/sqlrowserr.go
@@ -0,0 +1,151 @@ +// 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 sqlrowserr defines an analyzer for uses of sql.Rows +// in which the user has forgotten to check Rows.Err. +package sqlrowserr + +import ( + "fmt" + "go/ast" + "go/token" + "go/types" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/edge" + "golang.org/x/tools/go/ast/inspector" + typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" + "golang.org/x/tools/internal/moreiters" + "golang.org/x/tools/internal/typesinternal/typeindex" +) + +const doc = `sqlrowserr: report failure to check sql.Rows.Err + +This analyzer reports uses of sql.Rows in which the result of a query +such as db.Query() is assigned to a local variable that is then used +in a loop that calls Rows.Next, but lacks a final check of Rows.Err. +This causes row iteration errors to be discarded. + +For example: + + rows, err := db.Query("select ...") // error: "sql.Rows rows is used in Next loop without final check of rows.Err()" + if err != nil { + return err + } + defer rows.Close() // ignore error + for rows.Next() { + var x int + if err := rows.Scan(&x); err != nil { + return err + } + use(x) + } + /* ...no use of rows.Err()... */ + +Correct usage of sql.Rows demands both a call to Rows.Close to release +resources and a call to Rows.Err to report iteration errors. It is +not critical to report resource cleanup errors, but it is crucial to +report iteration errors as they would otherwise be indistinguishable +from a smaller result. + +To avoid false positives, the analyzer is silent if the Rows is passed +into or out of the function or assigned somewhere other than a local +variable. + +It is not this analyzer's goal to ensure proper handling of errors in +all cases, but merely the simple mistakes where the user may have been +oblivious to the existence of the Rows.Err method. +` + +var Analyzer = &analysis.Analyzer{ + Name: "sqlrowserr", + Doc: doc, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sqlrowserr", + Requires: []*analysis.Analyzer{inspect.Analyzer, typeindexanalyzer.Analyzer}, + Run: run, +} + +// TODO(adonovan): factor common structures with the scannererr analyzer. + +func run(pass *analysis.Pass) (any, error) { + var ( + index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) + info = pass.TypesInfo + ) + + checkCall := func(curCall inspector.Cursor) { + var lhs ast.Expr + switch curCall.ParentEdgeKind() { + case edge.ValueSpec_Values: + // var sc, err = db.Query(...) + curName := curCall.Parent().ChildAt(edge.ValueSpec_Names, 0) + lhs = curName.Node().(*ast.Ident) + case edge.AssignStmt_Rhs: + // sc, err := db.Query(...) (or '=') + curLhs := curCall.Parent().ChildAt(edge.AssignStmt_Lhs, 0) + lhs = curLhs.Node().(ast.Expr) + } + id, ok := lhs.(*ast.Ident) + if !ok { + return + } + rows, ok := info.ObjectOf(id).(*types.Var) + if !ok { + return + } + // Have: rows, err := db.Query(...) + + // Check all uses of the var rows. + nextLoop := token.NoPos // position of rows.Next() call within a loop + for curUse := range index.Uses(rows) { + // If the var rows is used in a context other than rows.Method(...), + // assume conservatively that it may escape, and reject this candidate. + if curUse.ParentEdgeKind() != edge.SelectorExpr_X || + curUse.Parent().ParentEdgeKind() != edge.CallExpr_Fun { + return + } + + switch curUse.Parent().Node().(*ast.SelectorExpr).Sel.Name { + case "Err": + // If the rows.Err method is called anywhere, reject this candidate. + return + case "Next": + // The Next call must be in a loop that intervenes the declaration of rows. + if curLoop, ok := moreiters.First(curUse.Enclosing((*ast.RangeStmt)(nil), (*ast.ForStmt)(nil))); ok { + if curLoop.Node().Pos() > rows.Pos() { + nextLoop = curUse.Node().Pos() + } + } + } + } + if !nextLoop.IsValid() { + return + } + pass.Report(analysis.Diagnostic{ + Pos: curCall.Node().Pos(), + End: curCall.Node().End(), + Message: fmt.Sprintf("sql.Rows %q is used in Next loop at line %d without final check of %s.Err()", + rows.Name(), pass.Fset.Position(nextLoop).Line, rows.Name()), + }) + } + + // Check each query method in the sql package that returns (*Rows, error). + // (This could be generalized for arbitrary such functions...) + for _, m := range [...][2]string{ + {"Conn", "QueryContext"}, + {"DB", "QueryContext"}, + {"DB", "Query"}, + {"Stmt", "QueryContext"}, + {"Stmt", "Query"}, + {"Tx", "QueryContext"}, + {"Tx", "Query"}, + } { + for cur := range index.Calls(index.Selection("database/sql", m[0], m[1])) { + checkCall(cur) + } + } + + return nil, nil +}
diff --git a/go/analysis/passes/sqlrowserr/sqlrowserr_test.go b/go/analysis/passes/sqlrowserr/sqlrowserr_test.go new file mode 100644 index 0000000..d4073bf --- /dev/null +++ b/go/analysis/passes/sqlrowserr/sqlrowserr_test.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 sqlrowserr_test + +import ( + "testing" + + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/sqlrowserr" +) + +func Test(t *testing.T) { + testdata := analysistest.TestData() + analysistest.RunWithSuggestedFixes(t, testdata, sqlrowserr.Analyzer, "a") +}
diff --git a/go/analysis/passes/sqlrowserr/testdata/src/a/a.go b/go/analysis/passes/sqlrowserr/testdata/src/a/a.go new file mode 100644 index 0000000..9074198 --- /dev/null +++ b/go/analysis/passes/sqlrowserr/testdata/src/a/a.go
@@ -0,0 +1,95 @@ +package a + +import "database/sql" + +// +// (Ballast comment to make it easier to adjust line numbers when imports change.) +// + +func missingErr(db *sql.DB) { + rows, err := db.Query("") // want `sql.Rows "rows" is used in Next loop at line 15 without final check of rows.Err\(\)` + if err != nil { + return + } + defer rows.Close() // ignore error + for rows.Next() { // L15 + println(rows.Scan()) + } +} + +func missingErr2(db *sql.DB) { + rows, _ := db.QueryContext(nil, "") // want `sql.Rows "rows" is used in Next loop at line 23 without final check of rows.Err\(\)` + for { + if !rows.Next() { // L23 + break + } + println(rows.Scan()) + } +} + +func stmt(stmt *sql.Stmt) { + { + rows, _ := stmt.QueryContext(nil, "") // want `sql.Rows "rows" is used in Next loop at line 33 without final check of rows.Err\(\)` + for rows.Next() { // L33 + rows.Scan() + } + } + { + rows, _ := stmt.Query("") // want `sql.Rows "rows" is used in Next loop at line .. without final check of rows.Err\(\)` + for rows.Next() { + rows.Scan() + } + } +} + +func tx(tx *sql.Tx) { + { + rows, _ := tx.QueryContext(nil, "") // want `sql.Rows "rows" is used in Next loop at line .. without final check of rows.Err\(\)` + for rows.Next() { + rows.Scan() + } + } + { + rows, _ := tx.Query("") // want `sql.Rows "rows" is used in Next loop at line .. without final check of rows.Err\(\)` + for rows.Next() { + rows.Scan() + } + } +} + +func conn(conn *sql.Conn) { + rows, _ := conn.QueryContext(nil, "") // want `sql.Rows "rows" is used in Next loop at line .. without final check of rows.Err\(\)` + for rows.Next() { + rows.Scan() + } +} + +func nopeErrIsChecked(db *sql.DB) { + rows, _ := db.Query("") + for rows.Next() { + } + if err := rows.Err(); err != nil { + panic(err) + } +} + +func nopeErrIsCalled(db *sql.DB) { + rows, _ := db.Query("") + for rows.Next() { + println(rows.Scan()) + } + _ = rows.Err() // ignore error +} + +func nopeRowsIsParam(rows *sql.Rows) { + for rows.Next() { + } +} + +func nopeRowsEscapes(rows *sql.Rows) { + for rows.Next() { + } + arbitraryEffects(rows) +} + +func arbitraryEffects(any)
diff --git a/go/analysis/suite/fix/fix.go b/go/analysis/suite/fix/fix.go new file mode 100644 index 0000000..aa63b45 --- /dev/null +++ b/go/analysis/suite/fix/fix.go
@@ -0,0 +1,46 @@ +// 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. + +// The fix package defines the suite of analyzers used by cmd/fix, +// the default analysis tool run by "go fix". +// Its behavior is equivalent to: +// +// func main() { unitchecker.Main(fix.Suite...) } +// +// If you need a different suite, define your own tool +// and run "go vet -vettool=mytool". +package fix + +import ( + "slices" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/buildtag" + "golang.org/x/tools/go/analysis/passes/hostport" + "golang.org/x/tools/go/analysis/passes/inline" + "golang.org/x/tools/go/analysis/passes/modernize" +) + +// Suite is the suite of analyzers run by cmd/fix. +// +// The fix suite analyzers produce fixes are unambiguously safe to apply, +// even if the diagnostics might not describe actual problems. +var Suite = slices.Concat( + []*analysis.Analyzer{ + buildtag.Analyzer, + hostport.Analyzer, + inline.Analyzer, + }, + modernize.Suite, + // TODO(adonovan): add any other vet analyzers whose fixes are always safe. + // Candidates to audit: sigchanyzer, printf, assign, unreachable. + // Many of staticcheck's analyzers would make good candidates + // (e.g. rewriting WriteString(fmt.Sprintf()) to Fprintf.) + // Rejected: + // - composites: some types (e.g. PointXY{1,2}) don't want field names. + // - timeformat: flipping MM/DD is a behavior change, but the code + // could potentially be a workaround for another bug. + // - stringintconv: offers two fixes, user input required to choose. + // - fieldalignment: poor signal/noise; fix could be a regression. +)
diff --git a/go/analysis/suite/vet/vet.go b/go/analysis/suite/vet/vet.go new file mode 100644 index 0000000..b85ee65 --- /dev/null +++ b/go/analysis/suite/vet/vet.go
@@ -0,0 +1,99 @@ +// 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. + +// The vet package defines the suite of analyzers used by cmd/vet, +// the default analysis tool run by "go vet". +// Its behavior is equivalent to: +// +// func main() { unitchecker.Main(vet.Suite...) } +// +// If you need a different suite, define your own tool +// and run "go vet -vettool=mytool". +package vet + +import ( + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/appends" + "golang.org/x/tools/go/analysis/passes/asmdecl" + "golang.org/x/tools/go/analysis/passes/assign" + "golang.org/x/tools/go/analysis/passes/atomic" + "golang.org/x/tools/go/analysis/passes/bools" + "golang.org/x/tools/go/analysis/passes/buildtag" + "golang.org/x/tools/go/analysis/passes/cgocall" + "golang.org/x/tools/go/analysis/passes/composite" + "golang.org/x/tools/go/analysis/passes/copylock" + "golang.org/x/tools/go/analysis/passes/defers" + "golang.org/x/tools/go/analysis/passes/directive" + "golang.org/x/tools/go/analysis/passes/errorsas" + "golang.org/x/tools/go/analysis/passes/framepointer" + "golang.org/x/tools/go/analysis/passes/hostport" + "golang.org/x/tools/go/analysis/passes/httpresponse" + "golang.org/x/tools/go/analysis/passes/ifaceassert" + "golang.org/x/tools/go/analysis/passes/loopclosure" + "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/shift" + "golang.org/x/tools/go/analysis/passes/sigchanyzer" + "golang.org/x/tools/go/analysis/passes/slog" + "golang.org/x/tools/go/analysis/passes/stdmethods" + "golang.org/x/tools/go/analysis/passes/stdversion" + "golang.org/x/tools/go/analysis/passes/stringintconv" + "golang.org/x/tools/go/analysis/passes/structtag" + "golang.org/x/tools/go/analysis/passes/testinggoroutine" + "golang.org/x/tools/go/analysis/passes/tests" + "golang.org/x/tools/go/analysis/passes/timeformat" + "golang.org/x/tools/go/analysis/passes/unmarshal" + "golang.org/x/tools/go/analysis/passes/unreachable" + "golang.org/x/tools/go/analysis/passes/unsafeptr" + "golang.org/x/tools/go/analysis/passes/unusedresult" + "golang.org/x/tools/go/analysis/passes/waitgroup" +) + +// Suite is the suite of analyzers run by cmd/vet. +// +// The vet suite analyzers report diagnostics. +// (Diagnostics must describe real problems, but need not +// suggest fixes, and fixes are not necessarily safe to apply.) +var Suite = []*analysis.Analyzer{ + appends.Analyzer, + asmdecl.Analyzer, + assign.Analyzer, + atomic.Analyzer, + bools.Analyzer, + buildtag.Analyzer, + cgocall.Analyzer, + composite.Analyzer, + copylock.Analyzer, + defers.Analyzer, + directive.Analyzer, + errorsas.Analyzer, + // fieldalignment.Analyzer omitted: too noisy + framepointer.Analyzer, + httpresponse.Analyzer, + hostport.Analyzer, + ifaceassert.Analyzer, + loopclosure.Analyzer, + lostcancel.Analyzer, + nilfunc.Analyzer, + printf.Analyzer, + // scannererr.Analyzer, // TODO(adonovan): add to go vet for 1.28 after the freeze (#17747) + // 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) + stdmethods.Analyzer, + stdversion.Analyzer, + stringintconv.Analyzer, + structtag.Analyzer, + tests.Analyzer, + testinggoroutine.Analyzer, + timeformat.Analyzer, + unmarshal.Analyzer, + unreachable.Analyzer, + unsafeptr.Analyzer, + unusedresult.Analyzer, + waitgroup.Analyzer, +}
diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go index a22aea0..fd31501 100644 --- a/go/analysis/unitchecker/unitchecker_test.go +++ b/go/analysis/unitchecker/unitchecker_test.go
@@ -28,7 +28,7 @@ // child process? switch os.Getenv("ENTRYPOINT") { case "vet": - vet() + vetmain() panic("unreachable") case "minivet": minivet()
diff --git a/go/analysis/unitchecker/vet_std_test.go b/go/analysis/unitchecker/vet_std_test.go index a761bc0..7d9ee8a 100644 --- a/go/analysis/unitchecker/vet_std_test.go +++ b/go/analysis/unitchecker/vet_std_test.go
@@ -9,84 +9,28 @@ "os" "os/exec" "runtime" + "slices" "strings" "testing" - "golang.org/x/tools/go/analysis/passes/appends" - "golang.org/x/tools/go/analysis/passes/asmdecl" - "golang.org/x/tools/go/analysis/passes/assign" - "golang.org/x/tools/go/analysis/passes/atomic" - "golang.org/x/tools/go/analysis/passes/bools" - "golang.org/x/tools/go/analysis/passes/buildtag" - "golang.org/x/tools/go/analysis/passes/cgocall" - "golang.org/x/tools/go/analysis/passes/composite" - "golang.org/x/tools/go/analysis/passes/copylock" - "golang.org/x/tools/go/analysis/passes/defers" - "golang.org/x/tools/go/analysis/passes/directive" - "golang.org/x/tools/go/analysis/passes/errorsas" - "golang.org/x/tools/go/analysis/passes/framepointer" - "golang.org/x/tools/go/analysis/passes/gofix" - "golang.org/x/tools/go/analysis/passes/hostport" - "golang.org/x/tools/go/analysis/passes/httpresponse" - "golang.org/x/tools/go/analysis/passes/ifaceassert" - "golang.org/x/tools/go/analysis/passes/loopclosure" - "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/shift" - "golang.org/x/tools/go/analysis/passes/sigchanyzer" - "golang.org/x/tools/go/analysis/passes/stdmethods" - "golang.org/x/tools/go/analysis/passes/stdversion" - "golang.org/x/tools/go/analysis/passes/stringintconv" - "golang.org/x/tools/go/analysis/passes/structtag" - "golang.org/x/tools/go/analysis/passes/testinggoroutine" - "golang.org/x/tools/go/analysis/passes/tests" - "golang.org/x/tools/go/analysis/passes/timeformat" - "golang.org/x/tools/go/analysis/passes/unmarshal" + "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/unreachable" - "golang.org/x/tools/go/analysis/passes/unusedresult" + "golang.org/x/tools/go/analysis/passes/unsafeptr" + "golang.org/x/tools/go/analysis/suite/vet" "golang.org/x/tools/go/analysis/unitchecker" ) -// vet is the entrypoint of this executable when ENTRYPOINT=vet. -// Keep consistent with the actual vet in GOROOT/src/cmd/vet/main.go. -func vet() { - unitchecker.Main( - appends.Analyzer, - asmdecl.Analyzer, - assign.Analyzer, - atomic.Analyzer, - bools.Analyzer, - buildtag.Analyzer, - cgocall.Analyzer, - composite.Analyzer, - copylock.Analyzer, - defers.Analyzer, - directive.Analyzer, - errorsas.Analyzer, - framepointer.Analyzer, - gofix.Analyzer, - httpresponse.Analyzer, - hostport.Analyzer, - ifaceassert.Analyzer, - loopclosure.Analyzer, - lostcancel.Analyzer, - nilfunc.Analyzer, - printf.Analyzer, - shift.Analyzer, - sigchanyzer.Analyzer, - stdmethods.Analyzer, - stdversion.Analyzer, - stringintconv.Analyzer, - structtag.Analyzer, - testinggoroutine.Analyzer, - tests.Analyzer, - timeformat.Analyzer, - unmarshal.Analyzer, - unreachable.Analyzer, - // unsafeptr.Analyzer, // currently reports findings in runtime - unusedresult.Analyzer, - ) +// vetmain is the entrypoint of this executable when ENTRYPOINT=vet. +func vetmain() { + suite := slices.Clone(vet.Suite) + suite = slices.DeleteFunc(suite, func(a *analysis.Analyzer) bool { + // This logic mirrors code in cmd/go/internal/work.Builder.vet + // to tailor the default analyzer suite used by go vet/test in GOROOT. + // (See https://go.dev/issue/79622.) + return a == unsafeptr.Analyzer || a == unreachable.Analyzer + }) + + unitchecker.Main(suite...) } // TestVetStdlib runs the same analyzers as the actual vet over the
diff --git a/go/callgraph/rta/rta.go b/go/callgraph/rta/rta.go index 442a226..6cee0c3 100644 --- a/go/callgraph/rta/rta.go +++ b/go/callgraph/rta/rta.go
@@ -114,15 +114,13 @@ type concreteTypeInfo struct { C types.Type - mset *types.MethodSet fprint uint64 // fingerprint of method set implements []*types.Interface // unordered set of implemented interfaces } type interfaceTypeInfo struct { I *types.Interface - mset *types.MethodSet - fprint uint64 + fprint uint64 // fingerprint of method set implementations []types.Type // unordered set of concrete implementations } @@ -361,11 +359,9 @@ if v := r.concreteTypes.At(C); v != nil { cinfo = v.(*concreteTypeInfo) } else { - mset := r.prog.MethodSets.MethodSet(C) cinfo = &concreteTypeInfo{ C: C, - mset: mset, - fprint: fingerprint(mset), + fprint: fingerprint(r.prog.MethodSets.MethodSet(C)), } r.concreteTypes.Set(C, cinfo) @@ -390,11 +386,9 @@ if v := r.interfaceTypes.At(I); v != nil { iinfo = v.(*interfaceTypeInfo) } else { - mset := r.prog.MethodSets.MethodSet(I) iinfo = &interfaceTypeInfo{ I: I, - mset: mset, - fprint: fingerprint(mset), + fprint: fingerprint(r.prog.MethodSets.MethodSet(I)), } r.interfaceTypes.Set(I, iinfo) @@ -544,6 +538,9 @@ for method := range mset.Methods() { method := method.Obj() sig := method.Type().(*types.Signature) + if sig.TypeParams() != nil { + continue // skip generic methods since interfaces don't have them + } sum := crc32.ChecksumIEEE(fmt.Appendf(space[:], "%s/%d/%d", method.Id(), sig.Params().Len(),
diff --git a/go/callgraph/rta/rta_test.go b/go/callgraph/rta/rta_test.go index 7b273cc..8a96b99 100644 --- a/go/callgraph/rta/rta_test.go +++ b/go/callgraph/rta/rta_test.go
@@ -20,6 +20,7 @@ "golang.org/x/tools/go/callgraph/rta" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/testfiles" "golang.org/x/tools/txtar" ) @@ -37,6 +38,10 @@ "testdata/rtype.txtar", "testdata/multipkgs.txtar", } + if testenv.Go1Point() >= 27 { + archivePaths = append(archivePaths, "testdata/genericmethod.txtar") + } + for _, archive := range archivePaths { t.Run(archive, func(t *testing.T) { ar, err := txtar.ParseFile(archive)
diff --git a/go/callgraph/rta/testdata/genericmethod.txtar b/go/callgraph/rta/testdata/genericmethod.txtar new file mode 100644 index 0000000..e2b96cd --- /dev/null +++ b/go/callgraph/rta/testdata/genericmethod.txtar
@@ -0,0 +1,31 @@ +-- go.mod -- +module example.com +go 1.27 + +-- a/a.go -- +package main + +type C struct{} + +func (C) Foo[T any]() { f() } + +func f() {} + +func main() { + var c C + c.Foo[int]() + c.Foo[string]() +} + +// WANT: +// +// edge main --static method call--> (C).Foo[int] +// edge main --static method call--> (C).Foo[string] +// edge (C).Foo[string] --static function call--> f +// edge (C).Foo[int] --static function call--> f +// +// reachable (C).Foo[int] +// reachable (C).Foo[string] +// reachable f +// reachable init +// reachable main
diff --git a/go/callgraph/static/static.go b/go/callgraph/static/static.go index 84a95ac..70f8149 100644 --- a/go/callgraph/static/static.go +++ b/go/callgraph/static/static.go
@@ -73,7 +73,9 @@ if !types.IsInterface(T) { mset := prog.MethodSets.MethodSet(T) for method := range mset.Methods() { - visit(cg.CreateNode(prog.MethodValue(method))) + if method.Obj().(*types.Func).Signature().TypeParams() == nil { + visit(cg.CreateNode(prog.MethodValue(method))) + } } } } @@ -87,7 +89,7 @@ visit(cg.CreateNode(mem)) case *ssa.Type: - // methods of package-level non-interface non-parameterized types + // non-parameterized methods of package-level non-interface non-parameterized types if !types.IsInterface(mem.Type()) { if named, ok := mem.Type().(*types.Named); ok && named.TypeParams() == nil {
diff --git a/go/callgraph/static/static_test.go b/go/callgraph/static/static_test.go index a0c5878..b155c24 100644 --- a/go/callgraph/static/static_test.go +++ b/go/callgraph/static/static_test.go
@@ -14,6 +14,7 @@ "golang.org/x/tools/go/callgraph/static" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/testfiles" "golang.org/x/tools/txtar" ) @@ -93,13 +94,40 @@ instantiated[A](a) instantiated[B](b) } + +func j[T any]() { k[T]() } +func k[T any]() {} +` + +const genericMethodsInput = ` +-- go.mod -- +module example.com +go 1.27 + +-- p/p.go -- +package p + +type C struct{} + +func (C) F[T any]() {} + +func f() { + var c C + c.F[string]() + c.F[int]() +} + +func g[T any]() { + new(C).F[T]() +} ` func TestStatic(t *testing.T) { - for _, e := range []struct { + type testcase struct { input string want []string - }{ + } + tests := []testcase{ {input, []string{ "(*C).f -> (C).f", "f -> (C).f", @@ -113,26 +141,40 @@ "f -> instantiated[x.io/p.B]", "instantiated[x.io/p.A] -> (A).F", "instantiated[x.io/p.B] -> (B).F", + "j -> k[T]", + "k[T] -> k", }}, - } { - pkgs := testfiles.LoadPackages(t, txtar.Parse([]byte(e.input)), "./p") - prog, _ := ssautil.Packages(pkgs, ssa.InstantiateGenerics) - prog.Build() - p := pkgs[0].Types + } + if testenv.Go1Point() >= 27 { + tests = append(tests, testcase{genericMethodsInput, []string{ + "(C).F[T] -> (C).F", + "f -> (C).F[int]", + "f -> (C).F[string]", + "g -> (C).F[T]", + }}) + } - cg := static.CallGraph(prog) + for _, test := range tests { + t.Run("", func(t *testing.T) { + pkgs := testfiles.LoadPackages(t, txtar.Parse([]byte(test.input)), "./p") + prog, _ := ssautil.Packages(pkgs, ssa.InstantiateGenerics) + prog.Build() + p := pkgs[0].Types - var edges []string - callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error { - edges = append(edges, fmt.Sprintf("%s -> %s", - e.Caller.Func.RelString(p), - e.Callee.Func.RelString(p))) - return nil + cg := static.CallGraph(prog) + + var edges []string + callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error { + edges = append(edges, fmt.Sprintf("%s -> %s", + e.Caller.Func.RelString(p), + e.Callee.Func.RelString(p))) + return nil + }) // ignore error + sort.Strings(edges) + + if !reflect.DeepEqual(edges, test.want) { + t.Errorf("Got edges %v, want %v", edges, test.want) + } }) - sort.Strings(edges) - - if !reflect.DeepEqual(edges, e.want) { - t.Errorf("Got edges %v, want %v", edges, e.want) - } } }
diff --git a/go/callgraph/util.go b/go/callgraph/util.go index 5499320..d8562a9 100644 --- a/go/callgraph/util.go +++ b/go/callgraph/util.go
@@ -101,6 +101,7 @@ edges[*e] = true } } + // Note: these cross-products can exceed 100 x 100. for fn, cgn := range g.Nodes { if cgn == g.Root || isInit(cgn.Func) || fn.Syntax() != nil { continue // keep
diff --git a/go/callgraph/vta/graph_test.go b/go/callgraph/vta/graph_test.go index 725749e..2c57ca3 100644 --- a/go/callgraph/vta/graph_test.go +++ b/go/callgraph/vta/graph_test.go
@@ -13,7 +13,6 @@ "testing" "golang.org/x/tools/go/callgraph/cha" - "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" ) @@ -26,7 +25,7 @@ // - "foo" function // - "main" function and its // - first register instruction t0 := *gl - prog, _, err := testProg(t, "testdata/src/simple.go", ssa.BuilderMode(0)) + prog, _, err := testProg(t, "testdata/src/simple.go") if err != nil { t.Fatalf("couldn't load testdata/src/simple.go program: %v", err) } @@ -88,7 +87,7 @@ func TestVtaGraph(t *testing.T) { // Get the basic type int from a real program. - prog, _, err := testProg(t, "testdata/src/simple.go", ssa.BuilderMode(0)) + prog, _, err := testProg(t, "testdata/src/simple.go") if err != nil { t.Fatalf("couldn't load testdata/src/simple.go program: %v", err) } @@ -217,7 +216,7 @@ "testdata/src/panic.go", } { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(t, file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) }
diff --git a/go/callgraph/vta/helpers_test.go b/go/callgraph/vta/helpers_test.go index be5e756..762d87b 100644 --- a/go/callgraph/vta/helpers_test.go +++ b/go/callgraph/vta/helpers_test.go
@@ -38,9 +38,9 @@ // testProg returns an ssa representation of a program at // `path`, assumed to define package "testdata," and the // test want result as list of strings. -func testProg(t testing.TB, path string, mode ssa.BuilderMode) (*ssa.Program, []string, error) { +func testProg(t testing.TB, path string) (*ssa.Program, []string, error) { // Set debug mode to exercise DebugRef instructions. - pkg, ssapkg := loadFile(t, path, mode|ssa.GlobalDebug) + pkg, ssapkg := loadFile(t, path, ssa.InstantiateGenerics|ssa.GlobalDebug) return ssapkg.Prog, want(pkg.Syntax[0]), nil }
diff --git a/go/callgraph/vta/vta.go b/go/callgraph/vta/vta.go index ed12001..26b33f0 100644 --- a/go/callgraph/vta/vta.go +++ b/go/callgraph/vta/vta.go
@@ -73,6 +73,9 @@ // CallGraph does not make any assumptions on initial types global variables // and function/method inputs can have. CallGraph is then sound, modulo use of // reflection and unsafe, if the initial call graph is sound. +// +// The supplied SSA functions must have been constructed with the +// [ssa.InstantiateGenerics] mode flag. func CallGraph(funcs map[*ssa.Function]bool, initial *callgraph.Graph) *callgraph.Graph { callees := makeCalleesFunc(funcs, initial) vtaG, canon := typePropGraph(funcs, callees)
diff --git a/go/callgraph/vta/vta_test.go b/go/callgraph/vta/vta_test.go index 3c99797..73f1783 100644 --- a/go/callgraph/vta/vta_test.go +++ b/go/callgraph/vta/vta_test.go
@@ -46,7 +46,7 @@ for _, file := range files { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(t, file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } @@ -75,7 +75,7 @@ // enabled by having an arbitrary function set as input to CallGraph // instead of the whole program (i.e., ssautil.AllFunctions(prog)). func TestVTAProgVsFuncSet(t *testing.T) { - prog, want, err := testProg(t, "testdata/src/callgraph_nested_ptr.go", ssa.BuilderMode(0)) + prog, want, err := testProg(t, "testdata/src/callgraph_nested_ptr.go") if err != nil { t.Fatalf("couldn't load test `testdata/src/callgraph_nested_ptr.go`: %s", err) } @@ -152,7 +152,7 @@ } for _, file := range files { t.Run(file, func(t *testing.T) { - prog, want, err := testProg(t, file, ssa.InstantiateGenerics) + prog, want, err := testProg(t, file) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) } @@ -172,7 +172,7 @@ func TestVTACallGraphGo117(t *testing.T) { file := "testdata/src/go117.go" - prog, want, err := testProg(t, file, ssa.BuilderMode(0)) + prog, want, err := testProg(t, file) if err != nil { t.Fatalf("couldn't load test file '%s': %s", file, err) }
diff --git a/go/packages/gopackages/main.go b/go/packages/gopackages/main.go index 7ec0bdc..c6693a9 100644 --- a/go/packages/gopackages/main.go +++ b/go/packages/gopackages/main.go
@@ -16,48 +16,43 @@ "go/types" "log" "os" + "runtime/pprof" + "runtime/trace" "sort" "strings" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/drivertest" - "golang.org/x/tools/internal/tool" ) func main() { drivertest.RunIfChild() - tool.Main(context.Background(), &application{Mode: "imports"}, os.Args[1:]) -} -type application struct { - // Embed the basic profiling flags supported by the tool package - tool.Profile + var ( + cpuprofile = flag.String("profile.cpu", "", "write CPU profile to this file") + memprofile = flag.String("profile.mem", "", "write memory profile to this file") + traceprofile = flag.String("profile.trace", "", "write trace log to this file") + ) - Deps bool `flag:"deps" help:"show dependencies too"` - Test bool `flag:"test" help:"include any tests implied by the patterns"` - Mode string `flag:"mode" help:"mode (one of files, imports, types, syntax, allsyntax)"` - Tags string `flag:"tags" help:"comma-separated list of extra build tags (see: go help buildconstraint)"` - Private bool `flag:"private" help:"show non-exported declarations too (if -mode=syntax)"` - PrintJSON bool `flag:"json" help:"print package in JSON form"` - BuildFlags stringListValue `flag:"buildflag" help:"pass argument to underlying build system (may be repeated)"` - Driver bool `flag:"driver" help:"use golist passthrough driver (for debugging driver issues)"` -} + var ( + deps = flag.Bool("deps", false, "show dependencies too") + test = flag.Bool("test", false, "include any tests implied by the patterns") + mode = flag.String("mode", "imports", "mode (one of files, imports, types, syntax, allsyntax)") + tags = flag.String("tags", "", "comma-separated list of extra build tags (see: go help buildconstraint)") + private = flag.Bool("private", false, "show non-exported declarations too (if -mode=syntax)") + printJSON = flag.Bool("json", false, "print package in JSON form") + driver = flag.Bool("driver", false, "use golist passthrough driver (for debugging driver issues)") + buildFlags stringListValue + ) + flag.Var(&buildFlags, "buildflag", "pass argument to underlying build system (may be repeated)") -// Name implements tool.Application returning the binary name. -func (app *application) Name() string { return "gopackages" } + flag.Usage = func() { + fmt.Fprint(flag.CommandLine.Output(), `gopackages loads, parses, type-checks, and prints one or more Go packages. -// Usage implements tool.Application returning empty extra argument usage. -func (app *application) Usage() string { return "package..." } +Usage: + gopackages [flags] package... -// ShortHelp implements tool.Application returning the main binary help. -func (app *application) ShortHelp() string { - return "gopackages loads, parses, type-checks, and prints one or more Go packages." -} - -// DetailedHelp implements tool.Application returning the main binary help. -func (app *application) DetailedHelp(f *flag.FlagSet) { - fmt.Fprint(f.Output(), ` Packages are specified using the notation of "go list", or other underlying build system. @@ -78,30 +73,67 @@ Flags: `) - f.PrintDefaults() -} - -// Run takes the args after flag processing and performs the specified query. -func (app *application) Run(ctx context.Context, args ...string) error { - if len(args) == 0 { - return tool.CommandLineErrorf("not enough arguments") + flag.PrintDefaults() } + flag.Parse() + if flag.NArg() == 0 { + flag.Usage() + os.Exit(2) + } + + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + + if *traceprofile != "" { + f, err := os.Create(*traceprofile) + if err != nil { + log.Fatal(err) + } + trace.Start(f) + defer trace.Stop() + } + + if *memprofile != "" { + defer func() { + f, err := os.Create(*memprofile) + if err != nil { + log.Fatal(err) + } + pprof.WriteHeapProfile(f) + f.Close() + }() + } + + if err := run(context.Background(), flag.Args(), *deps, *test, *mode, *tags, *private, *printJSON, *driver, buildFlags); err != nil { + fmt.Fprintf(os.Stderr, "gopackages: %v\n", err) + os.Exit(1) + } +} + +func run(ctx context.Context, args []string, deps, test bool, mode, tags string, private, printJSON, driver bool, buildFlags []string) error { env := os.Environ() - if app.Driver { + if driver { env = append(env, drivertest.Env(log.Default())...) } // Load, parse, and type-check the packages named on the command line. cfg := &packages.Config{ Mode: packages.LoadSyntax, - Tests: app.Test, - BuildFlags: append([]string{"-tags=" + app.Tags}, app.BuildFlags...), + Tests: test, + BuildFlags: append([]string{"-tags=" + tags}, buildFlags...), Env: env, + Context: ctx, } // -mode flag - switch strings.ToLower(app.Mode) { + switch strings.ToLower(mode) { case "files": cfg.Mode = packages.LoadFiles case "imports": @@ -113,7 +145,7 @@ case "allsyntax": cfg.Mode = packages.LoadAllSyntax default: - return tool.CommandLineErrorf("invalid mode: %s", app.Mode) + return fmt.Errorf("invalid mode: %s", mode) } cfg.Mode |= packages.NeedModule @@ -123,7 +155,7 @@ } // -deps: print dependencies too. - if app.Deps { + if deps { // We can't use packages.All because // we need an ordered traversal. var all []*packages.Package // postorder @@ -153,13 +185,13 @@ } for _, lpkg := range lpkgs { - app.print(lpkg) + printPkg(lpkg, printJSON, private) } return nil } -func (app *application) print(lpkg *packages.Package) { - if app.PrintJSON { +func printPkg(lpkg *packages.Package, printJSON, private bool) { + if printJSON { data, _ := json.MarshalIndent(lpkg, "", "\t") os.Stdout.Write(data) return @@ -225,14 +257,14 @@ scope := lpkg.Types.Scope() for _, name := range scope.Names() { obj := scope.Lookup(name) - if !obj.Exported() && !app.Private { + if !obj.Exported() && !private { continue // skip unexported names } fmt.Printf("\t%s\n", types.ObjectString(obj, qual)) if _, ok := obj.(*types.TypeName); ok { for _, meth := range typeutil.IntuitiveMethodSet(obj.Type(), nil) { - if !meth.Obj().Exported() && !app.Private { + if !meth.Obj().Exported() && !private { continue // skip unexported names } fmt.Printf("\t%s\n", types.SelectionString(meth, qual))
diff --git a/go/packages/packages.go b/go/packages/packages.go index de68368..1e5549a 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go
@@ -815,6 +815,12 @@ needsrc: needsrc, goVersion: response.GoVersion, } + // Don't trust the driver to respond with duplicate-free + // package names (go.dev/issue/63822). + if _, ok := ld.pkgs[lpkg.ID]; ok { + return nil, fmt.Errorf("%s response contained duplicate packages for ID %q", + cond(ld.externalDriver, "go/packages driver", "go list"), lpkg.ID) + } ld.pkgs[lpkg.ID] = lpkg if rootIndex >= 0 { initial[rootIndex] = lpkg @@ -1589,3 +1595,11 @@ } type unit struct{} + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +}
diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 102eb48..7d6cffa 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go
@@ -13,10 +13,13 @@ "go/parser" "go/token" "go/types" + "io" + "log" "os" "os/exec" "path/filepath" "reflect" + "regexp" "runtime" "slices" "sort" @@ -37,7 +40,19 @@ func TestMain(m *testing.M) { testenv.ExitIfSmallMachine() - os.Exit(m.Run()) + // This executable also behaves as various flavors of + // GOPACKAGESDRIVER command for some tests, based on ENTRYPOINT. + entrypoint := os.Getenv("ENTRYPOINT") + switch entrypoint { + case "": + os.Exit(m.Run()) + case "driver1": + driver1() + case "driver2": + driver2() + default: + log.Fatalf("unexpected ENTRYPOINT %q", entrypoint) + } } func skipIfShort(t *testing.T, reason string) { @@ -1621,111 +1636,174 @@ } -func TestConfigDefaultEnv(t *testing.T) { +// TestDriver tests various configurations of default driver +// (go list) and alternative drivers specified explicitly via +// GOPACKAGESDRIVER or implicitly via $(which gopackagesdriver). +func TestDriver(t *testing.T) { // packagestest.TestAll instead of testAllOrModulesParallel because this test // can't be parallelized (it modifies the environment). - packagestest.TestAll(t, testConfigDefaultEnv) + packagestest.TestAll(t, testDriver) } -func testConfigDefaultEnv(t *testing.T, exporter packagestest.Exporter) { - const driverJSON = `{ - "Roots": ["gopackagesdriver"], - "Packages": [{"ID": "gopackagesdriver", "Name": "gopackagesdriver"}] -}` - var ( - pathKey string - driverScript packagestest.Writer - ) - switch runtime.GOOS { - case "android": - t.Skip("doesn't run on android") - case "windows": - // TODO(jayconrod): write an equivalent batch script for windows. - // Hint: "type" can be used to read a file to stdout. - t.Skip("test requires sh") - case "plan9": - pathKey = "path" - driverScript = packagestest.Script(`#!/bin/rc -cat <<'EOF' -` + driverJSON + ` -EOF -`) - default: - pathKey = "PATH" - driverScript = packagestest.Script(`#!/bin/sh +func testDriver(t *testing.T, exporter packagestest.Exporter) { + testenv.NeedsTool(t, "go") -cat - <<'EOF' -` + driverJSON + ` -EOF -`) + // Copy the current test executable to a temp dir. + // We'll use it as a GOPACKAGESDRIVER. + exe, err := os.Executable() + if err != nil { + t.Fatal(err) } - exported := packagestest.Export(t, exporter, []packagestest.Module{{ - Name: "golang.org/fake", - Files: map[string]any{ - "bin/gopackagesdriver": driverScript, - "golist/golist.go": "package golist", - }}}) - defer exported.Cleanup() - driver := exported.File("golang.org/fake", "bin/gopackagesdriver") - binDir := filepath.Dir(driver) - if err := os.Chmod(driver, 0755); err != nil { + gopackagesdriver := filepath.Join(t.TempDir(), "gopackagesdriver"+cond(runtime.GOOS == "windows", ".exe", "")) + if err := copyfile(gopackagesdriver, exe); err != nil { t.Fatal(err) } - path, ok := os.LookupEnv(pathKey) - var pathWithDriver string - if ok { - pathWithDriver = binDir + string(os.PathListSeparator) + path - } else { - pathWithDriver = binDir + sanitize := func(s string) string { + return strings.ReplaceAll(s, filepath.Dir(gopackagesdriver), "PATH") } + + exported := packagestest.Export(t, exporter, []packagestest.Module{{ + Name: "golang.org/fake", + Files: map[string]any{ + "golist/golist.go": "package golist", + }}}) + defer exported.Cleanup() + for _, test := range []struct { - desc string - path string - driver string - wantIDs string + desc string + onpath bool // whether gopackagesdriver's directory is on the PATH + gopackagesdriver string // value of GOPACKAGESDRIVER seen by packages.Load ("" and unset are equivalent) + entrypoint string // value of ENTRYPOINT var to be seen by driver executable + want string // expected list of package IDs, or "error: regexp" matching message }{ + // -- tests of driver selection -- { - desc: "driver_off", - path: pathWithDriver, - driver: "off", - wantIDs: "[golist]", - }, { - desc: "driver_unset", - path: pathWithDriver, - driver: "", - wantIDs: "[gopackagesdriver]", - }, { - desc: "driver_set", - path: "", - driver: driver, - wantIDs: "[gopackagesdriver]", + // GOPACKAGESDRIVER unset, no driver on path: executes go list. + desc: "default", + want: "[golist]", + }, + { + // GOPACKAGESDRIVER=off executes go list, not driver on the PATH. + desc: "driver-explicitly-off", + onpath: true, + gopackagesdriver: "off", + entrypoint: "nope", + want: "[golist]", + }, + { + // GOPACKAGESDRIVER=exe runs the specified executable. + desc: "driver-set-via-environment", + onpath: false, + gopackagesdriver: gopackagesdriver, + entrypoint: "driver1", + want: "[gopackagesdriver]", + }, + { + // GOPACKAGESDRIVER set to nonexistent does not look up 'gopackagesdriver' on the PATH + desc: "driver-not-found-on-path", + onpath: true, + gopackagesdriver: gopackagesdriver + "nonesuch", + entrypoint: "driver1", + want: "error: no such file|file does not exist", + }, + // -- tests of driver response payload -- + { + // GOPACKAGESDRIVER denotes a driver that returns duplicate IDs + desc: "driver-returns-duplicate-IDs", + gopackagesdriver: gopackagesdriver, + entrypoint: "driver2", + want: `error: go/packages driver response contained duplicate packages for ID "a"`, }, } { t.Run(test.desc, func(t *testing.T) { - oldPath := os.Getenv(pathKey) - os.Setenv(pathKey, test.path) - defer os.Setenv(pathKey, oldPath) - // Clone exported.Config - config := exported.Config + // Defend against recursive exec due to forgotten entrypoint. + if test.gopackagesdriver != "" && test.entrypoint == "" { + t.Fatalf("missing entrypoint") + } + + if test.onpath { + pathVar := cond(runtime.GOOS == "plan9", "path", "PATH") + t.Setenv(pathVar, filepath.Dir(gopackagesdriver)+string(os.PathListSeparator)+os.Getenv(pathVar)) + } + + // Clone and update the config. + config := *exported.Config config.Env = slices.Clone(exported.Config.Env) - config.Env = append(config.Env, "GOPACKAGESDRIVER="+test.driver) - pkgs, err := packages.Load(exported.Config, "golist") + config.Env = append(config.Env, + "GOPACKAGESDRIVER="+test.gopackagesdriver, + "ENTRYPOINT="+test.entrypoint) + + pattern, wantErr := strings.CutPrefix(test.want, "error: ") + + // We request golist, but that's + // not necessarily what we get back. + pkgs, err := packages.Load(&config, "golist") if err != nil { - t.Fatal(err) + errStr := sanitize(err.Error()) + if wantErr { + if m, err := regexp.MatchString(pattern, errStr); err != nil { + t.Fatalf("bad 'want' pattern: %v", err) + } else if m { + return // success + } + t.Fatalf("Load failed with wrong error: %q, want match for %q", errStr, pattern) + } + t.Fatalf("Load failed: %v", errStr) + } + if wantErr { + t.Errorf("Load succeeded unexpectedly, want error matching %q", pattern) } gotIds := make([]string, len(pkgs)) for i, pkg := range pkgs { gotIds[i] = pkg.ID } - if fmt.Sprint(pkgs) != test.wantIDs { - t.Errorf("got %v; want %v", gotIds, test.wantIDs) + got := sanitize(fmt.Sprint(pkgs)) + if got != test.want { + t.Errorf("got %s, want %s", got, test.want) } }) } } +// driver1 returns a single package. +func driver1() { + fmt.Println(`{"Roots": ["gopackagesdriver"], "Packages": [{"ID": "gopackagesdriver", "Name": "gopackagesdriver"}]}`) +} + +// driver2 returns two package with the same ID. +func driver2() { + fmt.Println(`{"Roots": ["a", "b", "c"], "Packages": [ +{"ID": "a", "Name": "a"}, +{"ID": "b", "Name": "b"}, +{"ID": "c", "Name": "c"}, +{"ID": "a", "Name": "a"} +]}`) +} + +// copyfile copies a file from src to dst. +func copyfile(dst, src string) error { + srcf, err := os.Open(src) + if err != nil { + return err + } + defer srcf.Close() + fi, err := srcf.Stat() + if err != nil { + return err + } + dstf, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, fi.Mode()) + if err != nil { + return err + } + if _, err := io.Copy(dstf, srcf); err != nil { + dstf.Close() // ignore error + return err + } + return dstf.Close() +} + // This test that a simple x test package layout loads correctly. // There was a bug in go list where it returned multiple copies of the same // package (specifically in this case of golang.org/fake/a), and this triggered @@ -2941,15 +3019,48 @@ } } -func TestEmptyEnvironment(t *testing.T) { - t.Parallel() +// TestConfigEnvDoesNotInheritProcessEnv tests that when Config.Env is non-nil +// and doesn't contain os.Environ(), packages.Load doesn't inherit the process +// environment. +func TestConfigEnvDoesNotInheritProcessEnv(t *testing.T) { + testenv.NeedsGoPackages(t) - cfg := &packages.Config{ - Env: []string{"FOO=BAR"}, + dir := writeTree(t, ` +-- go.mod -- +module example.com + +go 1.18 + +-- p/p.go -- +package p + +-- p/tagged.go -- +//go:build fromparent +// +build fromparent + +package p +`) + + t.Setenv("GOFLAGS", "-tags=fromparent") + + pkgs, err := packages.Load(&packages.Config{ + Dir: dir, + Mode: packages.NeedFiles, + Env: []string{ + "PATH=" + os.Getenv("PATH"), + "GOCACHE=" + t.TempDir(), + "GOPACKAGESDRIVER=off", + "GOWORK=off", + }, + }, "./p") + if err != nil { + t.Fatal(err) } - _, err := packages.Load(cfg, "fmt") - if err == nil { - t.Fatal("Load with explicitly empty environment should fail") + if len(pkgs) != 1 { + t.Fatalf("Load returned %d packages, want 1", len(pkgs)) + } + if got, want := strings.Join(srcs(pkgs[0]), " "), "p.go"; got != want { + t.Fatalf("GoFiles = %s, want %s", got, want) } } @@ -3590,3 +3701,11 @@ t.Errorf("types.Info for package unsafe has one or more nil maps: %#v", *info) } } + +func cond[T any](cond bool, t, f T) T { + if cond { + return t + } else { + return f + } +}
diff --git a/go/ssa/builder.go b/go/ssa/builder.go index 2420749..1669d80 100644 --- a/go/ssa/builder.go +++ b/go/ssa/builder.go
@@ -583,33 +583,6 @@ } return } - - if _, ok := loc.(*address); ok { - if isNonTypeParamInterface(loc.typ()) { - // e.g. var x interface{} = T{...} - // Can't in-place initialize an interface value. - // Fall back to copying. - } else { - // x = T{...} or x := T{...} - addr := loc.address(fn) - if sb != nil { - b.compLit(fn, addr, e, isZero, sb) - } else { - var sb storebuf - b.compLit(fn, addr, e, isZero, &sb) - sb.emit(fn) - } - - // Subtle: emit debug ref for aggregate types only; - // slice and map are handled by store ops in compLit. - switch typeparams.CoreType(loc.typ()).(type) { - case *types.Struct, *types.Array: - emitDebugRef(fn, e, addr, true) - } - - return - } - } } // simple case: just copy @@ -825,8 +798,8 @@ } callee := v.(*Function) // (func) if callee.typeparams.Len() > 0 { - targs := fn.subst.types(instanceArgs(fn.info, e)) - callee = callee.instance(targs, b) + targs := fn.subtargs(e) + callee = callee.instance(nil, targs, b) } return callee } @@ -847,15 +820,16 @@ case types.MethodExpr: // (*T).f or T.f, the method f from the method-set of type T. // The result is a "thunk". - thunk := createThunk(fn.Prog, sel) + targs := fn.subtargs(e.Sel) + thunk := createThunk(fn.Prog, sel, targs) b.enqueue(thunk) - return emitConv(fn, thunk, fn.typ(tv.Type)) + return thunk case types.MethodVal: // e.f where e is an expression and f is a method. // The result is a "bound". - obj := sel.obj.(*types.Func) - rt := fn.typ(recvType(obj)) + m := sel.obj.(*types.Func) + rt := fn.typ(recvType(m)) wantAddr := isPointer(rt) escaping := true v := b.receiver(fn, e.X, wantAddr, escaping, sel) @@ -886,11 +860,13 @@ emitTypeAssert(fn, v, rt, e.Sel.Pos()) } } - if targs := receiverTypeArgs(obj); len(targs) > 0 { - // obj is generic. - obj = fn.Prog.canon.instantiateMethod(obj, fn.subst.types(targs), fn.Prog.ctxt) + + if rtargs := fn.subrtargs(m); len(rtargs) > 0 { + m = fn.Prog.canon.instantiateMethod(m, rtargs, fn.Prog.ctxt) } - bound := createBound(fn.Prog, obj) + + targs := fn.subtargs(e.Sel) + bound := createBound(fn.Prog, m, targs) b.enqueue(bound) // The assignment may widen a type parameter to its @@ -902,7 +878,7 @@ Bindings: []Value{v}, } c.setPos(e.Sel.Pos()) - c.setType(fn.typ(tv.Type)) + c.setType(bound.Signature) return fn.emit(c) case types.FieldVal: @@ -1015,8 +991,15 @@ func (b *builder) setCallFunc(fn *Function, e *ast.CallExpr, c *CallCommon) { c.pos = e.Lparen - // Is this a method call? - if selector, ok := ast.Unparen(e.Fun).(*ast.SelectorExpr); ok { + // Is this a (possibly generic) method call? + m := ast.Unparen(e.Fun) + switch e := m.(type) { + case *ast.IndexExpr: + m = e.X + case *ast.IndexListExpr: + m = e.X + } + if selector, ok := m.(*ast.SelectorExpr); ok { sel := fn.selection(selector) if sel != nil && sel.kind == types.MethodVal { obj := sel.obj.(*types.Func) @@ -1031,7 +1014,8 @@ c.Method = obj } else { // "Call"-mode call. - c.Value = fn.Prog.objectMethod(obj, b) + targs := fn.subtargs(selector.Sel) + c.Value = fn.Prog.objectMethod(obj, targs, b) c.Args = append(c.Args, v) } return
diff --git a/go/ssa/builder_generic_test.go b/go/ssa/builder_generic_test.go index 851a53a..5a8a796 100644 --- a/go/ssa/builder_generic_test.go +++ b/go/ssa/builder_generic_test.go
@@ -622,7 +622,7 @@ print(i, 0) } - //@ instrs("f1", "*ssa.Alloc", "local T (u)") + //@ instrs("f1", "*ssa.Alloc", "local T (complit)") //@ instrs("f1", "*ssa.FieldAddr", "&t0.x [#0]") func f1[T ~struct{ x string }]() T { u := T{"lorem"} @@ -688,15 +688,15 @@ } //@ instrs("f10", "*ssa.FieldAddr", "&t0.x [#0]") - //@ instrs("f10", "*ssa.Store", "*t0 = *new(T):T", "*t1 = 4:int") + //@ instrs("f10", "*ssa.Store", "*t1 = 4:int") func f10[T ~struct{ x, y int }]() T { var u T u = T{x: 4} return u } - //@ instrs("f11", "*ssa.FieldAddr", "&t1.y [#1]") - //@ instrs("f11", "*ssa.Store", "*t1 = *new(T):T", "*t2 = 5:int") + //@ instrs("f11", "*ssa.FieldAddr", "&t2.y [#1]") + //@ instrs("f11", "*ssa.Store", "*t1 = t4", "*t3 = 5:int") func f11[T ~struct{ x, y int }, PT *T]() PT { var u PT = new(T) *u = T{y: 5} @@ -707,8 +707,8 @@ //@ instrs("f12", "*ssa.MakeMap", "make map[P]bool 1:int") func f12[T any, P *struct{f T}](x T) map[P]bool { return map[P]bool{{}: true} } - //@ instrs("f13", "*ssa.IndexAddr", "&v[0:int]") - //@ instrs("f13", "*ssa.Store", "*t0 = 7:int", "*v = *new(A):A") + //@ instrs("f13", "*ssa.IndexAddr", "&t0[0:int]") + //@ instrs("f13", "*ssa.Store", "*t1 = 7:int", "*v = t2") func f13[A [3]int, PA *A](v PA) { *v = A{7} }
diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index adb04cd..c02c222 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go
@@ -270,6 +270,21 @@ {`package N; var g interface{}; func f[S any]() { var v []S; g = v }; `, nil, }, + // ...including a parameterized type boxed by a closure in the body of a method of a generic type (go.dev/issue/80055). + {`package O; type box[N any] struct{ n N }; type holder[N any] struct{}; func (h *holder[N]) get() any { f := func() any { return &box[N]{} }; return f() }`, + nil, + }, + // The type []T within generic F[T] is parameterized, so not a RuntimeType. + // But []T within instance f[T=C] is a ground type, so a RuntimeType. + {`package P +var _ = G[C] +func G[U any]() { F[U]() } +func F[T any]() any { return []T{} } +type C struct{} +func (C) f() +`, + []string{"*p.C", "p.C"}, + }, } for _, test := range tests { // Parse the file. @@ -282,7 +297,7 @@ // Create a single-file main package. // Load dependencies from gc binary export data. - mode := ssa.SanityCheckFunctions + mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics ssapkg, _, err := ssautil.BuildPackage(&types.Config{Importer: importer.Default()}, fset, types.NewPackage("p", ""), []*ast.File{f}, mode) if err != nil { @@ -598,19 +613,19 @@ "bound", "*func() int", "(p.S[int]).M$bound", - "(p.S[int]).M[int]", + "(p.S[int]).M", }, { "thunk", "*func(p.S[int]) int", "(p.S[int]).M$thunk", - "(p.S[int]).M[int]", + "(p.S[int]).M", }, { "indirect", "*func(p.R[int]) int", "(p.R[int]).M$thunk", - "(p.S[int]).M[int]", + "(p.S[int]).M", }, } { t.Run(entry.name, func(t *testing.T) { @@ -822,7 +837,7 @@ } sort.Strings(callees) // ignore the order in the code. - want := "[example.com/a.F[int] example.com/a.G[int string] example.com/a.H[int]]" + want := "[example.com/a.F[int] example.com/a.G[int, string] example.com/a.H[int]]" if got := fmt.Sprint(callees); got != want { t.Errorf("Expected main() to contain calls %v. got %v", want, got) } @@ -1047,6 +1062,7 @@ "issue66783b", "issue73594", "issue78110", + "issue73871", } { t.Run(name, func(t *testing.T) { base := name + ".go"
diff --git a/go/ssa/create.go b/go/ssa/create.go index d94cb6f..3d20e55 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go
@@ -115,33 +115,26 @@ func createFunction(prog *Program, obj *types.Func, name string, syntax ast.Node, info *types.Info, goversion string) *Function { sig := obj.Type().(*types.Signature) - // Collect type parameters. - var tparams *types.TypeParamList - if rtparams := sig.RecvTypeParams(); rtparams.Len() > 0 { - tparams = rtparams // method of generic type - } else if sigparams := sig.TypeParams(); sigparams.Len() > 0 { - tparams = sigparams // generic function - } - /* declared function/method (from syntax or export data) */ fn := &Function{ - name: name, - object: obj, - Signature: sig, - build: (*builder).buildFromSyntax, - syntax: syntax, - info: info, - goversion: goversion, - pos: obj.Pos(), - Pkg: nil, // may be set by caller - Prog: prog, - typeparams: tparams, + name: name, + object: obj, + Signature: sig, + build: (*builder).buildFromSyntax, + syntax: syntax, + info: info, + goversion: goversion, + pos: obj.Pos(), + Pkg: nil, // may be set by caller + Prog: prog, + recvtypeparams: sig.RecvTypeParams(), + typeparams: sig.TypeParams(), } if fn.syntax == nil { fn.Synthetic = "from type information" fn.build = (*builder).buildParamsOnly } - if tparams.Len() > 0 { + if fn.hasTypeParams() { fn.generic = new(generic) } return fn
diff --git a/go/ssa/emit.go b/go/ssa/emit.go index 31aa5de..fd54b3d 100644 --- a/go/ssa/emit.go +++ b/go/ssa/emit.go
@@ -248,7 +248,7 @@ // Record the types of operands to MakeInterface, if // non-parameterized, as they are the set of runtime types. t := val.Type() - if f.typeparams.Len() == 0 || !f.Prog.isParameterized(t) { + if !f.Prog.isParameterized(t) { addMakeInterfaceType(f.Prog, t) }
diff --git a/go/ssa/generic_methods_test.go b/go/ssa/generic_methods_test.go new file mode 100644 index 0000000..72e07cf --- /dev/null +++ b/go/ssa/generic_methods_test.go
@@ -0,0 +1,405 @@ +// 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 ssa_test + +import ( + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "testing" + + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/internal/testenv" +) + +// generic +const g = ` +package p + +type G[P any] struct { + x P +} + +func (g G[P]) M[Q any](q Q) (P, Q) { + return g.x, q +} + +func (g *G[P]) N[Q any](q Q) (P, Q) { + return g.x, q +} + +func f() { + g := G[int]{ x: 42 } + %s +} +` + +// non-generic +const n = ` +package p + +type N struct{} + +func (N) M[P any](p P) P { + return p +} + +func (*N) N[P any](p P) P { + return p +} + +func f() { + n := N{} + %s +} +` + +// TestGenericMethods ensures that generic methods are properly instantiated. +func TestGenericMethods(t *testing.T) { + testenv.NeedsGoCommand1Point(t, 27) + + // TODO(mark): include implicit type arguments for direct callees? + // TODO(mark): ensure tpars == targs? + testCalls := []struct { + prog string + stmts string + callee string + tparams int // number of type parameters + targs int // number of type arguments + }{ + // value receivers on generic types + { + prog: g, + stmts: "g.M[bool](true)", + callee: "(p.G[int]).M[bool]", + tparams: 2, + targs: 2, + }, + { + // same as previous but with inferred type arguments + prog: g, + stmts: "g.M(true)", + callee: "(p.G[int]).M[bool]", + tparams: 2, + targs: 2, + }, + { + prog: g, + stmts: "f := g.M[bool]; f(true)", + callee: "(p.G[int]).M[bool]$bound", + tparams: 0, // not propagated + targs: 1, // receiver eagerly specialized + }, + { + // same as previous but with inferred type arguments + prog: g, + stmts: "var f func(bool) (int, bool); f = g.M; f(true)", + callee: "(p.G[int]).M[bool]$bound", + tparams: 0, + targs: 1, + }, + { + prog: g, + stmts: "f := G[int].M[bool]; f(g, true)", + callee: "(p.G[int]).M[bool]$thunk", + tparams: 0, // not propagated + targs: 1, // receiver eagerly specialized + }, + { + // same as previous but with inferred type arguments + prog: g, + stmts: "var f func(G[int], bool) (int, bool); f = (G[int]).M; f(g, true)", + callee: "(p.G[int]).M[bool]$thunk", + tparams: 0, + targs: 1, + }, + // pointer receivers on generic types + { + prog: g, + stmts: "g.N[bool](true)", + callee: "(*p.G[int]).N[bool]", + tparams: 2, + targs: 2, + }, + { + // same as previous but with inferred type arguments + prog: g, + stmts: "g.N(true)", + callee: "(*p.G[int]).N[bool]", + tparams: 2, + targs: 2, + }, + { + prog: g, + stmts: "f := g.N[bool]; f(true)", + callee: "(*p.G[int]).N[bool]$bound", + tparams: 0, // not propagated + targs: 1, // receiver eagerly specialized + }, + { + // same as previous but with inferred type arguments + prog: g, + stmts: "var f func(bool) (int, bool); f = g.N; f(true)", + callee: "(*p.G[int]).N[bool]$bound", + tparams: 0, + targs: 1, + }, + { + prog: g, + stmts: "f := (*G[int]).N[bool]; f(&g, true)", + callee: "(*p.G[int]).N[bool]$thunk", + tparams: 0, // not propagated + targs: 1, // receiver eagerly specialized + }, + { + // same as previous but with inferred type arguments + prog: g, + stmts: "var f func(*G[int], bool) (int, bool); f = (*G[int]).N[bool]; f(&g, true)", + callee: "(*p.G[int]).N[bool]$thunk", + tparams: 0, + targs: 1, + }, + // value receivers on non-generic types + { + prog: n, + stmts: "n.M[bool](true)", + callee: "(p.N).M[bool]", + tparams: 1, + targs: 1, + }, + { + // same as previous but with inferred type arguments + prog: n, + stmts: "n.M(true)", + callee: "(p.N).M[bool]", + tparams: 1, + targs: 1, + }, + { + prog: n, + stmts: "f := n.M[bool]; f(true)", + callee: "(p.N).M[bool]$bound", + tparams: 0, // not propagated + targs: 1, + }, + { + // same as previous but with inferred type arguments + prog: n, + stmts: "var f func(bool) bool; f = n.M; f(true)", + callee: "(p.N).M[bool]$bound", + tparams: 0, + targs: 1, + }, + { + prog: n, + stmts: "f := N.M[bool]; f(n, true)", + callee: "(p.N).M[bool]$thunk", + tparams: 0, // not propagated + targs: 1, + }, + { + // same as previous but with inferred type arguments + prog: n, + stmts: "var f func(N, bool) bool; f = N.M; f(n, true)", + callee: "(p.N).M[bool]$thunk", + tparams: 0, + targs: 1, + }, + // pointer receivers on non-generic types + { + prog: n, + stmts: "n.N[bool](true)", + callee: "(*p.N).N[bool]", + tparams: 1, + targs: 1, + }, + { + // same as previous but with inferred type arguments + prog: n, + stmts: "n.N(true)", + callee: "(*p.N).N[bool]", + tparams: 1, + targs: 1, + }, + { + prog: n, + stmts: "f := n.N[bool]; f(true)", + callee: "(*p.N).N[bool]$bound", + tparams: 0, // not propagated + targs: 1, + }, + { + // same as previous but with inferred type arguments + prog: n, + stmts: "var f func(bool) bool; f = n.N[bool]; f(true)", + callee: "(*p.N).N[bool]$bound", + tparams: 0, + targs: 1, + }, + { + prog: n, + stmts: "f := (*N).N[bool]; f(&n, true)", + callee: "(*p.N).N[bool]$thunk", + tparams: 0, // not propagated + targs: 1, + }, + { + // same as previous but with inferred type arguments + prog: n, + stmts: "var f func(*N, bool) bool; f = (*N).N; f(&n, true)", + callee: "(*p.N).N[bool]$thunk", + tparams: 0, + targs: 1, + }, + // bounds of instantiated signatures which only differ by receiver pointerness + { + prog: g, + stmts: "f := g.M[bool]; _ = g.N[bool]; f(true)", + callee: "(p.G[int]).M[bool]$bound", + tparams: 0, // not propagated + targs: 1, // receiver eagerly specialized + }, + { + prog: g, + stmts: "_ = g.M[bool]; f := g.N[bool]; f(true)", + callee: "(*p.G[int]).N[bool]$bound", + tparams: 0, // not propagated + targs: 1, // receiver eagerly specialized + }, + { + prog: n, + stmts: "f := n.M[bool]; _ = n.N[bool]; f(true)", + callee: "(p.N).M[bool]$bound", + tparams: 0, // not propagated + targs: 1, + }, + { + prog: n, + stmts: "_ = n.M[bool]; f := n.N[bool]; f(true)", + callee: "(*p.N).N[bool]$bound", + tparams: 0, // not propagated + targs: 1, + }, + // thunks of instantiated signatures which only differ by receiver pointerness + { + prog: g, + stmts: "f := G[int].M[bool]; _ = (*G[int]).N[bool]; f(g, true)", + callee: "(p.G[int]).M[bool]$thunk", + tparams: 0, // not propagated + targs: 1, // receiver eagerly specialized + }, + { + prog: g, + stmts: "_ = G[int].M[bool]; f := (*G[int]).N[bool]; f(&g, true)", + callee: "(*p.G[int]).N[bool]$thunk", + tparams: 0, // not propagated + targs: 1, // receiver eagerly specialized + }, + { + prog: n, + stmts: "f := N.M[bool]; _ = (*N).N[bool]; f(n, true)", + callee: "(p.N).M[bool]$thunk", + tparams: 0, // not propagated + targs: 1, + }, + { + prog: n, + stmts: "_ = N.M[bool]; f := (*N).N[bool]; f(&n, true)", + callee: "(*p.N).N[bool]$thunk", + tparams: 0, + targs: 1, + }, + } + + for _, want := range testCalls { + prog := fmt.Sprintf(want.prog, want.stmts) + calls := getCalls(t, build(t, prog)) + + if len(calls) != 1 { + t.Fatalf("too many direct calls for %s: got %d, want 1", prog, len(calls)) + } + + got := calls[0] + if got.callee != want.callee { + t.Errorf("wrong callee for %s: got %s, want %s", prog, got.callee, want.callee) + } + if got.tparams != want.tparams { + t.Errorf("wrong number of tparams for %s: got %d, want %d", prog, got.tparams, want.tparams) + } + if got.targs != want.targs { + t.Errorf("wrong number of targs for %s: got %d, want %d", prog, got.targs, want.targs) + } + } + + testBuilds := []string{ + // instantiate same signature with value / pointer receivers + fmt.Sprintf(g, "_ = g.M[bool]; _ = g.N[bool]"), + fmt.Sprintf(n, "_ = n.M[bool]; _ = n.N[bool]"), + } + + for _, prog := range testBuilds { + build(t, prog) + } +} + +type call struct { + callee string + tparams int + targs int +} + +func build(t *testing.T, src string) *ssa.Package { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", src, 0) + if err != nil { + t.Fatal(err) + } + + conf := types.Config{Importer: importer.Default()} + info := &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Instances: make(map[*ast.Ident]types.Instance), + } + pkg, err := conf.Check("p", fset, []*ast.File{f}, info) + if err != nil { + t.Fatal(err) + } + + prog := ssa.NewProgram(fset, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) + p := prog.CreatePackage(pkg, []*ast.File{f}, info, true) + prog.Build() + + return p +} + +func getCalls(t *testing.T, p *ssa.Package) []*call { + fun := p.Func("f") + if fun == nil { + t.Fatal("f not found") + } + + var calls []*call + for _, block := range fun.Blocks { + for _, instr := range block.Instrs { + if c, ok := instr.(*ssa.Call); ok { + fn := c.Call.StaticCallee() + calls = append(calls, &call{ + callee: fn.String(), + tparams: fn.TypeParams().Len(), + targs: len(fn.TypeArgs()), + }) + } + // don't care about the extra call for MakeClosure + } + } + + return calls +}
diff --git a/go/ssa/instantiate.go b/go/ssa/instantiate.go index 5862440..4eb53aa 100644 --- a/go/ssa/instantiate.go +++ b/go/ssa/instantiate.go
@@ -19,13 +19,13 @@ } // instance returns a Function that is the instantiation of generic -// origin function fn with the type arguments targs. +// origin function fn with the type arguments rtargs and targs. // // Any created instance is added to cr. // // Acquires fn.generic.instancesMu. -func (fn *Function) instance(targs []types.Type, b *builder) *Function { - key := fn.Prog.canon.List(targs) +func (fn *Function) instance(rtargs, targs []types.Type, b *builder) *Function { + key := fn.Prog.canon.List(slices.Concat(rtargs, targs)) gen := fn.generic @@ -33,7 +33,7 @@ defer gen.instancesMu.Unlock() inst, ok := gen.instances[key] if !ok { - inst = createInstance(fn, targs) + inst = createInstance(fn, rtargs, targs) inst.buildshared = b.shared() b.enqueue(inst) @@ -48,20 +48,46 @@ } // createInstance returns the instantiation of generic function fn using targs. +// If fn is a method on a generic type, fn's receiver type will be instantiated +// using rtargs. // // Requires fn.generic.instancesMu. -func createInstance(fn *Function, targs []types.Type) *Function { +func createInstance(fn *Function, rtargs, targs []types.Type) *Function { prog := fn.Prog // Compute signature. var sig *types.Signature var obj *types.Func if recv := fn.Signature.Recv(); recv != nil { - // method - obj = prog.canon.instantiateMethod(fn.object, targs, prog.ctxt) - sig = obj.Type().(*types.Signature) + // method, len(rtargs) > 0 || len(targs) > 0 + if len(rtargs) > 0 { + // possibly generic method on generic type + obj = prog.canon.instantiateMethod(fn.object, rtargs, prog.ctxt) + } else { + // generic method on non-generic type + obj = fn.object // instantiation does not exist yet + } + if len(targs) > 0 { + // generic method + instSig, err := types.Instantiate(prog.ctxt, obj.Signature(), targs, false) + if err != nil { + panic(err) + } + instance, ok := instSig.(*types.Signature) + if !ok { + panic("Instantiate of a Signature returned a non-signature") + } + // Do not canonicalize generic methods, because the receiver is not + // part of a Signature's type identity. For example, + // (*G[int]).m[int] and (G[int]).n[int] may be identical types even + // though their receiver types differ. + sig = instance + } else { + // non-generic method on generic type + sig = obj.Signature() + } } else { - // function + // function, len(rtargs) == 0 && len(targs) > 0 instSig, err := types.Instantiate(prog.ctxt, fn.Signature, targs, false) if err != nil { panic(err) @@ -80,10 +106,10 @@ subst *subster build buildFunc ) - if prog.mode&InstantiateGenerics != 0 && !prog.isParameterized(targs...) { + if prog.mode&InstantiateGenerics != 0 && !prog.isParameterized(slices.Concat(rtargs, targs)...) { synthetic = fmt.Sprintf("instance of %s", fn.Name()) if fn.syntax != nil { - subst = makeSubster(prog.ctxt, obj, fn.typeparams, targs) + subst = makeSubster(prog.ctxt, obj, fn.recvtypeparams, rtargs, fn.typeparams, targs) build = (*builder).buildFromSyntax } else { build = (*builder).buildParamsOnly @@ -93,9 +119,14 @@ build = (*builder).buildInstantiationWrapper } + name := fn.Name() + if len(targs) > 0 { + name = fmt.Sprintf("%s%s", name, targstr(targs)) // may not be unique + } + /* generic instance or instantiation wrapper */ return &Function{ - name: fmt.Sprintf("%s%s", fn.Name(), targs), // may not be unique + name: name, object: obj, Signature: sig, Synthetic: synthetic, @@ -107,6 +138,8 @@ pos: obj.Pos(), Pkg: nil, Prog: fn.Prog, + recvtypeparams: fn.recvtypeparams, // share with origin + recvtypeargs: rtargs, typeparams: fn.typeparams, // share with origin typeargs: targs, subst: subst,
diff --git a/go/ssa/instantiate_test.go b/go/ssa/instantiate_test.go index c82196f..8e0b9a5 100644 --- a/go/ssa/instantiate_test.go +++ b/go/ssa/instantiate_test.go
@@ -73,25 +73,34 @@ } intSliceTyp := types.NewSlice(types.Typ[types.Int]) - instance := instantiateLoadMethod(intSliceTyp) // (*Pointer[[]int]).Load - if instance.Origin() != meth { - t.Errorf("Expected Origin of %s to be %s. got %s", instance, meth, instance.Origin()) + inst1 := instantiateLoadMethod(intSliceTyp) // (*p.Pointer[[]int]).Load + if inst1.Origin() != meth { + t.Errorf("Expected Origin of %s to be %s. got %s", inst1, meth, inst1.Origin()) } - if len(instance.TypeArgs()) != 1 || !types.Identical(instance.TypeArgs()[0], intSliceTyp) { - t.Errorf("Expected TypeArgs of %s to be %v. got %v", instance, []types.Type{intSliceTyp}, instance.TypeArgs()) + if len(inst1.TypeArgs()) != 1 || !types.Identical(inst1.TypeArgs()[0], intSliceTyp) { + t.Errorf("Expected TypeArgs of %s to be %v. got %v", inst1, []types.Type{intSliceTyp}, inst1.TypeArgs()) } // A second request with an identical type returns the same Function. second := instantiateLoadMethod(types.NewSlice(types.Typ[types.Int])) - if second != instance { + if second != inst1 { t.Error("Expected second identical instantiation to be the same function") } - // (*Pointer[[]uint]).Load + // (*p.Pointer[[]uint]).Load inst2 := instantiateLoadMethod(types.NewSlice(types.Typ[types.Uint])) - if instance.Name() >= inst2.Name() { - t.Errorf("Expected name of instance %s to be before instance %v", instance, inst2) + if inst1.Name() != "Load" { + t.Errorf("Unexpected name of instance %s; got %s, want Load", inst1.String(), inst1.Name()) + } + if inst2.Name() != "Load" { + t.Errorf("Unexpected name of instance %s; got %s, want Load", inst1.String(), inst2.Name()) + } + if inst1.String() != "(*p.Pointer[[]int]).Load" { + t.Errorf("Unexpected string of first instance; got %s, want (*p.Pointer[[]int]).Load", inst1.String()) + } + if inst2.String() != "(*p.Pointer[[]uint]).Load" { + t.Errorf("Unexpected string of second instance; got %s, want (*p.Pointer[[]uint]).Load", inst2.String()) } } } @@ -263,7 +272,7 @@ instances string }{ {"H", "[p.H[T] p.H[T]]"}, - {"Foo", "[p.Foo[S T] p.Foo[T S]]"}, + {"Foo", "[p.Foo[S, T] p.Foo[T, S]]"}, } { t.Run(test.orig, func(t *testing.T) { f := p.Members[test.orig].(*ssa.Function)
diff --git a/go/ssa/interp/external.go b/go/ssa/interp/external.go index 9de53ff..5376793 100644 --- a/go/ssa/interp/external.go +++ b/go/ssa/interp/external.go
@@ -221,7 +221,10 @@ func extÛ°strconvÛ°Atoi(fr *frame, args []value) value { i, e := strconv.Atoi(args[0].(string)) if e != nil { - return tuple{i, iface{fr.i.runtimeErrorString, e.Error()}} + if fr.i.runtimeErrorString != nil { + return tuple{i, iface{fr.i.runtimeErrorString, e.Error()}} + } + return tuple{i, e.Error()} } return tuple{i, iface{}} }
diff --git a/go/ssa/interp/interp.go b/go/ssa/interp/interp.go index 1fd61a7..7e060cd 100644 --- a/go/ssa/interp/interp.go +++ b/go/ssa/interp/interp.go
@@ -87,7 +87,7 @@ reflectPackage *ssa.Package // the fake reflect package errorMethods methodSet // the method set of reflect.error, which implements the error interface. rtypeMethods methodSet // the method set of rtype, which implements the reflect.Type interface. - runtimeErrorString types.Type // the runtime.errorString type + runtimeErrorString types.Type // the runtime.errorString type (iff "runtime" is present) sizes types.Sizes // the effective type-sizing function goroutines int32 // atomically updated } @@ -692,10 +692,9 @@ goroutines: 1, } runtimePkg := i.prog.ImportedPackage("runtime") - if runtimePkg == nil { - panic("ssa.Program doesn't include runtime package") + if runtimePkg != nil { + i.runtimeErrorString = runtimePkg.Type("errorString").Object().Type() } - i.runtimeErrorString = runtimePkg.Type("errorString").Object().Type() initReflect(i)
diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go index 1283424..dd44b7e 100644 --- a/go/ssa/interp/interp_test.go +++ b/go/ssa/interp/interp_test.go
@@ -124,6 +124,8 @@ "fixedbugs/issue66783.go", "fixedbugs/issue69929.go", "forvarlifetime_go122.go", + "issue79414a.go", + "issue79414b.go", "forvarlifetime_old.go", "ifaceconv.go", "ifaceprom.go",
diff --git a/go/ssa/interp/reflect.go b/go/ssa/interp/reflect.go index 7c549ab..c3542aa 100644 --- a/go/ssa/interp/reflect.go +++ b/go/ssa/interp/reflect.go
@@ -124,7 +124,7 @@ func extÛ°reflectÛ°rtypeÛ°NumMethod(fr *frame, args []value) value { // Signature: func (t reflect.rtype) int - return fr.i.prog.MethodSets.MethodSet(args[0].(rtype).t).Len() + return fr.i.prog.MethodSets.MethodSet(args[0].(rtype).t).Len() // beware: falsely reports generic methods } func extÛ°reflectÛ°rtypeÛ°NumOut(fr *frame, args []value) value {
diff --git a/go/ssa/interp/testdata/issue79414a.go b/go/ssa/interp/testdata/issue79414a.go new file mode 100644 index 0000000..f4dd405 --- /dev/null +++ b/go/ssa/interp/testdata/issue79414a.go
@@ -0,0 +1,29 @@ +// Regression test for an unsound optimization to initialize S{...} in +// place at *s (even though s is nil). + +package main + +type S struct { + F1, F2 int +} + +var n int + +func gen() int { + n++ + return n +} + +func main() { + defer func() { + if r := recover(); r != nil { + if n != 2 { + panic("n should be 2") + } + } else { + panic("should have panicked") + } + }() + var s *S + *s = S{gen(), gen()} +}
diff --git a/go/ssa/interp/testdata/issue79414b.go b/go/ssa/interp/testdata/issue79414b.go new file mode 100644 index 0000000..5449742 --- /dev/null +++ b/go/ssa/interp/testdata/issue79414b.go
@@ -0,0 +1,36 @@ +// Regression test for an unsound optimization to initialize S{...} in +// place, even though that violates the required ordering between the +// evaluation of the "..." operands and the assignments to the fields +// of S. + +package main + +type S struct { + X, Y int +} + +var s S + +func main() { + // The function calls must occur before the assignment. That + // means g() should observe the s.X=42 effect of the call f() + // but not s.X=1 effect of the composite literal field assignment; + // that should happens after g(). + s = S{f(), g()} + + if s.X != 1 || s.Y != 2 { + panic("s should be {1, 2}") + } +} + +func f() int { + s.X = 42 + return 1 +} + +func g() int { + if s.X != 42 { + panic("g should see s.X == 42 from side effect of f") + } + return 2 +}
diff --git a/go/ssa/methods.go b/go/ssa/methods.go index 4b116f4..82faade 100644 --- a/go/ssa/methods.go +++ b/go/ssa/methods.go
@@ -32,8 +32,8 @@ return nil // interface method or type parameter } - if prog.isParameterized(T) { - return nil // generic method + if prog.isParameterized(T, sel.Type()) { + return nil // method on generic type or generic method } if prog.mode&LogSource != 0 { @@ -61,11 +61,11 @@ needsPromotion := len(sel.Index()) > 1 needsIndirection := !isPointer(recvType(obj)) && isPointer(T) if needsPromotion || needsIndirection { - fn = createWrapper(prog, toSelection(sel)) + fn = createWrapper(prog, toSelection(sel), nil) fn.buildshared = b.shared() b.enqueue(fn) } else { - fn = prog.objectMethod(obj, &b) + fn = prog.objectMethod(obj, nil, &b) } if fn.Signature.Recv() == nil { panic(fn) @@ -91,25 +91,22 @@ // objectMethod panics if the function is not a method. // // Acquires prog.objectMethodsMu. -func (prog *Program) objectMethod(obj *types.Func, b *builder) *Function { +func (prog *Program) objectMethod(obj *types.Func, targs []types.Type, b *builder) *Function { sig := obj.Type().(*types.Signature) if sig.Recv() == nil { panic("not a method: " + obj.String()) } + // Instantiation of generic? + if orig := obj.Origin(); orig != obj || len(targs) > 0 { + return prog.objectMethod(orig, nil, b).instance(receiverTypeArgs(obj), targs, b) + } + // Belongs to a created package? if fn := prog.FuncValue(obj); fn != nil { return fn } - // Instantiation of generic? - if originObj := obj.Origin(); originObj != obj { - origin := prog.objectMethod(originObj, b) - assert(origin.typeparams.Len() > 0, "origin is not generic") - targs := receiverTypeArgs(obj) - return origin.instance(targs, b) - } - // Consult/update cache of methods created from types.Func. prog.objectMethodsMu.Lock() defer prog.objectMethodsMu.Unlock()
diff --git a/go/ssa/sanity.go b/go/ssa/sanity.go index 5bd5d97..824252d 100644 --- a/go/ssa/sanity.go +++ b/go/ssa/sanity.go
@@ -454,7 +454,7 @@ } if !types.Identical(sigType, param.Type()) { - s.errorf("expect type %s in signature but got type %s in param %d", param.Type(), sigType, i) + s.errorf("expect type %s in signature but got type %s in param %d", sigType, param.Type(), i) } } } @@ -529,16 +529,16 @@ strings.HasSuffix(fn.name, "Error") || strings.HasPrefix(fn.Synthetic, "instance ") || strings.HasPrefix(fn.Synthetic, "instantiation ") || - (fn.parent != nil && len(fn.typeargs) > 0) /* anon fun in instance */ { + fn.parent != nil && fn.parent.hasTypeArgs() /* anon fun in instance */ { // ok } else { s.errorf("nil Pkg") } } if src, syn := fn.Synthetic == "", fn.Syntax() != nil; src != syn { - if len(fn.typeargs) > 0 && fn.Prog.mode&InstantiateGenerics != 0 { + if fn.hasTypeArgs() && fn.Prog.mode&InstantiateGenerics != 0 { // ok (instantiation with InstantiateGenerics on) - } else if fn.topLevelOrigin != nil && len(fn.typeargs) > 0 { + } else if fn.hasTypeArgs() && fn.topLevelOrigin != nil { // ok (we always have the syntax set for instantiation) } else if _, rng := fn.syntax.(*ast.RangeStmt); rng && fn.Synthetic == "range-over-func yield" { // ok (range-func-yields are both synthetic and keep syntax)
diff --git a/go/ssa/source_test.go b/go/ssa/source_test.go index 81b26d8..c1b7d28 100644 --- a/go/ssa/source_test.go +++ b/go/ssa/source_test.go
@@ -20,6 +20,7 @@ "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ssa" "golang.org/x/tools/internal/expect" + "golang.org/x/tools/internal/testenv" ) func TestObjValueLookup(t *testing.T) { @@ -303,12 +304,13 @@ } func TestEnclosingFunction(t *testing.T) { - tests := []struct { + type testcase struct { desc string input string // the input file substr string // first occurrence of this string denotes interval fn string // name of expected containing function - }{ + } + tests := []testcase{ // We use distinctive numbers as syntactic landmarks. {"Ordinary function", ` package main @@ -349,6 +351,16 @@ "1000", "(*main.S[T]).Foo", }, } + if testenv.Go1Point() >= 27 { + tests = append(tests, testcase{ + "generic method", ` + package main + type S struct{} + func (S) Foo[T any]() { println(1100) } + type P[T any] struct{ S }`, + "1100", "(main.S).Foo", + }) + } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { pkg, ppkg := buildPackage(t, test.input, ssa.BuilderMode(0))
diff --git a/go/ssa/ssa.go b/go/ssa/ssa.go index 7c84494..514ba2b 100644 --- a/go/ssa/ssa.go +++ b/go/ssa/ssa.go
@@ -13,7 +13,11 @@ "go/constant" "go/token" "go/types" + "reflect" + "slices" + "strings" "sync" + "unsafe" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" @@ -364,8 +368,10 @@ referrers []Instruction // referring instructions (iff Parent() != nil) anonIdx int32 // position of a nested function in parent's AnonFuncs. fn.Parent()!=nil => fn.Parent().AnonFunc[fn.anonIdx] == fn. - typeparams *types.TypeParamList // type parameters of this function. typeparams.Len() > 0 => generic or instance of generic function - typeargs []types.Type // type arguments that instantiated typeparams. len(typeargs) > 0 => instance of generic function + recvtypeparams *types.TypeParamList // receiver type parameters of this function. recvtypeparams.Len() > 0 => method on generic or instance of generic type + recvtypeargs []types.Type // type arguments that instantiated recvtypeparams. len(recvtypeargs) > 0 => method on instance of generic type + typeparams *types.TypeParamList // type parameters of this function. typeparams.Len() > 0 => generic or instance of generic function or method + typeargs []types.Type // type arguments that instantiated typeparams. len(typeargs) > 0 => instance of generic function or method topLevelOrigin *Function // the origin function if this is an instance of a source function. nil if Parent()!=nil. generic *generic // instances of this function, if generic @@ -1577,18 +1583,73 @@ // TypeParams are the function's type parameters if generic or the // type parameters that were instantiated if fn is an instantiation. +// +// Specifically, the resulting list behaves like: +// +// func f // [] +// func f[P] // [P] +// func (T) m // [] +// func (T) m[P] // [P] +// func (T[P]) m // [P] +// func (T[P]) m[Q] // [P (index=0), Q (index=0)] +// +// Note that receiver type parameters precede other type parameters. +// Also, type parameters may have the same index if they come from +// different source type parameter lists. func (fn *Function) TypeParams() *types.TypeParamList { - return fn.typeparams + return consTypeParamLists(fn.recvtypeparams, fn.typeparams) +} + +func consTypeParamLists(l, r *types.TypeParamList) *types.TypeParamList { + if l.Len() == 0 { + return r + } + if r.Len() == 0 { + return l + } + + tpars := make([]*types.TypeParam, l.Len()+r.Len()) + for i := range l.Len() { + tpars[i] = l.At(i) + } + for i := range r.Len() { + tpars[i+l.Len()] = r.At(i) + } + // This logic unsafely assumes (and asserts) that the layout of the + // TypeParamList is identical to that of a slice of TypeParams. This + // is a hack while we work on getting a constructor for TypeParamList + // approved (see go.dev/issue/79603). + t := reflect.TypeFor[types.TypeParamList]() + if t.NumField() != 1 { + panic("TypeParamList has unexpected fields") + } + if f := t.Field(0); f.Offset != 0 || f.Type != reflect.TypeFor[[]*types.TypeParam]() { + panic("TypeParamList field is not []*TypeParam") + } + return (*types.TypeParamList)(unsafe.Pointer(&tpars)) } // TypeArgs are the types that TypeParams() were instantiated by to create fn // from fn.Origin(). -func (fn *Function) TypeArgs() []types.Type { return fn.typeargs } +// +// Specifically, the resulting slice behaves like: +// +// f // [] +// f[int] // [int] +// T.m // [] +// T.m[int] // [int] +// T[int].m // [int] +// T[int].m[uint] // [int, uint] +// +// Note that receiver type arguments precede other type arguments. +func (fn *Function) TypeArgs() []types.Type { + return slices.Concat(fn.recvtypeargs, fn.typeargs) +} // Origin returns the generic function from which fn was instantiated, // or nil if fn is not an instantiation. func (fn *Function) Origin() *Function { - if fn.parent != nil && len(fn.typeargs) > 0 { + if fn.parent != nil && fn.parent.hasTypeArgs() { // Nested functions are BUILT at a different time than their instances. // Build declared package if not yet BUILT. This is not an expected use // case, but is simple and robust. @@ -1597,12 +1658,48 @@ return origin(fn) } +// hasTypeParams returns whether fn has any type parameters +func (fn *Function) hasTypeParams() bool { + return fn.recvtypeparams.Len()+fn.typeparams.Len() > 0 +} + +// hasTypeArgs returns whether fn has any type arguments +func (fn *Function) hasTypeArgs() bool { + return len(fn.recvtypeargs)+len(fn.typeargs) > 0 +} + +// subrtargs returns fn's receiver type parameters substituted with receiver type arguments +func (fn *Function) subrtargs(m *types.Func) []types.Type { + return fn.subst.types(receiverTypeArgs(m)) +} + +// subtargs returns fn's type parameters substituted with (possibly implied) type arguments +func (fn *Function) subtargs(id *ast.Ident) []types.Type { + return fn.subst.types(instanceArgs(fn.info, id)) +} + +// targstr returns a comma-separated string of the types in targs +func targstr(targs []types.Type) string { + var sb strings.Builder + if len(targs) > 0 { + sb.WriteString("[") + for i := range targs { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(targs[i].String()) + } + sb.WriteString("]") + } + return sb.String() +} + // origin is the function that fn is an instantiation of. Returns nil if fn is // not an instantiation. // // Precondition: fn and the origin function are done building. func origin(fn *Function) *Function { - if fn.parent != nil && len(fn.typeargs) > 0 { + if fn.parent != nil && fn.parent.hasTypeArgs() { return origin(fn.parent).AnonFuncs[fn.anonIdx] } return fn.topLevelOrigin
diff --git a/go/ssa/ssautil/visit.go b/go/ssa/ssautil/visit.go index 7300d2b..3cfc2ba 100644 --- a/go/ssa/ssautil/visit.go +++ b/go/ssa/ssautil/visit.go
@@ -74,8 +74,11 @@ methodsOf := func(T types.Type) { if !types.IsInterface(T) { mset := prog.MethodSets.MethodSet(T) - for method := range mset.Methods() { - function(prog.MethodValue(method)) + for sel := range mset.Methods() { + // Skip generic methods. + if sel.Obj().(*types.Func).Signature().TypeParams() == nil { + function(prog.MethodValue(sel)) + } } } } @@ -104,6 +107,7 @@ // Consider only named types. // (Ignore aliases and unsafe.Pointer.) if named, ok := t.Type().(*types.Named); ok { + // Skip generic types. if named.TypeParams() == nil { methodsOf(named) // T methodsOf(types.NewPointer(named)) // *T @@ -117,6 +121,11 @@ switch mem := mem.(type) { case *ssa.Function: // Visit all package-level declared functions. + // + // (This may include generic functions, which is + // inconsistent with the treatment of methods: + // we skip both generic methods, + // and methods of generic types.) function(mem) case *ssa.Type:
diff --git a/go/ssa/subst.go b/go/ssa/subst.go index a544169..00b56ef 100644 --- a/go/ssa/subst.go +++ b/go/ssa/subst.go
@@ -5,6 +5,7 @@ package ssa import ( + "fmt" "go/types" "golang.org/x/tools/go/types/typeutil" @@ -56,18 +57,25 @@ // TODO(taking): consider adding Pos } -// Returns a subster that replaces tparams[i] with targs[i]. Uses ctxt as a cache. -// targs should not contain any types in tparams. +// Returns a subster that replaces rtparams[i] with rtargs[i] and tparams[i] with targs[i]. +// Uses ctxt as a cache. rtargs and targs should not contain any types in rtparams or tparams. // fn is the generic function for which we are substituting. -func makeSubster(ctxt *types.Context, fn *types.Func, tparams *types.TypeParamList, targs []types.Type) *subster { - assert(tparams.Len() == len(targs), "makeSubster argument count must match") +func makeSubster(ctxt *types.Context, fn *types.Func, rtparams *types.TypeParamList, rtargs []types.Type, tparams *types.TypeParamList, targs []types.Type) *subster { + got := len(rtargs) + len(targs) + want := rtparams.Len() + tparams.Len() + if got != want { + panic(fmt.Sprintf("makeSubster argument count must match: got %d; want %d", got, want)) + } subst := &subster{ - replacements: make(map[*types.TypeParam]types.Type, tparams.Len()), + replacements: make(map[*types.TypeParam]types.Type, want), cache: make(map[types.Type]types.Type), origin: fn.Origin(), ctxt: ctxt, } + for i := 0; i < rtparams.Len(); i++ { + subst.replacements[rtparams.At(i)] = rtargs[i] + } for i := 0; i < tparams.Len(); i++ { subst.replacements[tparams.At(i)] = targs[i] }
diff --git a/go/ssa/subst_test.go b/go/ssa/subst_test.go index 55051ba..38e495f 100644 --- a/go/ssa/subst_test.go +++ b/go/ssa/subst_test.go
@@ -103,7 +103,8 @@ T := tv.Type.(*types.Named) - subst := makeSubster(types.NewContext(), within, T.TypeParams(), targs) + // TODO(mark): Add more tests here. + subst := makeSubster(types.NewContext(), within, T.TypeParams(), targs, nil, nil) sub := subst.typ(T.Underlying()) if got := sub.String(); got != test.want { t.Errorf("subst{%v->%v}.typ(%s) = %v, want %v", test.expr, test.args, T.Underlying(), got, test.want)
diff --git a/go/ssa/testdata/fixedbugs/issue73871.go b/go/ssa/testdata/fixedbugs/issue73871.go new file mode 100644 index 0000000..a402bc0 --- /dev/null +++ b/go/ssa/testdata/fixedbugs/issue73871.go
@@ -0,0 +1,12 @@ +package issue73871 + +// Regression test for panic instantiating signature for a call append(x, y...). + +func f[T ~[]byte](y T) { + _ = append([]byte(nil), y...) +} + +func _() { + type B []byte + f(B(nil)) // must not panic +}
diff --git a/go/ssa/testdata/objlookup.go b/go/ssa/testdata/objlookup.go index 7c79f0c..4f0cc88 100644 --- a/go/ssa/testdata/objlookup.go +++ b/go/ssa/testdata/objlookup.go
@@ -12,6 +12,9 @@ // For const and func objects, the results don't vary by reference and // are always values not addresses, so no annotations are needed. The // declaration is enough. +// +// In retrospect, the intended behavior of ssa.Program.VarValue is +// rather strange. import ( "fmt" @@ -94,7 +97,7 @@ v8a[0] = 0 //@ ssa(v8a,"Slice") print(v8a[:]) //@ ssa(v8a,"Slice") - v9 := S{} //@ ssa(v9,"&Alloc") + v9 := S{} //@ ssa(v9,"Const") v10 := &v9 //@ ssa(v10,"Alloc"), ssa(v9,"&Alloc") _ = v10 //@ ssa(v10,"Alloc")
diff --git a/go/ssa/testdata/valueforexpr.go b/go/ssa/testdata/valueforexpr.go index 8c834ef..e79fcd8 100644 --- a/go/ssa/testdata/valueforexpr.go +++ b/go/ssa/testdata/valueforexpr.go
@@ -55,7 +55,7 @@ _ = /*@UnOp*/ (global) /*@UnOp*/ (global)[""] = "" - /*@Global*/ (global) = map[string]string{} + /*@MakeMap*/ (global) = map[string]string{} var local t /*UnOp*/ (local.x) = 1
diff --git a/go/ssa/wrappers.go b/go/ssa/wrappers.go index aeb160e..6cadd04 100644 --- a/go/ssa/wrappers.go +++ b/go/ssa/wrappers.go
@@ -40,14 +40,14 @@ // following axes of variation when making changes: // - optional receiver indirection // - optional implicit field selections +// - optional method type arguments // - meth.Obj() may denote a concrete or an interface method // - the result may be a thunk or a wrapper. -func createWrapper(prog *Program, sel *selection) *Function { - obj := sel.obj.(*types.Func) // the declared function - sig := sel.typ.(*types.Signature) // type of this wrapper +func createWrapper(prog *Program, sel *selection, targs []types.Type) *Function { + obj := sel.obj.(*types.Func) // the declared function + name, sig := maybeInstance(prog, obj.Name(), sel.typ.(*types.Signature), targs) var recv *types.Var // wrapper's receiver or thunk's params[0] - name := obj.Name() var description string if sel.kind == types.MethodExpr { name += "$thunk" @@ -58,7 +58,7 @@ recv = sig.Recv() } - description = fmt.Sprintf("%s for %s", description, sel.obj) + description = fmt.Sprintf("%s for %s", description, obj) if prog.mode&LogSource != 0 { defer logStack("create %s to (%s)", description, recv.Type())() } @@ -71,6 +71,7 @@ Synthetic: description, Prog: prog, pos: obj.Pos(), + typeargs: targs, // wrappers have no syntax build: (*builder).buildWrapper, syntax: nil, @@ -79,6 +80,20 @@ } } +// maybeInstance returns name and sig instantiated to reflect any type arguments in targs. +func maybeInstance(prog *Program, name string, sig *types.Signature, targs []types.Type) (string, *types.Signature) { + if len(targs) > 0 { + name = fmt.Sprintf("%s%s", name, targstr(targs)) + instSig, err := types.Instantiate(prog.ctxt, sig, targs, false) + if err != nil { + // validate was false, we should never get an error + panic(err) + } + sig = prog.canon.Type(instSig).(*types.Signature) + } + return name, sig +} + // buildWrapper builds fn.Body for a method wrapper. func (b *builder) buildWrapper(fn *Function) { var recv *types.Var // wrapper's receiver or thunk's params[0] @@ -137,7 +152,7 @@ if !isPointer(r) { v = emitLoad(fn, v) } - c.Call.Value = fn.Prog.objectMethod(fn.object, b) + c.Call.Value = fn.Prog.objectMethod(fn.object, fn.typeargs, b) c.Call.Args = append(c.Call.Args, v) } else { c.Call.Method = fn.object @@ -184,19 +199,22 @@ // Unlike createWrapper, createBound need perform no indirection or field // selections because that can be done before the closure is // constructed. -func createBound(prog *Program, obj *types.Func) *Function { +func createBound(prog *Program, obj *types.Func, targs []types.Type) *Function { description := fmt.Sprintf("bound method wrapper for %s", obj) if prog.mode&LogSource != 0 { defer logStack("%s", description)() } + name, sig := maybeInstance(prog, obj.Name(), obj.Type().(*types.Signature), targs) + /* bound method wrapper */ fn := &Function{ - name: obj.Name() + "$bound", + name: name + "$bound", object: obj, - Signature: changeRecv(obj.Type().(*types.Signature), nil), // drop receiver + Signature: changeRecv(sig, nil), // drop receiver Synthetic: description, Prog: prog, pos: obj.Pos(), + typeargs: targs, // wrappers have no syntax build: (*builder).buildBound, syntax: nil, @@ -215,7 +233,7 @@ recv := fn.FreeVars[0] if !types.IsInterface(recvType(fn.object)) { // concrete - c.Call.Value = fn.Prog.objectMethod(fn.object, b) + c.Call.Value = fn.Prog.objectMethod(fn.object, fn.typeargs, b) c.Call.Args = []Value{recv} } else { c.Call.Method = fn.object @@ -246,12 +264,12 @@ // f is a synthetic wrapper defined as if by: // // f := func(t T) { return t.meth() } -func createThunk(prog *Program, sel *selection) *Function { +func createThunk(prog *Program, sel *selection, targs []types.Type) *Function { if sel.kind != types.MethodExpr { panic(sel) } - fn := createWrapper(prog, sel) + fn := createWrapper(prog, sel, targs) if fn.Signature.Recv() != nil { panic(fn) // unexpected receiver }
diff --git a/go/types/objectpath/objectpath_test.go b/go/types/objectpath/objectpath_test.go index b1a55f8..8f95c85 100644 --- a/go/types/objectpath/objectpath_test.go +++ b/go/types/objectpath/objectpath_test.go
@@ -488,7 +488,7 @@ // objectpath would inspect the methods of I: // - First we see I.A, which leads to Anon, to F, which is marked as seen. // - Second we see I.F, embedded via alias Anon. - // Since we've already seen F, we break ouf the the interface method loop. + // Since we've already seen F, we break out of the interface method loop. // - Third, we fail to visit I.Z. // The solution is to skip only I.F, not the rest of the interface.
diff --git a/go/types/typeutil/ui_test.go b/go/types/typeutil/ui_test.go index 5986b04..0590c86 100644 --- a/go/types/typeutil/ui_test.go +++ b/go/types/typeutil/ui_test.go
@@ -14,6 +14,7 @@ "testing" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/testenv" ) func TestIntuitiveMethodSet(t *testing.T) { @@ -62,4 +63,54 @@ t.Errorf("IntuitiveMethodSet(%s) = %q, want %q", test.expr, got, test.want) } } + + // variant with generic methods + if testenv.Go1Point() >= 27 { + const source = ` +package P +type A int +func (A) f() +func (*A) g() +func (*A) h[T any]() {} +` + + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "hello.go", source, 0) + if err != nil { + t.Fatal(err) + } + + var conf types.Config + pkg, err := conf.Check("P", fset, []*ast.File{f}, nil) + if err != nil { + t.Fatal(err) + } + qual := types.RelativeTo(pkg) + + for _, test := range []struct { + expr string // type expression + want string // intuitive method set + }{ + {"A", "(A).f (*A).g (*A).h"}, + {"*A", "(*A).f (*A).g (*A).h"}, + {"error", "(error).Error"}, + {"*error", ""}, + {"struct{A}", "(struct{A}).f (*struct{A}).g (*struct{A}).h"}, + {"*struct{A}", "(*struct{A}).f (*struct{A}).g (*struct{A}).h"}, + } { + tv, err := types.Eval(fset, pkg, 0, test.expr) + if err != nil { + t.Errorf("Eval(%s) failed: %v", test.expr, err) + } + var names []string + for _, m := range typeutil.IntuitiveMethodSet(tv.Type, nil) { + name := fmt.Sprintf("(%s).%s", types.TypeString(m.Recv(), qual), m.Obj().Name()) + names = append(names, name) + } + got := strings.Join(names, " ") + if got != test.want { + t.Errorf("IntuitiveMethodSet(%s) = %q, want %q", test.expr, got, test.want) + } + } + } }
diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index 62ee238..8cbe47e 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md
@@ -3142,7 +3142,7 @@ The errorsas analyzer reports calls to errors.As where the type of the second argument is not a pointer to a type implementing error. For example: var unwrappedErr net.DNSError - errors.As(err, unwrappedErr) // should use &unwrappedErr, DNSError.Error has a pointer reciever + errors.As(err, unwrappedErr) // should use &unwrappedErr, DNSError.Error has a pointer receiver Default: on. @@ -3172,8 +3172,8 @@ Package documentation: [errorsastype](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#errorsastype) -<a id='errorsastype'></a> -## `errorsastype`: Reports misuse of errors.AsType[T] in if/else chains. +<a id='errorsastypeshadow'></a> +## `errorsastypeshadow`: report shadowing of errors.AsType[T] in if/else chains For example: @@ -3189,7 +3189,7 @@ Default: on. -Package documentation: [errorsastype](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/errorsastype) +Package documentation: [errorsastypeshadow](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/errorsastypeshadow) <a id='fieldalignment'></a> ## `fieldalignment`: find structs that would use less memory if their fields were sorted @@ -3354,7 +3354,11 @@ <a id='inline'></a> ## `inline`: apply fixes based on 'go:fix inline' comment directives -The inline analyzer inlines functions and constants that are marked for inlining. +The inline analyzer inlines functions, constants, and type aliases that are marked for inlining. + +Use this command to apply (just) inline fixes en masse: + + $ go fix -inline ./... \## Functions @@ -3390,8 +3394,6 @@ (In cases where it is not safe to "reduce" a call—that is, to replace a call f(x) by the body of function f, suitably substituted—the inliner machinery is capable of replacing f by a function literal, func(){...}(). However, the inline analyzer discards all such "literalizations" unconditionally, again on grounds of style.) -A call to a function F from its dedicated test (TestF) is not inlined, since the purpose of the test is to exercise F itself, even when it's a deprecated function to which other calls should be inlined. This is not true for type aliases; see [https://go.dev/issue/79271](https://go.dev/issue/79271). See further discussion in [https://go.dev/issue/79272](https://go.dev/issue/79272). - \## Constants Given a constant that is marked for inlining, like this one: @@ -3418,14 +3420,23 @@ //go:fix inline const ( Ptr = Pointer - Val = Value + Val = Value ) -The proposal [https://go.dev/issue/32816](https://go.dev/issue/32816) introduces the "//go:fix inline" directives. +\## Type aliases -You can use this command to apply inline fixes en masse: +Similar to named constants, a type alias can also be marked for inlining: - $ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./... + //go:fix inline + type A = newpkg.A + +The analyzer will replace all references to the annotated type (A) by the type on the right-hand side of the declaration (newpkg.A). + +\## Tests + +A use of a function, named constant, or type alias X from its dedicated test (TestX), is not inlined, since the purpose of the test is to exercise X itself, even if it is deprecated and other uses of it should be inlined. This applies to benchmarks and examples too, and follows the usual conventions of test function naming. + +Similarly, if the symbol X is declared in a file named foo.go, any use of it within a file named foo\_test.go will also not be inlined. Default: on. @@ -4006,6 +4017,30 @@ Package documentation: [simplifyslice](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifyslice) +<a id='slicesbackward'></a> +## `slicesbackward`: replace backward loops over slices with slices.Backward + +The slicesbackward analyzer suggests replacing manually-written backward loops of the form + + for i := len(s) - 1; i >= 0; i-- { + use(s[i]) + } + +with the more readable Go 1.23 style using slices.Backward: + + for _, v := range slices.Backward(s) { + use(v) + } + +If the loop index is needed beyond just indexing into the slice, both the index and value variables are kept: + + for i, v := range slices.Backward(s) { ... } + + +Default: on. + +Package documentation: [slicesbackward](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#slicesbackward) + <a id='slicescontains'></a> ## `slicescontains`: replace loops with slices.Contains or slices.ContainsFunc @@ -4078,6 +4113,38 @@ Package documentation: [sortslice](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sortslice) +<a id='sqlrowserr'></a> +## `sqlrowserr`: report failure to check sql.Rows.Err + +This analyzer reports uses of sql.Rows in which the result of a query such as db.Query() is assigned to a local variable that is then used in a loop that calls Rows.Next, but lacks a final check of Rows.Err. This causes row iteration errors to be discarded. + +For example: + + rows, err := db.Query("select ...") // error: "sql.Rows rows is used in Next loop without final check of rows.Err()" + if err != nil { + return err + } + defer rows.Close() // ignore error + for rows.Next() { + var x int + if err := rows.Scan(&x); err != nil { + return err + } + use(x) + } + /* ...no use of rows.Err()... */ + +Correct usage of sql.Rows demands both a call to Rows.Close to release resources and a call to Rows.Err to report iteration errors. It is not critical to report resource cleanup errors, but it is crucial to report iteration errors as they would otherwise be indistinguishable from a smaller result. + +To avoid false positives, the analyzer is silent if the Rows is passed into or out of the function or assigned somewhere other than a local variable. + +It is not this analyzer's goal to ensure proper handling of errors in all cases, but merely the simple mistakes where the user may have been oblivious to the existence of the Rows.Err method. + + +Default: on. + +Package documentation: [sqlrowserr](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sqlrowserr) + <a id='stditerators'></a> ## `stditerators`: use iterators instead of Len/At-style APIs
diff --git a/gopls/doc/contributing.md b/gopls/doc/contributing.md index 4d6615b..69e3891 100644 --- a/gopls/doc/contributing.md +++ b/gopls/doc/contributing.md
@@ -47,6 +47,7 @@ For more detail, see the Go project's [contribution guidelines](https://golang.org/doc/contribute.html). + ## Finding issues All `gopls` issues are labeled as such (see the [`gopls` label][issue-gopls]). @@ -63,6 +64,39 @@ Most of the `gopls` logic is in the `golang.org/x/tools/gopls/internal` directory. See [design/implementation.md](./design/implementation.md) for an overview of the code organization. +### Repository structure + +This repository provides two modules: +- The root directory defines the `golang.org/x/tools` module, which provides importable packages. +- The `gopls` subdirectory defines the `golang.org/x/tools/gopls` module, which provides the gopls application as its main package. + +The gopls/go.mod file contains a `replace` directive pointing to the parent directory to ensure that gopls uses the version of x/tools at the exact same git commit. + +We recommend creating a go.work file such as this in the root directory: + +```go +go 1.25 + +use . +use ./gopls +``` + +so that the go command will allow you to specify gopls packages even when working outside the gopls directory. For example: + +``` +tools$ go test -short ./gopls/... +``` + +For more details on how to use workspaces, see the [Go workspace documentation](https://go.dev/doc/tutorial/workspaces) or run `go help work`. + +With this setup, you can run tests for all modules in the workspace from the root directory using: + +```bash +go test work +``` + +This will run tests for both `golang.org/x/tools` and `golang.org/x/tools/gopls`. + ## Build To build a version of `gopls` with your changes applied: @@ -126,14 +160,6 @@ Note also that panicking is preferable to `log.Fatal` because it allows VS Code's crash reporting to recognize and capture the stack. -Bugs reported through `bug.Errorf` and friends are retrieved using the -`gopls bug` command, which opens a GitHub Issue template and populates -it with a summary of each bug and its frequency. -The text of the bug is rather fastidiously printed to stdout to avoid -sharing user names and error message strings (which could contain -project identifiers) with GitHub. -Users are invited to share it if they are willing. - ## Testing The normal command you should use to run the tests after a change is:
diff --git a/gopls/doc/design/integrating-interactive-refactoring.md b/gopls/doc/design/integrating-interactive-refactoring.md index a48fae1..395864c 100644 --- a/gopls/doc/design/integrating-interactive-refactoring.md +++ b/gopls/doc/design/integrating-interactive-refactoring.md
@@ -12,20 +12,22 @@ Client Capabilities -To enable interactive refactoring, the language client must advertise its support for specific input types by adding an `interactiveInputTypes` field to the `experimental` section of its client capabilities. +To enable interactive refactoring, the language client must advertise its support for specific input types by adding an `interactiveResolve` object with an `inputTypes` field to the `experimental` section of its client capabilities. -The value should be a `[]string` containing the types of input UI the client can render. +The value of `inputTypes` should be a `[]string` containing the types of input UI the client can render. Example: ```json { // ... existing client capabilities ... "experimental": { - "interactiveInputTypes": [ - "string", - "enum", - "bool" - ] + "interactiveResolve": { + "inputTypes": [ + "string", + "enum", + "bool" + ] + } } } ``` @@ -41,22 +43,24 @@ Server Capabilities -To enable interactive refactoring, the server must advertise its support for resolving commands by adding an `interactiveResolveProvider` field to the `experimental` section of its server capabilities. +To enable interactive refactoring, the server must advertise its support for resolving commands by adding an `interactiveResolveProvider` object to the `experimental` section of its server capabilities. ```json { // ... existing server capabilities ... "experimental": { - "interactiveResolveProvider": [ + "interactiveResolveProvider": { + "kinds": [ "command" - ] + ] + } } } ``` -The value should be a `[]string` indicating the supported resolution targets. If `"command"` is present in this list, the client may safely invoke the `command/resolve` method to interactively resolve `ExecuteCommandParams`. Otherwise, the client should not attempt to call this method. +The `kinds` field is a `[]string` indicating the supported resolution targets. If `"command"` is present in this list, the client may safely invoke the `command/resolve` method to interactively resolve `ExecuteCommandParams`. Otherwise, the client should not attempt to call this method. -Additional methods may be supported in the future by adding them to this array. +Additional kinds may be supported in the future by adding them to this array. ## Request @@ -64,11 +68,11 @@ When a client receives a `CodeAction` containing a command that supports interactive resolution, it should **not** execute the command immediately via `workspace/executeCommand`. Instead, the client must first send a `command/resolve` request to the server, passing the `ExecuteCommandParams` received in the code action. -The server responds with `ExecuteCommandParams` that may include a `formFields` property. If `formFields` is present and non-empty, it indicates that the command requires user inputs. The client must not proceed with command execution, but must instead present the questions from `formFields` to the user to collect answers. The `formFields` array contains `FormField` objects, each describing a prompt, expected type, and optional default value. +The server responds with `ExecuteCommandParams` that may include a `formFields` property. If `formFields` is present and non-empty, it indicates that the command requires user inputs. The client must not proceed with command execution, but must instead present the questions from `formFields` to the user to collect answers. The `formFields` array contains `FormField` objects, each describing a unique ID, prompt, expected type, whether it is required, and optional default value. -Once the user provides answers, the client sends another `command/resolve` request to the server, populating the `formAnswers` property in the `ExecuteCommandParams` and omitting the `formFields` property. The `formAnswers` array must be of the same length as the `formFields` previously received from the server, with the answer at index `i` corresponding to the question at index `i`. +Once the user provides answers, the client sends another `command/resolve` request to the server, populating the `formAnswers` property in the `ExecuteCommandParams` and omitting the `formFields` property. Answers are linked to their respective questions using the field's unique `id`. The list must not contain duplicate IDs, and each answer's ID must correspond to a field ID defined in `formFields`. The client must include answers for all required fields (where `required` is true). Answers for optional fields may be omitted or included as available. -Upon receiving `formAnswers`, the server validates the input. If the input is invalid, the server returns `ExecuteCommandParams` with `formFields` again, populating the `error` property on the fields that failed validation. The client can then choose to re-render the UI to display these errors and allow the user to correct their input for a retry, or it may abort the operation entirely. +Upon receiving `formAnswers`, the server validates the input. If the input is invalid, the server returns `ExecuteCommandParams` with `formFields` again, populating the `error` property on the fields that failed validation (identified by matching `id`). The client can then choose to re-render the UI to display these errors and allow the user to correct their input for a retry, or it may abort the operation entirely. This process repeats until the server returns a response where `formFields` is omitted or empty. This signals that the parameters are fully resolved and valid. At this point, the client may proceed to execute the command by calling `workspace/executeCommand` with the finalized `ExecuteCommandParams` containing the valid `formAnswers`. @@ -98,19 +102,31 @@ // current or default answers to the questions to support editing previous values. // // When sent by the language client, this field contains the user's answers. - // The slice must have the same length as FormFields, where the answer at - // index i corresponds to the question at index i. - formAnswers?: any[]; + // Answers are linked to their respective questions using the field's unique + // `id` rather than their array index. The list must not contain duplicate IDs, + // and each answer's ID must correspond to a field ID defined in `formFields`. + // + // The client must include answers for all required fields (where `required` + // is true). Answers for optional fields (where `required` is false) + // may be omitted if no answer was provided, or included if an answer is available. + formAnswers?: FormAnswer[]; } // FormField describes a single question in a form and its validation state. export interface FormField { + // ID is a unique identifier for this field. This key is used as the property + // name in FormAnswers to map the user's input back to this specific field. + id: string; + // Description is the text content of the question (the prompt) presented to the user. description: string; // Type specifies the data type and validation constraints for the answer. type: FormFieldType; + // Required specifies whether an answer is required for this field. + required: boolean; + // Default specifies an optional initial value for the answer. // If Type is FormFieldTypeEnum, this value must be present in the enum's values array. default?: any; @@ -120,6 +136,15 @@ error?: string; } +// FormAnswer describes a single answer to a FormField, identified by its unique ID. +export interface FormAnswer { + // The ID of the FormField being answered. + id: string; + + // The user's answer value. + value: any; +} + // FormFieldTypeString defines a text input. export interface FormFieldTypeString { kind: 'string'; @@ -166,6 +191,12 @@ // // Only applicable against existing file. type: FileType; + + // Filters specifies the allowed file extensions without the leading dot. A file + // is valid if it matches any of the extensions (OR logic). e.g. ["png", "jpg"]. + // + // If omitted or empty, no extension filter is applied. + filters?: string[]; } @@ -312,13 +343,17 @@ "arguments": [{ "Modification": "add" }], "formFields": [ { + "id": "tags", "description": "comma-separated list of tags to add", "type": { "kind": "string" }, + "required": true, "default": "json" }, { + "id": "transform", "description": "transform rule for added tags", "type": { "kind": "enum", "entries": [...] }, + "required": true, "default": "camelcase" } ] @@ -332,32 +367,42 @@ { "command": "gopls.modify_tags", "arguments": [{ "Modification": "add" }], - "formAnswers": ["json,foo", "camelcase"] + "formAnswers": [ + { "id": "tags", "value": "json,foo" }, + { "id": "transform", "value": "camelcase" } + ] } ``` 6. Resolution Response: The server validates the input. There are two possible outcomes: * **Case A: Validation Failure** - If the input is invalid (e.g., the user entered `"json,fo o"` with a space), the server returns `formFields` again with one error per 'invalid' answers. The error is attached to the formFields[i] where the formAnswers[i] is invalid. The client may decide to drop the entire command resolve and command execution or try to return to step 4 to recollect user input. + If the input is invalid (e.g., the user entered `"json,fo o"` with a space), the server returns `formFields` again with one error per invalid answer. The error is attached to the `FormField` object that has the corresponding `id`. The client may decide to drop the entire command resolve and command execution or try to return to step 4 to recollect user input. ```json { "command": "gopls.modify_tags", "arguments": [{ "Modification": "add" }], "formFields": [ { + "id": "tags", "description": "comma-separated list of tags to add", "type": { "kind": "string" }, + "required": true, "default": "json", "error": "cannot contain spaces, quotes, colons, or control characters" }, { + "id": "transform", "description": "transform rule for added tags", "type": { "kind": "enum", "entries": [...] }, + "required": true, "default": "camelcase" } ], - "formAnswers": ["json,fo o", "camelcase"] + "formAnswers": [ + { "id": "tags", "value": "json,fo o" }, + { "id": "transform", "value": "camelcase" } + ] } ``` @@ -367,7 +412,10 @@ { "command": "gopls.modify_tags", "arguments": [{ "Modification": "add" }], - "formAnswers": ["json,foo", "camelcase"] + "formAnswers": [ + { "id": "tags", "value": "json,foo" }, + { "id": "transform", "value": "camelcase" } + ] } ``` At this point, the client proceeds to execute the command via `workspace/executeCommand`, passing the finalized params (including `formAnswers`). @@ -375,6 +423,9 @@ { "command": "gopls.modify_tags", "arguments": [{ "Modification": "add" }], - "formAnswers": ["json,foo", "camelcase"] + "formAnswers": [ + { "id": "tags", "value": "json,foo" }, + { "id": "transform", "value": "camelcase" } + ] } ``` \ No newline at end of file
diff --git a/gopls/doc/editor/vim.md b/gopls/doc/editor/vim.md index 02c1f71..2db8cc9 100644 --- a/gopls/doc/editor/vim.md +++ b/gopls/doc/editor/vim.md
@@ -6,6 +6,7 @@ * [LanguageClient-neovim](#lcneovim) * [Ale](#ale) * [vim-lsp](#vimlsp) +* [yegappan/lsp](#yegappanlsp) * [vim-lsc](#vimlsc) * [coc.nvim](#cocnvim) * [govim](#govim) @@ -69,6 +70,25 @@ augroup END ``` +## <a href="#yegappanlsp" id="yegappanlsp">yegappan/lsp</a> + +Use [yegappan/lsp] (requires Vim 9.0 or later), with the following +configuration in Vim 9 script: + +```vim +vim9script + +var lspServers = [{ + name: 'gopls', + filetype: ['go'], + path: 'gopls', + args: ['serve'], + syncInit: true, +}] + +autocmd User LspSetup call LspAddServer(lspServers) +``` + ## <a href="#vimlsc" id="vimlsc">vim-lsc</a> Use [natebosch/vim-lsc], with the following configuration: @@ -240,6 +260,7 @@ [ale]: https://github.com/w0rp/ale [ale-issue-2179]: https://github.com/w0rp/ale/issues/2179 [prabirshrestha/vim-lsp]: https://github.com/prabirshrestha/vim-lsp/ +[yegappan/lsp]: https://github.com/yegappan/lsp/ [natebosch/vim-lsc]: https://github.com/natebosch/vim-lsc/ [natebosch/vim-lsc#180]: https://github.com/natebosch/vim-lsc/issues/180 [coc.nvim]: https://github.com/neoclide/coc.nvim/
diff --git a/gopls/doc/features/transformation.md b/gopls/doc/features/transformation.md index e03a06a..386b392 100644 --- a/gopls/doc/features/transformation.md +++ b/gopls/doc/features/transformation.md
@@ -248,7 +248,7 @@ (setq gofmt-command "goimports") (add-hook 'before-save-hook 'gofmt-before-save) ``` -- **CLI**: `gopls fix -a file.go:#offset source.organizeImports` +- **CLI**: `gopls codeaction -kind=source.organizeImports -exec -write file.go:#offset` <a name='source.addTest'></a> ## `source.addTest`: Add test for function or method @@ -271,7 +271,8 @@ value has no effect, so the test provides a zero-valued argument.) **Contexts**: If the first parameter is `context.Context`, the test passes -`context.Background()`. +`t.Context()` when the module's Go version is 1.24 or later, and +`context.Background()` otherwise. **Results**: the function's results are assigned to variables (`got`, `got2`, and so on) and compared with expected values (`want`, `want2`, etc.`) defined in @@ -362,6 +363,7 @@ ``` Using Rename to move a package: + To rename a package, execute the Rename operation over the `p` in a `package p` declaration at the start of a file. You will be prompted to edit the package's path and choose its new location. @@ -379,6 +381,13 @@ Renaming package main is not supported, because the main package has special meaning to the linker. Renaming x_test packages is currently not supported. +Using Rename to change a function signature: + +This feature enables choosing a new permutation of the order of a function's parameters. +To invoke it, execute a Rename request on the `func` token of a function declaration or literal, +and enter the new function signature as the new name. The new signature must have the same +parameters; adding/removing parameters and changing function results is currently not supported. + Some tips for best results: - The safety checks performed by the Rename algorithm require type @@ -801,7 +810,7 @@ When the selection is within an `if`/`else` statement that is not followed by `else if`, gopls offers a code action to invert the -statement, negating the condition and swapping the `if` and and `else` +statement, negating the condition and swapping the `if` and `else` blocks. 
diff --git a/gopls/doc/inlayHints.md b/gopls/doc/inlayHints.md index bcb1147..4b3b38e 100644 --- a/gopls/doc/inlayHints.md +++ b/gopls/doc/inlayHints.md
@@ -13,7 +13,7 @@ `"assignVariableTypes"` controls inlay hints for variable types in assign statements: ```go - i/* int*/, j/* int*/ := 0, len(r)-1 + i« int», j« int» := 0, len(r)-1 ``` @@ -23,7 +23,9 @@ `"compositeLiteralFields"` inlay hints for composite literal field names: ```go - {/*in: */"Hello, world", /*want: */"dlrow ,olleH"} + Point2D{«X: »1, «Y: »2} + + Outer{«Embedded.»Field: 0} ``` @@ -36,7 +38,7 @@ for _, c := range []struct { in, want string }{ - /*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"}, + «struct{ in string; want string }»{"Hello, world", "dlrow ,olleH"}, } ``` @@ -48,10 +50,10 @@ `"constantValues"` controls inlay hints for constant values: ```go const ( - KindNone Kind = iota/* = 0*/ - KindPrint/* = 1*/ - KindPrintf/* = 2*/ - KindErrorf/* = 3*/ + KindNone Kind = iota« = 0» + KindPrint« = 1» + KindPrintf« = 2» + KindErrorf« = 3» ) ``` @@ -62,7 +64,7 @@ `"functionTypeParameters"` inlay hints for implicit type parameters on generic functions: ```go - myFoo/*[int, string]*/(1, "hello") + myFoo«[int, string]»(1, "hello") ``` @@ -72,7 +74,7 @@ `"ignoredError"` inlay hints for implicitly discarded errors: ```go - f.Close() // ignore error + f.Close()« // ignore error» ``` This check inserts an `// ignore error` hint following any statement that is a function call whose error result is @@ -91,7 +93,7 @@ `"parameterNames"` controls inlay hints for parameter names: ```go - parseInt(/* str: */ "123", /* radix: */ 8) + parseInt(« str: » "123", « radix: » 8) ``` @@ -101,7 +103,7 @@ `"rangeVariableTypes"` controls inlay hints for variable types in range statements: ```go - for k/* int*/, v/* string*/ := range []string{} { + for k« int», v« string» := range []string{} { fmt.Println(k, v) } ```
diff --git a/gopls/doc/release/v0.22.0.md b/gopls/doc/release/v0.22.0.md index ae20a84..b3f0c7f 100644 --- a/gopls/doc/release/v0.22.0.md +++ b/gopls/doc/release/v0.22.0.md
@@ -4,8 +4,8 @@ In this release: -- commits: https://go.googlesource.com/tools/+log/refs/heads/gopls-release-branch.0.21..refs/heads/gopls-release-branch.0.22 -- issues closed: https://github.com/golang/go/milestone/415?closed=1 (plus +- [commits](https://go.googlesource.com/tools/+log/refs/heads/gopls-release-branch.0.21..refs/heads/gopls-release-branch.0.22) +- [issues closed](https://github.com/golang/go/milestone/415?closed=1) (plus a number of modernizer fixes in the [go1.26 milestone](https://github.com/golang/go/milestone/408?closed=1)). Key features are described below. @@ -15,7 +15,7 @@ recent years that the fraction of gopls telemetry reports clearly due to hardware failure is significant and growing. -### Interactive code transformations +## Interactive code transformations <!-- golang/go#76331 --> This release includes support for a non-standard LSP feature that allows code actions to have [interactive dialogs](https://go.dev/issue/76331). @@ -60,7 +60,10 @@ - The `importsSource` setting is deprecated and will be removed in the next release. - The `-port=int` debugging flag (redundant with `-listen`) has been removed. - The `-listen=address` flag now rejects an implicit host (e.g. `:0`). - You must explicitly specify a host such as `0.0.0.0` or (preferably) `localhost`. + You must explicitly specify a host such as `0.0.0.0` or (preferably) `localhost` +- When the `semanticTokens` setting is disabled (the default), the server + no longer advertises the semantic token capability during server initialization. + This prevents the client from sending unnecessary requests. ## Navigation features @@ -119,3 +122,12 @@ The `yield` analyzer, which detects problems in iterators that fail to stop once `yield()` returns false, has been rewritten as a non-sparse Killdall-style monotone dataflow analysis, improving its precision. + +## Server-side file watching + +Gopls introduces an experimental server-side file watching mechanism to monitor file system events directly. This feature supplements standard LSP file change notifications in environments or editor clients where client-side events may be unreliable or dropped. + +This feature is configured via the internal setting `fileWatcher`, which supports three strategies: +- `"off"` (default): the client is solely responsible for change notification +- `"poll"`: the server periodically scans workspace directories, using optimizations similar to `git status` +- `"fsnotify"`: the server uses kernel support for file-change notification. This strategy is system dependent and may need to open many directories to watch a large workspace, risking [file descriptor exhaustion](https://github.com/fsnotify/fsnotify#kqueue-macos-all-bsd-systems)
diff --git a/gopls/doc/release/v0.23.0.md b/gopls/doc/release/v0.23.0.md new file mode 100644 index 0000000..07e258b --- /dev/null +++ b/gopls/doc/release/v0.23.0.md
@@ -0,0 +1,68 @@ +--- +title: "Gopls release v0.23.0 (expected July 2026)" +--- + +In this release: + +- [commits](https://go.googlesource.com/tools/+log/refs/heads/gopls-release-branch.0.22..refs/heads/gopls-release-branch.0.23) +- [issues closed](https://github.com/golang/go/milestone/432?closed=1) + +Key features are described below. + +The main purpose of this relatively small release is to ensure that users of the Go 1.27 release candidate enjoy full support for the latest language features, alongside various bug fixes and improvements. + +## Configuration changes + +The `errorsastype` analyzer added in the previous release (which +detects shadowing mistakes using `errors.AsType`) was renamed to +`errorsastypeshadow` to avoid an unfortunate conflict with a +modernizer of the same name, which suggests fixes to replace calls to +`errors.As` by `AsType`. + +As part of an ongoing effort to revamp the `gopls` CLI (which is not a supported stable interface) to make it more useful and efficient for both humans and agents, several obsolete subcommands (`gopls fix`, `gopls inspect`, and `gopls bug`) have been removed. Users should use `gopls codeaction` instead of `gopls fix`, and `gopls remote` instead of `gopls inspect`. The main `serve` command continues to work as expected. + +The `importsSource` setting has been removed (commit `289728936`). Gopls now exclusively uses its built-in imports engine, and users who still have this option in their configuration will receive a warning diagnostic. + +## Web-based features + +## Editing features + +### Autocompletion suggests Go 1.27 promoted fields +<!-- golang/go#78553 --> +In files targeting Go 1.27 or later, autocompletion now suggests fields promoted from embedded structs within struct literals. + +### Inlay hints for Go 1.27 promoted fields +<!-- golang/go#78553 --> +In files targeting Go 1.27 or later, composite literal field name inlay hints (`compositeLiteralFields`) show the implicit selection path for promoted fields (e.g. `A{«B.C.»F: 0}`). + +## Analysis features + +### `sqlrowserr` analyzer +<!-- golang/go#17747 --> +The `sqlrowserr` analyzer reports failure to call the `Err` +method after calling the `Next` method of `sql.Rows` in a loop. + +### `inline` analyzer skips inlining in dedicated test files +<!-- golang/go#76190 --> +The `inline` analyzer now avoids inlining calls to a symbol from its dedicated test files (e.g., uses in `foo_test.go` of a symbol declared in `foo.go`), for functions, constants, and type aliases. + +### `deprecated` analyzer supports trailing line comments on fields and interface methods +<!-- golang/go#79801 --> +The `deprecated` analyzer now detects deprecations specified in trailing line comments next to struct fields and interface methods (for example, `field int // Deprecated: use newField`), in anticipation of updates to the [Go Deprecated guidelines](https://go.dev/wiki/Deprecated). + +## Code transformation features + +### `stubmethods` code action supports Go 1.27 generic interfaces +<!-- golang/go#77549 --> +The `Declare missing methods` (`stubmethods`) quick-fix now supports instantiating generic interfaces in files targeting Go 1.27 or later, generating correct method signatures with concrete type arguments. + +### `fillstruct` code action supports Go 1.27 promoted fields +<!-- golang/go#78553 --> +In files targeting Go 1.27 or later, the `fillstruct` code action now populates promoted fields of embedded structs directly in the parent struct literal. It uses a "minimum expansion" style: it only flattens/expands embedded structs along paths that have already been partially initialized by the user, leaving other embedded structs unexpanded. + +### "Add test for func" uses `t.Context()` +<!-- golang/go#79647 --> +When the function or method under test takes `context.Context` as its +first parameter and the module is on Go 1.24 or later, the generated +test now passes `t.Context()` instead of `context.Background()`. +Modules on older Go versions continue to receive `context.Background()`.
diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index 44ab0e7..93e3e87 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md
@@ -212,7 +212,18 @@ semanticTokens determines whether gopls will return a SemanticTokensProvider at initialization, or respond -to request for semantic tokens. +to requests for semantic tokens. + +This setting being `false` won't necessary disable the client's calls +for semantic tokens. If you want that, it would need to be configured in +the client. For example, in VSCode, this would disable all Go semantic +token calls to the LSP server: + +```json5 +"[go]": { + "editor.semanticHighlighting.enabled": false, +} +``` Default: `false`. @@ -280,6 +291,16 @@ Default: `false`. +<a id='moveType'></a> +### `moveType bool` + +**This setting is experimental and may be deleted.** + +moveType enables producing Move Type codeactions. The implementation +is unfinished so we use this setting to gate its use. + +Default: `false`. + <a id='completion'></a> ## Completion @@ -625,6 +646,29 @@ Default: `"all"`. +<a id='fileWatcher'></a> +### `fileWatcher enum` + +**This setting is experimental and may be deleted.** + +fileWatcher specifies the server-side file watching strategy used by gopls. + +By default, this is set to "off", meaning gopls relies exclusively on the +language client (e.g., the editor) to send file change notifications. + +Available options: + - "off" : Client-driven watching (default) + - "fsnotify" : OS-level event notifications + - "poll" : Periodic directory scanning + +Must be one of: + +* `"fsnotify"` +* `"off"` +* `"poll"` + +Default: `"off"`. + <a id='maxFileCacheBytes'></a> ### `maxFileCacheBytes int64`
diff --git a/gopls/doc/troubleshooting.md b/gopls/doc/troubleshooting.md index 2dea87b..1ec3615 100644 --- a/gopls/doc/troubleshooting.md +++ b/gopls/doc/troubleshooting.md
@@ -35,7 +35,7 @@ 1. The output of `gopls version` on the command line. 1. A complete gopls log file from a session where the issue occurred. It should have a `go env for <workspace folder>` log line near the beginning. It's also helpful to tell us the timestamp the problem occurred, so we can find it the log. See the [instructions](#capture-logs) for information on how to capture gopls logs. -Your editor may have a command that fills out some of the necessary information, such as `:GoReportGitHubIssue` in `vim-go`. Otherwise, you can use `gopls bug` on the command line. If neither of those work you can start from scratch directly on the [Go issue tracker](https://github.com/golang/go/issues/new?title=x%2Ftools%2Fgopls%3A%20%3Cfill%20this%20in%3E). +Your editor may have a command that fills out some of the necessary information, such as `:GoReportGitHubIssue` in `vim-go`. Otherwise, use the [Go issue tracker](https://github.com/golang/go/issues/new?title=x%2Ftools%2Fgopls%3A%20%3Cfill%20this%20in%3E). ## Capture logs
diff --git a/gopls/go.mod b/gopls/go.mod index efa6b83..6eca59c 100644 --- a/gopls/go.mod +++ b/gopls/go.mod
@@ -6,18 +6,19 @@ github.com/fatih/gomodifytags v1.17.1-0.20250423142747-f3939df9aa3c github.com/fsnotify/fsnotify v1.9.0 github.com/google/go-cmp v0.7.0 - github.com/google/jsonschema-go v0.4.2 + github.com/google/jsonschema-go v0.4.3 github.com/jba/templatecheck v0.7.1 - github.com/modelcontextprotocol/go-sdk v1.4.0 - golang.org/x/mod v0.35.0 - golang.org/x/sync v0.20.0 - golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa - golang.org/x/text v0.36.0 - golang.org/x/tools v0.43.0 - golang.org/x/vuln v1.1.4 + 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 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.7.0 - mvdan.cc/gofumpt v0.9.2 + mvdan.cc/gofumpt v0.10.0 mvdan.cc/xurls/v2 v2.6.0 ) @@ -27,12 +28,11 @@ github.com/fatih/structtag v1.2.0 // indirect github.com/google/safehtml v0.1.0 // indirect github.com/segmentio/asm v1.2.1 // indirect - github.com/segmentio/encoding v0.5.3 // 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-20260312153236-7ab1446f8b90 // indirect + golang.org/x/exp/typeparams v0.0.0-20260611194520-c48552f49976 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect + golang.org/x/sys v0.46.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect )
diff --git a/gopls/go.sum b/gopls/go.sum index 4ab3ce9..907b37c 100644 --- a/gopls/go.sum +++ b/gopls/go.sum
@@ -8,15 +8,15 @@ github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= +github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= -github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= +github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= github.com/jba/templatecheck v0.7.1 h1:yOEIFazBEwzdTPYHZF3Pm81NF1ksxx1+vJncSEwvjKc= @@ -25,14 +25,14 @@ 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.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8= -github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E= +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/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= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= -github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= -github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -48,8 +48,10 @@ golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= -golang.org/x/exp/typeparams v0.0.0-20260312153236-7ab1446f8b90 h1:cfW8UCYSVdPblxA7qQe3o5Iad55Vsx4BFmuGS9RNOmc= -golang.org/x/exp/typeparams v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= +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/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= @@ -61,8 +63,10 @@ golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= 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/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= @@ -76,6 +80,9 @@ golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= 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/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= @@ -83,8 +90,9 @@ golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= 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/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= @@ -98,10 +106,13 @@ golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +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/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= @@ -115,6 +126,8 @@ golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= 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/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= @@ -128,14 +141,16 @@ golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= 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/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.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= -golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= +golang.org/x/vuln v1.4.0 h1:FpmTZiV4PyqY3lFfuCkz1JftEXb/+8M2NEkjJM5TF4g= +golang.org/x/vuln v1.4.0/go.mod h1:FJ7XyKs83nAdxQ7PMsia2PoynwZMJ/QajXVMBBIgFe8= 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= @@ -143,7 +158,7 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= -mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= -mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= +mvdan.cc/gofumpt v0.10.0 h1:yGGpRS2pBN2OQIi7b21IXknJna7faPkFaVfHLrN6Euo= +mvdan.cc/gofumpt v0.10.0/go.mod h1:sU2ElXHzOEmvoPqfutYG7uunlueR4K2T1JFml40SzP4= mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
diff --git a/gopls/internal/analysis/deprecated/deprecated.go b/gopls/internal/analysis/deprecated/deprecated.go index 2341120..6b71962 100644 --- a/gopls/internal/analysis/deprecated/deprecated.go +++ b/gopls/internal/analysis/deprecated/deprecated.go
@@ -156,15 +156,19 @@ // them both as Facts and the return value. This is a simplified copy // of staticcheck's fact_deprecated analyzer. func collectDeprecatedNames(pass *analysis.Pass, ins *inspector.Inspector) (deprecatedNames, error) { - doDocs := func(names []*ast.Ident, docs *ast.CommentGroup) { - alt := strings.TrimPrefix(internalastutil.Deprecation(docs), "Deprecated: ") - if alt == "" { - return - } + doDocs := func(names []*ast.Ident, docs ...*ast.CommentGroup) { + for _, doc := range docs { + // Find the first doc with a deprecation marker. + alt := strings.TrimPrefix(internalastutil.Deprecation(doc), "Deprecated: ") + if alt == "" { + continue + } - for _, name := range names { - obj := pass.TypesInfo.ObjectOf(name) - pass.ExportObjectFact(obj, &deprecationFact{alt}) + for _, name := range names { + obj := pass.TypesInfo.ObjectOf(name) + pass.ExportObjectFact(obj, &deprecationFact{alt}) + } + return } } @@ -222,14 +226,16 @@ names = node.Names case *ast.StructType: for _, field := range node.Fields.List { - doDocs(field.Names, field.Doc) + doDocs(field.Names, field.Doc, field.Comment) } case *ast.InterfaceType: for _, field := range node.Methods.List { - doDocs(field.Names, field.Doc) + doDocs(field.Names, field.Doc, field.Comment) } } if docs != nil && len(names) > 0 { + // Only consider line comments for struct fields and interface + // methods. doDocs(names, docs) } })
diff --git a/gopls/internal/analysis/deprecated/testdata/src/a/a.go b/gopls/internal/analysis/deprecated/testdata/src/a/a.go index 7ffa07d..66c1117 100644 --- a/gopls/internal/analysis/deprecated/testdata/src/a/a.go +++ b/gopls/internal/analysis/deprecated/testdata/src/a/a.go
@@ -4,11 +4,35 @@ package usedeprecated -import "io/ioutil" // want "\"io/ioutil\" is deprecated: .*" +import ( + "io/ioutil" // want "\"io/ioutil\" is deprecated: .*" + + "legacy" +) func x() { _, _ = ioutil.ReadFile("") // want "ioutil.ReadFile is deprecated: As of Go 1.16, .*" Legacy() // expect no deprecation notice. + + x := legacy.Object{} // want "legacy.Object is deprecated: Use obj instead" + + x.DocCommentMethod(1) // expect no deprecation notice, deprecation is only on interface. + x.LineCommentMethod(1) // expect no deprecation notice, deprecation is only on interface. + + _ = x.DocCommentField // want "x.DocCommentField is deprecated: Use `Field` instead." + _ = x.LineCommentField // want "x.LineCommentField is deprecated: Use `Field` instead." + + // Make sure that the doc comment is chosen over the line comment if both have + // deprecation tags. + _ = x.BothCommentField // want "x.BothCommentField is deprecated: Doc comment chosen" + + legacy.Legacy() // want "Legacy is deprecated: use X instead." + y(x) +} + +func y(i legacy.Interface) { + i.DocCommentMethod(1) // want "i.DocCommentMethod is deprecated: Use Method instead." + i.LineCommentMethod(1) // want "i.LineCommentMethod is deprecated: Use Method instead." } // Legacy is deprecated.
diff --git a/gopls/internal/analysis/deprecated/testdata/src/legacy/legacy.go b/gopls/internal/analysis/deprecated/testdata/src/legacy/legacy.go new file mode 100644 index 0000000..badd93f --- /dev/null +++ b/gopls/internal/analysis/deprecated/testdata/src/legacy/legacy.go
@@ -0,0 +1,38 @@ +// Package legacy +package legacy + +// Object docs +// +// Deprecated: Use obj instead. +type Object struct { + // Don't use. + // + // Deprecated: Use `Field` instead. + // + // Paragraph after. + DocCommentField int + + LineCommentField int // Deprecated: Use `Field` instead. + + // Deprecated: Doc comment chosen + BothCommentField int // Deprecated: Line comment chosen +} + +type Interface interface { + // Deprecated: Use Method instead. + DocCommentMethod(int) int + LineCommentMethod(int) int // Old method. Deprecated: Use Method instead. +} + +// No deprecation notice in concrete method docs. +func (Object) DocCommentMethod(int) int { + return 1 +} + +// No deprecation notice in concrete method docs. +func (Object) LineCommentMethod(int) int { + return 1 +} + +// Deprecated: use X instead. +func Legacy() {}
diff --git a/gopls/internal/analysis/errorsastypeshadow/doc.go b/gopls/internal/analysis/errorsastypeshadow/doc.go new file mode 100644 index 0000000..b233744 --- /dev/null +++ b/gopls/internal/analysis/errorsastypeshadow/doc.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. + +// Package errorsastypeshadow checks for shadowing problems when +// [errors.AsType] is used in an if/else chain. +// +// # Analyzer errorsastypeshadow +// +// errorsastypeshadow: report shadowing of errors.AsType[T] in if/else chains +// +// For example: +// +// err := f() +// if err, ok := errors.AsType[*FooErr](err); ok { +// useFoo(err) +// } else if err, ok := errors.AsType[*BarErr](err); ok { +// useBar(err) +// } +// +// In this case, the second call to errors.AsType does not operate on the +// original error. Instead, its operand is the zero value of type *FooErr +// produced by the first if statement; this is invariably a mistake. +package errorsastypeshadow
diff --git a/gopls/internal/analysis/errorsastype/errorsastype.go b/gopls/internal/analysis/errorsastypeshadow/errorsastypeshadow.go similarity index 91% rename from gopls/internal/analysis/errorsastype/errorsastype.go rename to gopls/internal/analysis/errorsastypeshadow/errorsastypeshadow.go index 961999f..b703399 100644 --- a/gopls/internal/analysis/errorsastype/errorsastype.go +++ b/gopls/internal/analysis/errorsastypeshadow/errorsastypeshadow.go
@@ -2,11 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// Package errorsastype checks whether [errors.AsType] is used correctly -// in if/else chains. -package errorsastype +package errorsastypeshadow import ( + _ "embed" "fmt" "go/ast" "go/token" @@ -15,30 +14,19 @@ "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" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/typesinternal/typeindex" ) -const Doc = `Reports misuse of errors.AsType[T] in if/else chains. -For example: - - err := f() - if err, ok := errors.AsType[*FooErr](err); ok { - useFoo(err) - } else if err, ok := errors.AsType[*BarErr](err); ok { - useBar(err) - } - -In this case, the second call to errors.AsType does not operate on the -original error. Instead, its operand is the zero value of type *FooErr -produced by the first if statement; this is invariably a mistake. -` +//go:embed doc.go +var doc string var Analyzer = &analysis.Analyzer{ - Name: "errorsastype", - Doc: Doc, - URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/errorsastype", + Name: "errorsastypeshadow", + Doc: analyzerutil.MustExtractDoc(doc, "errorsastypeshadow"), + URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/errorsastypeshadow", Requires: []*analysis.Analyzer{typeindexanalyzer.Analyzer}, Run: run, }
diff --git a/gopls/internal/analysis/errorsastype/errorsastype_test.go b/gopls/internal/analysis/errorsastypeshadow/errorsastypeshadow_test.go similarity index 68% rename from gopls/internal/analysis/errorsastype/errorsastype_test.go rename to gopls/internal/analysis/errorsastypeshadow/errorsastypeshadow_test.go index b50e502..8b8f719 100644 --- a/gopls/internal/analysis/errorsastype/errorsastype_test.go +++ b/gopls/internal/analysis/errorsastypeshadow/errorsastypeshadow_test.go
@@ -2,18 +2,18 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package errorsastype_test +package errorsastypeshadow_test import ( "testing" "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/gopls/internal/analysis/errorsastype" + "golang.org/x/tools/gopls/internal/analysis/errorsastypeshadow" "golang.org/x/tools/internal/testenv" ) func Test(t *testing.T) { testenv.NeedsGo1Point(t, 26) // AsType introduced in 1.26 testdata := analysistest.TestData() - analysistest.Run(t, testdata, errorsastype.Analyzer, "astype") + analysistest.Run(t, testdata, errorsastypeshadow.Analyzer, "errorsastypeshadow") }
diff --git a/gopls/internal/analysis/errorsastypeshadow/main.go b/gopls/internal/analysis/errorsastypeshadow/main.go new file mode 100644 index 0000000..be95b4e --- /dev/null +++ b/gopls/internal/analysis/errorsastypeshadow/main.go
@@ -0,0 +1,15 @@ +// 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 errorsastypeshadow command runs the errorsastypeshadow analyzer. +package main + +import ( + "golang.org/x/tools/go/analysis/singlechecker" + "golang.org/x/tools/gopls/internal/analysis/errorsastypeshadow" +) + +func main() { singlechecker.Main(errorsastypeshadow.Analyzer) }
diff --git a/gopls/internal/analysis/errorsastype/testdata/src/astype/astype.go b/gopls/internal/analysis/errorsastypeshadow/testdata/src/errorsastypeshadow/errorsastypeshadow.go similarity index 85% rename from gopls/internal/analysis/errorsastype/testdata/src/astype/astype.go rename to gopls/internal/analysis/errorsastypeshadow/testdata/src/errorsastypeshadow/errorsastypeshadow.go index f5dc672..7bebb05 100644 --- a/gopls/internal/analysis/errorsastype/testdata/src/astype/astype.go +++ b/gopls/internal/analysis/errorsastypeshadow/testdata/src/errorsastypeshadow/errorsastypeshadow.go
@@ -1,4 +1,4 @@ -package astype +package errorsastypeshadow import "errors" @@ -20,7 +20,7 @@ func _(err error) { if err, ok := errors.AsType[*FooError](err); ok { _ = err - } else if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*astype\.FooError` + } else if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*errorsastypeshadow\.FooError` _ = err } @@ -40,7 +40,7 @@ err = &FooError{} } else if err2 := err; modifies(&err2) && modifies(err2) { err = &FooError{} - } else if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*astype\.FooError` + } else if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*errorsastypeshadow\.FooError` _ = err } @@ -65,9 +65,9 @@ if err, ok := errors.AsType[*FooError](err); ok { _ = err - } else if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*astype\.FooError from failed prior call to AsType` + } else if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*errorsastypeshadow\.FooError from failed prior call to AsType` _ = err - } else if err, ok := errors.AsType[error](err); ok { // want `err passed to AsType is the zero value of \*astype\.BarError from failed prior call to AsType` + } else if err, ok := errors.AsType[error](err); ok { // want `err passed to AsType is the zero value of \*errorsastypeshadow\.BarError from failed prior call to AsType` _ = err } else if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of error from failed prior call to AsType` _ = err @@ -102,7 +102,7 @@ err = &FooError{} } else { _ = err - if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*astype\.FooError` + if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*errorsastypeshadow\.FooError` _ = err } } @@ -127,7 +127,7 @@ if err, ok := errors.AsType[*FooError](err); ok { _ = err } else if global { - if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*astype\.FooError` + if err, ok := errors.AsType[*BarError](err); ok { // want `err passed to AsType is the zero value of \*errorsastypeshadow\.FooError` _ = err } }
diff --git a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go index 97cbd70..bbbd0f7 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct.go +++ b/gopls/internal/analysis/fillstruct/fillstruct.go
@@ -20,72 +20,171 @@ "go/printer" "go/token" "go/types" + "slices" "strings" "unicode" "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/analysis/fillreturns" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/fuzzy" "golang.org/x/tools/gopls/internal/util/cursorutil" "golang.org/x/tools/gopls/internal/util/safetoken" + "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" ) -// Diagnose computes diagnostics for fillable struct literals overlapping with -// the provided start and end position of file f. +// Diagnose computes a diagnostic for the enclosing struct literal enclosing +// the provided start and end position of curFile. +// +// If the target struct is already fully populated, no diagnostic is reported. // // The diagnostic contains a lazy fix; the actual patch is computed // (via the ApplyFix command) by a call to [SuggestedFix]. -// -// 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 - ast.Inspect(f, func(n ast.Node) bool { - if n == nil { - return true // pop +func Diagnose(curFile inspector.Cursor, start, end token.Pos, pkg *types.Package, info *types.Info) (diags []analysis.Diagnostic) { + cur, _, _, _ := astutil.Select(curFile, start, end) + + // Direct reference to embedded fields in struct literals requires Go 1.27+. + var supportsEmbedFields bool + if v := info.FileVersions[curFile.Node().(*ast.File)]; v != "" { + supportsEmbedFields = versions.AtLeast(v, versions.Go1_27) + } + + var lits []*ast.CompositeLit + for c := range cur.Enclosing((*ast.CompositeLit)(nil)) { + lits = append(lits, c.Node().(*ast.CompositeLit)) + } + for c := range cur.Preorder((*ast.CompositeLit)(nil)) { + expr := c.Node().(*ast.CompositeLit) + // Avoid double-counting when cur.Node() is itself a [ast.CompositeLit]. + if expr == cur.Node() { + continue } - if start.IsValid() && n.End() < start || end.IsValid() && n.Pos() > end { - return false // skip non-overlapping subtree + if expr.Pos() <= end && expr.End() >= start { + lits = append(lits, expr) } - expr, ok := n.(*ast.CompositeLit) - if !ok { - return true - } + } + +nextComp: + for _, expr := range lits { typ := info.TypeOf(expr) if typ == nil { - return true + continue } // Find reference to the type declaration of the struct being initialized. typ = typeparams.Deref(typ) tStruct, ok := typeparams.CoreType(typ).(*types.Struct) if !ok { - return true + continue } + // Inv: typ is the possibly-named struct type. - fieldCount := tStruct.NumFields() - - // Skip any struct that is already populated or that has no fields. - if fieldCount == 0 || fieldCount == len(expr.Elts) { - return true + // fillableFields returns the number of fields in the struct that are + // accessible and thus can be filled in the current package. + fillableFields := func(t *types.Struct) (count int) { + for field := range t.Fields() { + if field.Pkg() == pkg || field.Exported() { + count++ + } + } + return count } - // Are any fields in need of filling? - var fillableFields []string - for i := range fieldCount { - field := tStruct.Field(i) - // Ignore fields that are not accessible in the current package. - if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { + // fieldCount tracks the maximum number of fillable fields under the minimum + // required expansion of embedded structs. + // + // It starts as the number of direct fields of the struct. + // + // Example structure: + // A + // ├── A1 + // ├── A2 + // └── B (embedded) + // ├── B1 (promoted to A) + // ├── B2 + // └── C (embedded) + // ├── C1 + // └── C2 + // + // When a promoted field is filled (e.g., B1 in A{B1: 1}), we expand + // fieldCount to include the fields of that embedded struct (B1, B2, C), + // while removing the embedded struct B itself from the count. + // + // This means we need to fill all fields at B's level (B1, B2, C) and + // its parent levels (A1, A2), but we don't need to fill B anymore. C + // will be filled as C: C{} unless its own fields are accessed. + fieldCount := fillableFields(tStruct) + + var seen map[*types.Var]bool + + nextElem: + for _, el := range expr.Elts { + kv, ok := el.(*ast.KeyValueExpr) + if !ok { + continue nextComp + } + + key, ok := kv.Key.(*ast.Ident) + if !ok { continue } - fillableFields = append(fillableFields, fmt.Sprintf("%s: %s", field.Name(), field.Type().String())) + + seln, ok := types.LookupSelection(typ, true, pkg, key.Name) + if !ok { + continue + } + + if len(seln.Index()) > 1 && !supportsEmbedFields { + continue + } + + length := len(seln.Index()) + if length == 0 { + continue + } + + var ( + fields = make([]*types.Var, length-1) + typs = make([]*types.Struct, length-1) + ) + + i := 0 + for field := range typesinternal.ImplicitFieldSelections(seln) { + t := field.Type() + ptr, isPtr := t.Underlying().(*types.Pointer) + if isPtr { + t = ptr.Elem() + } + structType, ok := t.Underlying().(*types.Struct) + if !ok { + continue nextElem + } + + fields[i] = field + typs[i] = structType + i++ + } + + for i, field := range fields { + if !seen[field] { + if seen == nil { + seen = make(map[*types.Var]bool) + } + seen[field] = true + fieldCount += fillableFields(typs[i]) - 1 + } + } } - if len(fillableFields) == 0 { - return true + + // Skip any struct that is already populated or that has no fillable fields. + if fieldCount == 0 || fieldCount == len(expr.Elts) { + continue } // Derive a name for the struct type. @@ -95,22 +194,26 @@ name = types.TypeString(typ, typesinternal.NameRelativeTo(pkg)) } else { // anonymous struct type - totalFields := len(fillableFields) - const maxLen = 20 - // Find the index to cut off printing of fields. - var i, fieldLen int - for i = range fillableFields { - if fieldLen > maxLen { - break + var buf strings.Builder + buf.WriteString("anonymous struct{ ") + + var printedCount int + + for field := range tStruct.Fields() { + if field.Pkg() == pkg || field.Exported() { + if buf.Len() > 38 || printedCount > 3 { + buf.WriteString("...") + break + } + fmt.Fprintf(&buf, "%s: %s; ", field.Name(), field.Type().String()) + printedCount++ } - fieldLen += len(fillableFields[i]) } - fillableFields = fillableFields[:i] - if i < totalFields { - fillableFields = append(fillableFields, "...") - } - name = fmt.Sprintf("anonymous struct{ %s }", strings.Join(fillableFields, ", ")) + + buf.WriteString(" }") + name = buf.String() } + diags = append(diags, analysis.Diagnostic{ Message: fmt.Sprintf("%s literal has missing fields", name), Pos: expr.Pos(), @@ -121,8 +224,7 @@ // No TextEdits => computed later by gopls. }}, }) - return true - }) + } return diags } @@ -144,86 +246,15 @@ return nil, nil, fmt.Errorf("no enclosing ast.Node") } expr, _ := cursorutil.FirstEnclosing[*ast.CompositeLit](cur) - typ := info.TypeOf(expr) - if typ == nil { - return nil, nil, fmt.Errorf("no composite literal") + + // newElts accumulates the newly generated field element AST nodes. + newElts, err := populateMissingFields(info, pkg, file, expr, pos) + if err != nil { + return nil, nil, err } - // Find reference to the type declaration of the struct being initialized. - typ = typeparams.Deref(typ) - tStruct, ok := typ.Underlying().(*types.Struct) - if !ok { - return nil, nil, fmt.Errorf("%s is not a (pointer to) struct type", - types.TypeString(typ, typesinternal.NameRelativeTo(pkg))) - } - // Inv: typ is the possibly-named struct type. - - fieldCount := tStruct.NumFields() - - // Check which types have already been filled in. (we only want to fill in - // the unfilled types, or else we'll blat user-supplied details) - prefilledFields := map[string]ast.Expr{} - var elts []ast.Expr - for _, e := range expr.Elts { - if kv, ok := e.(*ast.KeyValueExpr); ok { - if key, ok := kv.Key.(*ast.Ident); ok { - prefilledFields[key.Name] = kv.Value - elts = append(elts, kv) - } - } - } - - var fieldTyps []types.Type - for i := range fieldCount { - field := tStruct.Field(i) - // Ignore fields that are not accessible in the current package. - if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { - fieldTyps = append(fieldTyps, nil) - continue - } - fieldTyps = append(fieldTyps, field.Type()) - } - matches := fillreturns.MatchingIdents(fieldTyps, file, start, info, pkg) - qual := typesinternal.FileQualifier(file, pkg) - - for i, fieldTyp := range fieldTyps { - if fieldTyp == nil { - continue // TODO(adonovan): is this reachable? - } - fieldName := tStruct.Field(i).Name() - if _, ok := prefilledFields[fieldName]; ok { - // We already stored these when looping over expr.Elt. - // Want to preserve the original order of prefilled fields - continue - } - - kv := &ast.KeyValueExpr{ - Key: &ast.Ident{ - Name: fieldName, - }, - } - - names, ok := matches[fieldTyp] - if !ok { - return nil, nil, fmt.Errorf("invalid struct field type: %v", fieldTyp) - } - - // Find the name most similar to the field name. - // If no name matches the pattern, generate a zero value. - // NOTE: We currently match on the name of the field key rather than the field type. - if best := fuzzy.BestMatch(fieldName, names); best != "" { - kv.Value = ast.NewIdent(best) - } else if expr, isValid := populateValue(fieldTyp, qual); isValid { - kv.Value = expr - } else { - return nil, nil, nil // no fix to suggest - } - - elts = append(elts, kv) - } - - // If all of the struct's fields are unexported, we have nothing to do. - if len(elts) == 0 { + // If we failed to generate any new fields to fill, we have nothing to do. + if len(newElts) == 0 { return nil, nil, fmt.Errorf("no elements to fill") } @@ -238,16 +269,11 @@ index := bytes.Index(firstLine, trimmed) whitespace := firstLine[:index] - // Write a new composite literal "_{...}" composed of all prefilled and new elements, - // preserving existing formatting and comments. - // An alternative would be to only format the new fields, - // but by printing the entire composite literal, we ensure - // that the result is gofmt'ed. var buf bytes.Buffer buf.WriteString("_{\n") fcmap := ast.NewCommentMap(fset, file, file.Comments) comments := fcmap.Filter(expr).Comments() // comments inside the expr, in source order - for _, elt := range elts { + for _, elt := range slices.Concat(expr.Elts, newElts) { // Print comments before the current elt for len(comments) > 0 && comments[0].Pos() < elt.Pos() { for _, co := range comments[0].List { @@ -304,6 +330,154 @@ }, nil } +// populateMissingFields returns a slice of ast.Expr (specifically *ast.KeyValueExpr) +// representing the populated missing fields of tStruct that can be filled. +// +// It traverses the struct fields in depth-first order (DFS), flattening embedded +// structs where the user has partially initialized their sub-fields, and attempts +// to generate a matching local variable or zero-value for each missing field. +// Fields that cannot be populated are skipped, returning only the ones that can +// be successfully populated. +func populateMissingFields(info *types.Info, pkg *types.Package, file *ast.File, expr *ast.CompositeLit, pos token.Pos) ([]ast.Expr, error) { + typ := info.TypeOf(expr) + if typ == nil { + return nil, fmt.Errorf("no composite literal") + } + + // Find reference to the type declaration of the struct being initialized. + typ = typeparams.Deref(typ) + tStruct, ok := typ.Underlying().(*types.Struct) + if !ok { + return nil, fmt.Errorf("%s is not a (pointer to) struct type", + types.TypeString(typ, typesinternal.NameRelativeTo(pkg))) + } + + // Inv: typ is the possibly-named struct type. + + // Direct reference to embedded fields in struct literals requires Go 1.27+. + var supportsEmbedFields bool + if v := info.FileVersions[file]; v != "" { + supportsEmbedFields = versions.AtLeast(v, versions.Go1_27) + } + + // explicit records whether each encountered field is explicitly initialized. + // Each non-last field in a selection path is accessed implicitly (false), + // and the last field is accessed explicitly (true). + // Fields not present in this map are completely missing and need to be populated. + explicit := make(map[*types.Var]bool) + for _, elem := range expr.Elts { + kv, ok := elem.(*ast.KeyValueExpr) + if !ok { + return nil, fmt.Errorf("cannot fill struct literal containing unkeyed elements") + } + + key, ok := kv.Key.(*ast.Ident) + if !ok { + continue + } + + seln, ok := types.LookupSelection(tStruct, true, pkg, key.Name) + if !ok { + continue + } + + if len(seln.Index()) > 1 && !supportsEmbedFields { + continue + } + + field, ok := seln.Obj().(*types.Var) + if !ok { + continue + } + + isExplicit, ok := explicit[field] + if ok && !isExplicit { + return nil, fmt.Errorf("cannot fill both %q and its subfields", field.Name()) + } + explicit[field] = true // last field is explicit + + for field := range typesinternal.ImplicitFieldSelections(seln) { + if explicit[field] { + return nil, fmt.Errorf("cannot fill both %q and its subfields", field.Name()) + } + explicit[field] = false // all the others are implicit + } + } + + // Collect the final list of fields to be filled. Traverse the struct fields + // in depth-first order, flattening embedded fields that are marked for + // expansion because the user has partially initialized their sub-fields. + var fields []*types.Var + { + var addFields func(*types.Struct) + addFields = func(tStruct *types.Struct) { + for field := range tStruct.Fields() { + if field.Pkg() != pkg && !field.Exported() { + continue + } + + isExplicit, ok := explicit[field] + if !ok { + fields = append(fields, field) + continue + } + + if !isExplicit { + tInner, ok := typeparams.Deref(field.Type()).Underlying().(*types.Struct) + if !ok { + continue // can't happen + } + addFields(tInner) + } + } + } + addFields(tStruct) + } + + typs := make([]types.Type, len(fields)) + for i, f := range fields { + typs[i] = f.Type() + } + + var newElts []ast.Expr + matches := fillreturns.MatchingIdents(typs, file, pos, info, pkg) + qual := typesinternal.FileQualifier(file, pkg) + + // Iterate in the order fields were discovered to ensure deterministic + // output and match the struct definition order. + for _, field := range fields { + // TODO(hxjiang): Provide a separate quick-fix option to fill the + // struct using nested composite literals, ensuring we always have a + // valid suggestion even if shadowing prevents flattening. + if obj, _, _ := types.LookupFieldOrMethod(tStruct, true, pkg, field.Name()); obj != field { + return nil, fmt.Errorf("field %s shadowed", field.Name()) + } + + kv := &ast.KeyValueExpr{ + Key: ast.NewIdent(field.Name()), + } + + names, ok := matches[field.Type()] + if !ok { + return nil, fmt.Errorf("invalid struct field type: %v", field.Type()) + } + + // Find the name most similar to the field name. + // If no name matches the pattern, generate a zero value. + // NOTE: We currently match on the name of the field key rather than the field type. + if best := fuzzy.BestMatch(field.Name(), names); best != "" { + kv.Value = ast.NewIdent(best) + } else if expr, isValid := populateValue(field.Type(), qual); isValid { + kv.Value = expr + } else { + continue + } + + newElts = append(newElts, kv) + } + return newElts, nil +} + // indent works line by line through str, indenting (prefixing) each line with // ind. func indent(str, ind []byte) []byte {
diff --git a/gopls/internal/analysis/fillstruct/fillstruct_test.go b/gopls/internal/analysis/fillstruct/fillstruct_test.go index 3ae34e9..63bb751 100644 --- a/gopls/internal/analysis/fillstruct/fillstruct_test.go +++ b/gopls/internal/analysis/fillstruct/fillstruct_test.go
@@ -5,11 +5,13 @@ package fillstruct_test import ( - "go/token" + "fmt" "testing" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/analysis/fillstruct" "golang.org/x/tools/internal/testenv" ) @@ -17,11 +19,18 @@ // analyzer allows us to test the fillstruct code action using the analysistest // harness. (fillstruct used to be a gopls analyzer.) var analyzer = &analysis.Analyzer{ - Name: "fillstruct", - Doc: "test only", + Name: "fillstruct", + Doc: "test only", + Requires: []*analysis.Analyzer{inspect.Analyzer}, Run: func(pass *analysis.Pass) (any, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + for _, f := range pass.Files { - for _, diag := range fillstruct.Diagnose(f, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { + curFile, ok := inspect.Root().FindNode(f) + if !ok { + return nil, fmt.Errorf("can't find file %s", f.Name.Name) + } + for _, diag := range fillstruct.Diagnose(curFile, f.Pos(), f.End(), pass.Pkg, pass.TypesInfo) { pass.Report(diag) } } @@ -38,7 +47,6 @@ func TestIssue78553(t *testing.T) { testenv.NeedsGo1Point(t, 27) - t.Skip("Skipping as this feature is not yet implemented. Ref: go.dev/issues/78553") testdata := analysistest.TestData() analysistest.Run(t, testdata, analyzer, "issue78553") }
diff --git a/gopls/internal/analysis/fillstruct/testdata/src/a/a.go b/gopls/internal/analysis/fillstruct/testdata/src/a/a.go index 4a16a80..2c1377a 100644 --- a/gopls/internal/analysis/fillstruct/testdata/src/a/a.go +++ b/gopls/internal/analysis/fillstruct/testdata/src/a/a.go
@@ -5,7 +5,7 @@ package fillstruct import ( - data "b" + "b" "go/ast" "go/token" "unsafe" @@ -39,7 +39,9 @@ var _ = nestedStruct{} // want `nestedStruct literal has missing fields` -var _ = data.B{} // want `fillstruct.B literal has missing fields` +var _ = b.B{} // want `b.B literal has missing fields` + +var _ = b.B{ExportedInt: 0} type typedStruct struct { m map[string]int @@ -110,3 +112,23 @@ } var _ = unsafeStruct{} // want `unsafeStruct literal has missing fields` + +type inner struct { + bar int +} + +type outer struct { + inner inner + foo int +} + +var _ = outer{ // want `outer literal has missing fields` + inner: inner{} // want `inner literal has missing fields` +} + +type untagged struct { + A, B string +} + +// no diagnostic expected (contains untagged elements) +var _ = untagged{"a"} \ No newline at end of file
diff --git a/gopls/internal/analysis/fillstruct/testdata/src/b/b.go b/gopls/internal/analysis/fillstruct/testdata/src/b/b.go index a4b3946..f6a7bc3 100644 --- a/gopls/internal/analysis/fillstruct/testdata/src/b/b.go +++ b/gopls/internal/analysis/fillstruct/testdata/src/b/b.go
@@ -1,4 +1,4 @@ -package fillstruct +package b type B struct { ExportedInt int
diff --git a/gopls/internal/analysis/fillstruct/testdata/src/issue78553/issue78553.go b/gopls/internal/analysis/fillstruct/testdata/src/issue78553/issue78553.go index 62bbffb..d1eb5e4 100644 --- a/gopls/internal/analysis/fillstruct/testdata/src/issue78553/issue78553.go +++ b/gopls/internal/analysis/fillstruct/testdata/src/issue78553/issue78553.go
@@ -2,17 +2,31 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.27 + package issue78553 +type F struct { + F1 int + F2 int +} + type E struct { - A int + E1 int + E2 int + F } type T struct { E } -// Current behavior: fillstruct thinks E is missing because it only looks at top-level fields. -// Expected behavior for issue 78553: T{A: 1} should be considered fully populated (or at least A should be recognized). -// For now, we expect it to report missing fields because it doesn't support the new syntax. -var _ = T{A: 1} // want `T literal has missing fields` +var _ = T{E1: 0, E2: 0} // want `T literal has missing fields` + +var _ = T{F1: 0, F2: 0} // want `T literal has missing fields` + +var _ = T{E: &E{}} // want `E literal has missing fields` + +var _ = T{E1: 0, E2: 0, F: F{}} // want `F literal has missing fields` + +var _ = T{E1: 0, E2: 0, F1: 0, F2: 0}
diff --git a/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go index 24e8a93..f623cf5 100644 --- a/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go +++ b/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go
@@ -42,7 +42,7 @@ var tests = []struct { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p string }{ - {}, // want "anonymous struct{ a: string, b: string, c: string, ... } literal has missing fields" + {}, // want "anonymous struct{ a: string; b: string; ... } literal has missing fields" } for _, test := range tests { _ = test
diff --git a/gopls/internal/analysis/yield/yield.go b/gopls/internal/analysis/yield/yield.go index 8d1391e..90300e5 100644 --- a/gopls/internal/analysis/yield/yield.go +++ b/gopls/internal/analysis/yield/yield.go
@@ -461,7 +461,7 @@ // -- SSA CFG as graph.Graph -- -// fnGraph adapts an [ssa.Function] to to the [graph.Graph] interface +// fnGraph adapts an [ssa.Function] to the [graph.Graph] interface // required by the flow analysis framework. // Nodes are labelled by their block indices and connected by the // successor relation.
diff --git a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go index ad9ccc8..f0270be 100644 --- a/gopls/internal/cache/analysis.go +++ b/gopls/internal/cache/analysis.go
@@ -584,10 +584,8 @@ // inFlightAnalyses) and must be treated as immutable; the caller // copies its fields onto the analysisNode rather than retaining it. const cacheKind = "analysis" - if summary, err := filecache.Get(cacheKind, key, analyzeSummaryCodec.Decode); err == nil { + if summary, ok := filecache.GetOrFatal(cacheKind, key, analyzeSummaryCodec.Decode); ok { return summary, nil // cache hit - } else if err != filecache.ErrNotFound { - return nil, bug.Errorf("internal error reading shared cache: %v", err) } // Cache miss: do the work.
diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index a502b24..4bca59f 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go
@@ -322,15 +322,11 @@ return types.Unsafe, nil } - data, err := filecache.Get(exportDataKind, ph.key, filecache.Bytes) - if err == filecache.ErrNotFound { - // No cached export data: type-check as fast as possible. - return b.checkPackageForImport(ctx, ph) + if data, ok := filecache.GetOrFatal(exportDataKind, ph.key, filecache.Bytes); ok { + return b.importPackage(ctx, ph.mp, data) } - if err != nil { - return nil, fmt.Errorf("failed to read cache data for %s: %v", ph.mp.ID, err) - } - return b.importPackage(ctx, ph.mp, data) + // No cached export data (or hit error): type-check as fast as possible. + return b.checkPackageForImport(ctx, ph) }) } @@ -1416,11 +1412,8 @@ // a cache miss. func (s *Snapshot) typerefData(ctx context.Context, id PackageID, imports map[ImportPath]*metadata.Package, cgfs []file.Handle) ([]byte, error) { key := typerefsKey(id, imports, cgfs) - if data, err := filecache.Get(typerefsKind, key, filecache.Bytes); err == nil { + if data, ok := filecache.GetOrFatal(typerefsKind, key, filecache.Bytes); ok { return data, nil - } else if err != filecache.ErrNotFound { - bug.Reportf("internal error reading typerefs data: %v", err) - // Unexpected error: treat as cache miss, and fall through. } pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full&^parser.ParseComments, true, cgfs...)
diff --git a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go index ac905fa..da83261 100644 --- a/gopls/internal/cache/load.go +++ b/gopls/internal/cache/load.go
@@ -28,7 +28,6 @@ "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/packagesinternal" "golang.org/x/tools/internal/typesinternal" - "golang.org/x/tools/internal/xcontext" ) var loadID uint64 // atomic identifier for loads @@ -757,7 +756,7 @@ // The provided context is used for reading snapshot files, which can only // fail due to context cancellation. Don't let this happen as it could lead // to inconsistent results. - ctx = xcontext.Detach(ctx) + ctx = context.WithoutCancel(ctx) workspacePackages := make(map[PackageID]PackagePath) for _, mp := range meta.Packages { if !isWorkspacePackageLocked(ctx, s, meta, mp) {
diff --git a/gopls/internal/cache/methodsets/methodsets.go b/gopls/internal/cache/methodsets/methodsets.go index 300c184..5936bd1 100644 --- a/gopls/internal/cache/methodsets/methodsets.go +++ b/gopls/internal/cache/methodsets/methodsets.go
@@ -392,9 +392,15 @@ var mask uint64 tricky := false var buf []byte - methods := make([]*gobMethod, mset.Len()) + // Preallocate capacity, but grow by append so that skipped + // (generic) methods leave no nil holes in the slice. + methods := make([]*gobMethod, 0, mset.Len()) for i := 0; i < mset.Len(); i++ { m := mset.At(i).Obj().(*types.Func) + // Generic methods do not participate in interface satisfaction. + if m.Signature().TypeParams().Len() > 0 { + continue + } id := m.Id() fp, isTricky := fingerprint.Encode(m.Signature()) if isTricky { @@ -402,9 +408,10 @@ } buf = append(append(buf[:0], id...), fp...) sum := crc32.ChecksumIEEE(buf) - methods[i] = &gobMethod{ID: id, Fingerprint: fp, Sum: sum, Tricky: isTricky} + gm := &gobMethod{ID: id, Fingerprint: fp, Sum: sum, Tricky: isTricky} + methods = append(methods, gm) if setIndexInfo != nil { - setIndexInfo(methods[i], m) // set Position, PkgPath, ObjectPath + setIndexInfo(gm, m) // set Position, PkgPath, ObjectPath } mask |= 1 << uint64(((sum>>24)^(sum>>16)^(sum>>8)^sum)&0x3f) }
diff --git a/gopls/internal/cache/os_windows.go b/gopls/internal/cache/os_windows.go index 3537451..47acb8f 100644 --- a/gopls/internal/cache/os_windows.go +++ b/gopls/internal/cache/os_windows.go
@@ -47,7 +47,7 @@ } longstr := syscall.UTF16ToString(long) - // Check that the the path -> short -> long roundtrip was idempotent. + // Check that the path -> short -> long roundtrip was idempotent. isRoot := func(p string) bool { return p[len(p)-1] == filepath.Separator }
diff --git a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go index 97189da..c01d4f2 100644 --- a/gopls/internal/cache/session.go +++ b/gopls/internal/cache/session.go
@@ -23,7 +23,6 @@ "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/memoize" "golang.org/x/tools/gopls/internal/util/persistent" @@ -32,7 +31,6 @@ "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" - "golang.org/x/tools/internal/xcontext" ) // NewSession creates a new gopls session with the given cache. @@ -161,7 +159,7 @@ // We want a true background context and not a detached context here // the spans need to be unrelated and no tag values should pollute it. - baseCtx := event.Detach(xcontext.Detach(ctx)) + baseCtx := event.Detach(context.WithoutCancel(ctx)) backgroundCtx, cancel := context.WithCancel(baseCtx) // Compute a skip function to use for module cache scanning. @@ -245,14 +243,7 @@ fs: s.overlayFS, viewDefinition: def, importsState: newImportsState(backgroundCtx, s.cache.modCache, pe), - } - - // Keep this in sync with golang.computeImportEdits. - // - // TODO(rfindley): encapsulate the imports state logic so that the handling - // for Options.ImportsSource is in a single location. - if def.folder.Options.ImportsSource == settings.ImportsSourceGopls { - v.modcacheState = newModcacheState(def.folder.Env.GOMODCACHE) + modcacheState: newModcacheState(def.folder.Env.GOMODCACHE), } s.snapshotWG.Add(1) @@ -297,7 +288,7 @@ ) // Initialize the view without blocking. - initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx)) + initCtx, initCancel := context.WithCancel(context.WithoutCancel(ctx)) v.cancelInitialWorkspaceLoad = initCancel snapshot := v.snapshot @@ -1093,7 +1084,7 @@ } func mustReadFile(ctx context.Context, fs file.Source, uri protocol.DocumentURI) file.Handle { - ctx = xcontext.Detach(ctx) + ctx = context.WithoutCancel(ctx) fh, err := fs.ReadFile(ctx, uri) if err != nil { // ReadFile cannot fail with an uncancellable context.
diff --git a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go index 8068e5c..a66133d 100644 --- a/gopls/internal/cache/snapshot.go +++ b/gopls/internal/cache/snapshot.go
@@ -545,13 +545,10 @@ } } pre := func(_ int, ph *packageHandle) bool { - diags, err := filecache.Get(diagnosticsKind, ph.key, decodeDiagnostics) - if err == nil { // hit + if diags, ok := filecache.GetOrFatal(diagnosticsKind, ph.key, decodeDiagnostics); ok { collect(ph.loadDiagnostics) collect(diags) return false - } else if err != filecache.ErrNotFound { - event.Error(ctx, "reading diagnostics from filecache", err) } return true } @@ -572,12 +569,9 @@ indexes := make([]xrefIndex, len(ids)) pre := func(i int, ph *packageHandle) bool { - idx, err := filecache.Get(xrefsKind, ph.key, xrefs.Decode) - if err == nil { // hit + if idx, ok := filecache.GetOrFatal(xrefsKind, ph.key, xrefs.Decode); ok { indexes[i] = xrefIndex{mp: ph.mp, idx: idx} return false - } else if err != filecache.ErrNotFound { - event.Error(ctx, "reading xrefs from filecache", err) } return true } @@ -608,14 +602,11 @@ indexes := make([]*methodsets.Index, len(ids)) pre := func(i int, ph *packageHandle) bool { pkgPath := ph.mp.PkgPath // capture for decode closure - idx, err := filecache.Get(methodSetsKind, ph.key, func(data []byte) *methodsets.Index { + if idx, ok := filecache.GetOrFatal(methodSetsKind, ph.key, func(data []byte) *methodsets.Index { return methodsets.Decode(pkgPath, data) - }) - if err == nil { // hit + }); ok { indexes[i] = idx return false - } else if err != filecache.ErrNotFound { - event.Error(ctx, "reading methodsets from filecache", err) } return true } @@ -636,12 +627,9 @@ indexes := make([]*testfuncs.Index, len(ids)) pre := func(i int, ph *packageHandle) bool { - idx, err := filecache.Get(testsKind, ph.key, testfuncs.Decode) - if err == nil { // hit + if idx, ok := filecache.GetOrFatal(testsKind, ph.key, testfuncs.Decode); ok { indexes[i] = idx return false - } else if err != filecache.ErrNotFound { - event.Error(ctx, "reading tests from filecache", err) } return true } @@ -981,7 +969,7 @@ defer s.mu.Unlock() meta := make([]*metadata.Package, 0, s.workspacePackages.Len()) - for id := range s.workspacePackages.All() { + for id := range s.workspacePackages.Keys() { meta = append(meta, s.meta.Packages[id]) } return meta, nil
diff --git a/gopls/internal/cache/source.go b/gopls/internal/cache/source.go index 807f316..7db1384 100644 --- a/gopls/internal/cache/source.go +++ b/gopls/internal/cache/source.go
@@ -6,7 +6,6 @@ import ( "context" - "log" "maps" "slices" "strings" @@ -21,23 +20,21 @@ // goplsSource is an imports.Source that provides import information using // gopls and the module cache index. type goplsSource struct { - snapshot *Snapshot - envSource *imports.ProcessEnvSource + snapshot *Snapshot // set by each invocation of ResolveReferences ctx context.Context } -func (s *Snapshot) NewGoplsSource(is *imports.ProcessEnvSource) *goplsSource { +func (s *Snapshot) NewGoplsSource() *goplsSource { return &goplsSource{ - snapshot: s, - envSource: is, + snapshot: s, } } func (s *goplsSource) LoadPackageNames(ctx context.Context, srcDir string, paths []imports.ImportPath) (map[imports.ImportPath]imports.PackageName, error) { - // TODO: use metadata graph. Aside from debugging, this is the only used of envSource - return s.envSource.LoadPackageNames(ctx, srcDir, paths) + // The goplsSource does not need to do this + return nil, nil } type result struct { @@ -84,55 +81,6 @@ fromWS = append(fromWS, s.bestCache(k, v)) } } - const debug = false - if debug { // debugging. - // what does the old one find? - old, err := s.envSource.ResolveReferences(ctx, filename, missing) - if err != nil { - log.Fatal(err) - } - log.Printf("fromCache:%d %s", len(fromCache), filename) - for i, c := range fromCache { - log.Printf("cans%d %#v %#v %v", i, c.res.Import, c.res.Package, c.deprecated) - } - for k, v := range missing { - for x := range v { - log.Printf("missing %s.%s", k, x) - } - } - for k, v := range needed { - for x := range v { - log.Printf("needed %s.%s", k, x) - } - } - - dbgpr := func(hdr string, v []*imports.Result) { - for i := range v { - log.Printf("%s%d %+v %+v", hdr, i, v[i].Import, v[i].Package) - } - } - - dbgpr("fromWS", fromWS) - dbgpr("old", old) - for k, v := range s.snapshot.workspacePackages.All() { - log.Printf("workspacePackages[%s]=%s", k, v) - } - // anything in ans with >1 matches? - seen := make(map[string]int) - for _, a := range fromWS { - seen[a.Package.Name]++ - } - for k, v := range seen { - if v > 1 { - log.Printf("saw %d %s", v, k) - for i, x := range fromWS { - if x.Package.Name == k { - log.Printf("%d: %+v %+v", i, x.Package, x.Import) - } - } - } - } - } return fromWS, nil }
diff --git a/gopls/internal/cache/symbols.go b/gopls/internal/cache/symbols.go index 85ef01b..142d38c 100644 --- a/gopls/internal/cache/symbols.go +++ b/gopls/internal/cache/symbols.go
@@ -46,11 +46,9 @@ return err } - if pkg, err := filecache.Get(symbolsKind, key, symbols.Decode); err == nil { + if pkg, ok := filecache.GetOrFatal(symbolsKind, key, symbols.Decode); ok { res[i] = pkg return nil - } else if err != filecache.ErrNotFound { - bug.Reportf("internal error reading symbol data: %v", err) } pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full&^parser.ParseComments, false, fhs...)
diff --git a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go index 4725a9f..6bfc7e1 100644 --- a/gopls/internal/cache/view.go +++ b/gopls/internal/cache/view.go
@@ -37,7 +37,6 @@ "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/modindex" - "golang.org/x/tools/internal/xcontext" ) // A Folder represents an LSP workspace folder, together with its per-folder @@ -371,6 +370,9 @@ // ModcacheIndex returns the module cache index func (v *View) ModcacheIndex() (*modindex.Index, error) { + if v.modcacheState == nil { + return nil, fmt.Errorf("view %q has no module cache", v.id) + } return v.modcacheState.getIndex() } @@ -782,7 +784,7 @@ // s.viewMu must be held while calling this method. func (s *Session) invalidateViewLocked(ctx context.Context, v *View, changed StateChange) (*Snapshot, func(), bool) { // Detach the context so that content invalidation cannot be canceled. - ctx = xcontext.Detach(ctx) + ctx = context.WithoutCancel(ctx) // This should be the only time we hold the view's snapshot lock for any period of time. v.snapshotMu.Lock()
diff --git a/gopls/internal/cache/xrefs/xrefs.go b/gopls/internal/cache/xrefs/xrefs.go index 49ae872..bc15c04 100644 --- a/gopls/internal/cache/xrefs/xrefs.go +++ b/gopls/internal/cache/xrefs/xrefs.go
@@ -44,8 +44,11 @@ objectpathFor := new(objectpath.Encoder).For for fileIndex, pgf := range files { - for cur := range pgf.Cursor().Preorder((*ast.Ident)(nil), (*ast.ImportSpec)(nil)) { - switch n := cur.Node().(type) { + // Avoid pgf.Cursor() here to prevent materialization + // of an Inspector during workspace reindexing, which + // increases peak and retained memory usage. + for n := range ast.Preorder(pgf.File) { + switch n := n.(type) { case *ast.Ident: // Report a reference for each identifier that // uses a symbol exported from another package.
diff --git a/gopls/internal/cmd/call_hierarchy.go b/gopls/internal/cmd/call_hierarchy.go index 42de3c7..890b21f 100644 --- a/gopls/internal/cmd/call_hierarchy.go +++ b/gopls/internal/cmd/call_hierarchy.go
@@ -11,7 +11,7 @@ "strings" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // callHierarchy implements the callHierarchy verb for gopls.
diff --git a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go index 360ba43..314e20e 100644 --- a/gopls/internal/cmd/cmd.go +++ b/gopls/internal/cmd/cmd.go
@@ -30,12 +30,12 @@ "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" - bugpkg "golang.org/x/tools/gopls/internal/util/bug" + "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/tool" ) // Application is the main application as passed to tool.Main @@ -132,7 +132,7 @@ For documentation of all its features, see: - https://github.com/golang/tools/blob/master/gopls/doc/features + https://go.dev/gopls/features Usage: gopls help [<subject>] @@ -266,7 +266,6 @@ return []tool.Application{ &app.Serve, &version{app: app}, - &bug{app: app}, &help{app: app}, &apiJSON{app: app}, &licenses{app: app}, @@ -287,15 +286,13 @@ &codelens{app: app}, &definition{app: app}, &execute{app: app}, - &fix{app: app}, // (non-functional) &foldingRanges{app: app}, &format{app: app}, &headlessMCP{app: app}, &highlight{app: app}, &implementation{app: app}, &imports{app: app}, - newRemote(app, ""), - newRemote(app, "inspect"), + newRemote(app), &links{app: app}, &prepareRename{app: app}, &references{app: app}, @@ -906,7 +903,7 @@ // requires querying the file system, and we don't want // to do that. if !strings.EqualFold(f.mapper.URI.Base(), s.URI().Base()) { - return protocol.Range{}, bugpkg.Errorf("mapper is for file %q instead of %q", f.mapper.URI, s.URI()) + return protocol.Range{}, bug.Errorf("mapper is for file %q instead of %q", f.mapper.URI, s.URI()) } start, err := pointPosition(f.mapper, s.Start()) if err != nil { @@ -929,17 +926,3 @@ } return protocol.Position{}, fmt.Errorf("point has neither offset nor line/column") } - -// TODO(adonovan): delete in 2025. -type fix struct{ app *Application } - -func (*fix) Name() string { return "fix" } -func (cmd *fix) Parent() string { return cmd.app.Name() } -func (*fix) Usage() string { return "" } -func (*fix) ShortHelp() string { return "apply suggested fixes (obsolete)" } -func (*fix) DetailedHelp(flags *flag.FlagSet) { - fmt.Fprintf(flags.Output(), `No longer supported; use "gopls codeaction" instead.`) -} -func (*fix) Run(ctx context.Context, args ...string) error { - return tool.CommandLineErrorf(`no longer supported; use "gopls codeaction" instead`) -}
diff --git a/gopls/internal/cmd/codeaction.go b/gopls/internal/cmd/codeaction.go index 0f97a0e..974b499 100644 --- a/gopls/internal/cmd/codeaction.go +++ b/gopls/internal/cmd/codeaction.go
@@ -13,7 +13,7 @@ "strings" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // codeaction implements the codeaction verb for gopls. @@ -41,7 +41,7 @@ - changing the state of the server; or - requesting that the client open a document. -The -kind and and -title flags filter the list of actions. +The -kind and -title flags filter the list of actions. The -kind flag specifies a comma-separated list of LSP CodeAction kinds. Only actions of these kinds will be requested from the server.
diff --git a/gopls/internal/cmd/codelens.go b/gopls/internal/cmd/codelens.go index 2b91107..1d32493 100644 --- a/gopls/internal/cmd/codelens.go +++ b/gopls/internal/cmd/codelens.go
@@ -11,7 +11,7 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // codelens implements the codelens verb for gopls.
diff --git a/gopls/internal/cmd/definition.go b/gopls/internal/cmd/definition.go index 76a5540..26dd86f 100644 --- a/gopls/internal/cmd/definition.go +++ b/gopls/internal/cmd/definition.go
@@ -14,7 +14,7 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // A Definition is the result of a 'definition' query.
diff --git a/gopls/internal/cmd/execute.go b/gopls/internal/cmd/execute.go index dce259f..0c40e59 100644 --- a/gopls/internal/cmd/execute.go +++ b/gopls/internal/cmd/execute.go
@@ -10,11 +10,14 @@ "flag" "fmt" "log" + "os" "slices" + "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/internal/tool" + "golang.org/x/tools/gopls/internal/tool" + "golang.org/x/tools/gopls/internal/util/bug" ) // execute implements the LSP ExecuteCommand verb for gopls. @@ -49,6 +52,15 @@ } func (e *execute) Run(ctx context.Context, args ...string) error { + // This undocumented environment variable allows + // the cmd integration test (and maintainers) to + // trigger a call to bug.Report. + if msg := os.Getenv("TEST_GOPLS_BUG"); msg != "" { + filecache.Start() // register bug handler + bug.Report(msg) + return nil + } + if len(args) == 0 { return tool.CommandLineErrorf("execute requires a command name") }
diff --git a/gopls/internal/cmd/folding_range.go b/gopls/internal/cmd/folding_range.go index 6634974..e2c2f4b 100644 --- a/gopls/internal/cmd/folding_range.go +++ b/gopls/internal/cmd/folding_range.go
@@ -10,7 +10,7 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // foldingRanges implements the folding_ranges verb for gopls
diff --git a/gopls/internal/cmd/help_test.go b/gopls/internal/cmd/help_test.go index 7b90b3e..43f9daf 100644 --- a/gopls/internal/cmd/help_test.go +++ b/gopls/internal/cmd/help_test.go
@@ -13,41 +13,40 @@ //go:generate go test -run Help -update-help-files import ( - "bytes" - "context" "flag" "os" "path/filepath" + "strings" "testing" "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/cmd" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/tool" ) var updateHelpFiles = flag.Bool("update-help-files", false, "Write out the help files instead of checking them") -const appName = "gopls" - func TestHelpFiles(t *testing.T) { testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. + t.Parallel() app := cmd.New() - ctx := context.Background() - for _, page := range append(app.Commands(), app) { - t.Run(page.Name(), func(t *testing.T) { - var buf bytes.Buffer - s := flag.NewFlagSet(page.Name(), flag.ContinueOnError) - s.SetOutput(&buf) - tool.Run(ctx, s, page, []string{"-h"}) // ignore error - name := page.Name() - if name == appName { + tree := writeTree(t, "") + for _, cmd := range append(app.Commands(), app) { + name := cmd.Name() + 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 { + args = args[1:] name = "usage" } + res := gopls(t, tree, args...) + res.checkExit(true) // -h should result in exit 0 + got := res.stderr helpFile := filepath.Join("usage", name+".hlp") - got := buf.Bytes() if *updateHelpFiles { - if err := os.WriteFile(helpFile, got, 0666); err != nil { + if err := os.WriteFile(helpFile, []byte(got), 0666); err != nil { t.Errorf("Failed writing %v: %v", helpFile, err) } return @@ -56,26 +55,22 @@ if err != nil { t.Fatalf("Missing help file %q", helpFile) } - if diff := cmp.Diff(string(want), string(got)); diff != "" { + if diff := cmp.Diff(string(want), got); diff != "" { t.Errorf("Help file %q did not match, run with -update-help-files to fix (-want +got)\n%s", helpFile, diff) } }) } } - func TestVerboseHelp(t *testing.T) { testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. - app := cmd.New() - ctx := context.Background() - var buf bytes.Buffer - s := flag.NewFlagSet(appName, flag.ContinueOnError) - s.SetOutput(&buf) - tool.Run(ctx, s, app, []string{"-v", "-h"}) // ignore error - got := buf.Bytes() - + t.Parallel() + tree := writeTree(t, "") + res := gopls(t, tree, "-v", "-h") + res.checkExit(true) // -h should result in exit 0 + got := res.stderr helpFile := filepath.Join("usage", "usage-v.hlp") if *updateHelpFiles { - if err := os.WriteFile(helpFile, got, 0666); err != nil { + if err := os.WriteFile(helpFile, []byte(got), 0666); err != nil { t.Errorf("Failed writing %v: %v", helpFile, err) } return @@ -84,7 +79,71 @@ if err != nil { t.Fatalf("Missing help file %q", helpFile) } - if diff := cmp.Diff(string(want), string(got)); diff != "" { + if diff := cmp.Diff(string(want), got); diff != "" { t.Errorf("Help file %q did not match, run with -update-help-files to fix (-want +got)\n%s", helpFile, diff) } } + +// TestHelpTree tests "gopls help" on a number +// of levels of the commmand tree. +func TestHelpTree(t *testing.T) { + t.Parallel() + + tree := writeTree(t, ``) + + for _, test := range []struct { + args []string + wantSuccess bool + wantPatterns []string + }{ + // gopls help + { + args: []string{"help"}, + wantSuccess: true, + wantPatterns: []string{ + "gopls is a Go language server", + "https://go.dev/gopls/features", + "Usage:", + "Command:", + " links.*list links in a file", // command menu + }, + }, + // gopls help remote + { + args: []string{"help", "remote"}, + wantSuccess: true, + wantPatterns: []string{ + "interact with the gopls daemon", + "Usage:", + "Subcommand:", + " sessions.*print information about current gopls sessions", // subcommand menu + }, + }, + // gopls help remote sessions + { + args: []string{"help", "remote", "sessions"}, + wantSuccess: true, + wantPatterns: []string{ + "print information about current gopls sessions", + "Usage:", + "list sessions for the default daemon", + }, + }, + // gopls help remote nonesuch + { + args: []string{"help", "remote", "nonesuch"}, + wantPatterns: []string{ + "gopls: no such subcommand: remote nonesuch", + }, + }, + } { + 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) + } + }) + } +}
diff --git a/gopls/internal/cmd/highlight.go b/gopls/internal/cmd/highlight.go index 8cbb288..26c7fe6 100644 --- a/gopls/internal/cmd/highlight.go +++ b/gopls/internal/cmd/highlight.go
@@ -10,7 +10,7 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // highlight implements the highlight verb for gopls.
diff --git a/gopls/internal/cmd/implementation.go b/gopls/internal/cmd/implementation.go index 0c3b31f..0c845f4 100644 --- a/gopls/internal/cmd/implementation.go +++ b/gopls/internal/cmd/implementation.go
@@ -11,7 +11,7 @@ "sort" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // implementation implements the implementation verb for gopls
diff --git a/gopls/internal/cmd/imports.go b/gopls/internal/cmd/imports.go index 2bb793a..0b8f143 100644 --- a/gopls/internal/cmd/imports.go +++ b/gopls/internal/cmd/imports.go
@@ -10,7 +10,7 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // imports implements the import verb for gopls.
diff --git a/gopls/internal/cmd/info.go b/gopls/internal/cmd/info.go index 90baf11..b099046 100644 --- a/gopls/internal/cmd/info.go +++ b/gopls/internal/cmd/info.go
@@ -4,7 +4,7 @@ package cmd -// This file defines the help, bug, version, api-json, licenses commands. +// This file defines the help, version, api-json, licenses commands. import ( "bytes" @@ -12,18 +12,13 @@ "flag" "fmt" "io" - "net/url" "os" - "sort" "strings" "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/doc" - "golang.org/x/tools/gopls/internal/filecache" licensespkg "golang.org/x/tools/gopls/internal/licenses" - "golang.org/x/tools/gopls/internal/util/browser" - goplsbug "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // help implements the help command. @@ -102,118 +97,6 @@ return err } -// bug implements the bug command. -type bug struct { - app *Application -} - -func (b *bug) Name() string { return "bug" } -func (b *bug) Parent() string { return b.app.Name() } -func (b *bug) Usage() string { return "" } -func (b *bug) ShortHelp() string { return "report a bug in gopls" } -func (b *bug) DetailedHelp(f *flag.FlagSet) { - fmt.Fprint(f.Output(), ``) - printFlagDefaults(f) -} - -const goplsBugPrefix = "x/tools/gopls: <DESCRIBE THE PROBLEM>" -const goplsBugHeader = `ATTENTION: Please answer these questions BEFORE submitting your issue. Thanks! - -#### What did you do? -If possible, provide a recipe for reproducing the error. -A complete runnable program is good. -A link on play.golang.org is better. -A failing unit test is the best. - -#### What did you expect to see? - - -#### What did you see instead? - - -` - -// Run collects some basic information and then prepares an issue ready to -// be reported. -func (b *bug) Run(ctx context.Context, args ...string) error { - // This undocumented environment variable allows - // the cmd integration test (and maintainers) to - // trigger a call to bug.Report. - if msg := os.Getenv("TEST_GOPLS_BUG"); msg != "" { - filecache.Start() // register bug handler - goplsbug.Report(msg) - return nil - } - - // Enumerate bug reports, grouped and sorted. - _, reports := filecache.BugReports() - sort.Slice(reports, func(i, j int) bool { - x, y := reports[i], reports[i] - if x.Key != y.Key { - return x.Key < y.Key // ascending key order - } - return y.AtTime.Before(x.AtTime) // most recent first - }) - keyDenom := make(map[string]int) // key is "file:line" - for _, report := range reports { - keyDenom[report.Key]++ - } - - // Privacy: the content of 'public' will be posted to GitHub - // to populate an issue textarea. Even though the user must - // submit the form to share the information with the world, - // merely populating the form causes us to share the - // information with GitHub itself. - // - // For that reason, we cannot write private information to - // public, such as bug reports, which may quote source code. - public := &bytes.Buffer{} - fmt.Fprint(public, goplsBugHeader) - if len(reports) > 0 { - fmt.Fprintf(public, "#### Internal errors\n\n") - fmt.Fprintf(public, "Gopls detected %d internal errors, %d distinct:\n", - len(reports), len(keyDenom)) - for key, denom := range keyDenom { - fmt.Fprintf(public, "- %s (%d)\n", key, denom) - } - fmt.Fprintf(public, "\nPlease copy the full information printed by `gopls bug` here, if you are comfortable sharing it.\n\n") - } - debug.WriteVersionInfo(public, true, debug.Markdown) - body := public.String() - title := strings.Join(args, " ") - if !strings.HasPrefix(title, goplsBugPrefix) { - title = goplsBugPrefix + title - } - if !browser.Open("https://github.com/golang/go/issues/new?title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body)) { - fmt.Print("Please file a new issue at golang.org/issue/new using this template:\n\n") - fmt.Print(body) - } - - // Print bug reports to stdout (not GitHub). - keyNum := make(map[string]int) - for _, report := range reports { - fmt.Printf("-- %v -- \n", report.AtTime) - - // Append seq number (e.g. " (1/2)") for repeated keys. - var seq string - if denom := keyDenom[report.Key]; denom > 1 { - keyNum[report.Key]++ - seq = fmt.Sprintf(" (%d/%d)", keyNum[report.Key], denom) - } - - // Privacy: - // - File and Stack may contain the name of the user that built gopls. - // - Description may contain names of the user's packages/files/symbols. - fmt.Printf("%s:%d: %s%s\n\n", report.File, report.Line, report.Description, seq) - fmt.Printf("%s\n\n", report.Stack) - } - if len(reports) > 0 { - fmt.Printf("Please copy the above information into the GitHub issue, if you are comfortable sharing it.\n") - } - - return nil -} - type apiJSON struct { app *Application }
diff --git a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go index 754d8bf..170629c 100644 --- a/gopls/internal/cmd/integration_test.go +++ b/gopls/internal/cmd/integration_test.go
@@ -23,7 +23,7 @@ // - Subcommands that accept -write and -diff flags implement them // consistently; factor their tests. // - Add missing test for 'vulncheck' subcommand. -// - Add tests for client-only commands: serve, bug, help, api-json, licenses. +// - Add tests for client-only commands: serve, help, api-json, licenses. import ( "bytes" @@ -41,10 +41,10 @@ "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" - "golang.org/x/tools/internal/tool" "golang.org/x/tools/txtar" ) @@ -871,7 +871,7 @@ oops := fmt.Sprintf("oops-%d", rand.Int()) { env := []string{"TEST_GOPLS_BUG=" + oops} - res := goplsWithEnv(t, tree, env, "bug") + res := goplsWithEnv(t, tree, env, "execute") res.checkExit(true) } @@ -914,8 +914,8 @@ { got := fmt.Sprint(stats.BugReports) wants := []string{ - "cmd/info.go", // File containing call to bug.Report - oops, // Description + "cmd/execute.go", // File containing call to bug.Report + oops, // Description } for _, want := range wants { if !strings.Contains(got, want) {
diff --git a/gopls/internal/cmd/links.go b/gopls/internal/cmd/links.go index 933f025..3b08868 100644 --- a/gopls/internal/cmd/links.go +++ b/gopls/internal/cmd/links.go
@@ -12,7 +12,7 @@ "os" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // links implements the links verb for gopls.
diff --git a/gopls/internal/cmd/mcp.go b/gopls/internal/cmd/mcp.go index ac42901..a35f049 100644 --- a/gopls/internal/cmd/mcp.go +++ b/gopls/internal/cmd/mcp.go
@@ -164,7 +164,15 @@ } watchQueueMu.Lock() for _, r := range res.Roots { - watchQueue = append(watchQueue, protocol.DocumentURI(r.URI).Path()) + uri, err := protocol.ParseDocumentURI(r.URI) + if err != nil { + // Discard invalid URIs. + // Unlike LSP, MCP does not check URI validity during unmarshaling. + // Fixes go.dev/issue/74652, crash of May 18 2026. + log.Printf("mcp.ListRootsResult[*].Roots[*].URI contains invalid URI: %v", err) + continue + } + watchQueue = append(watchQueue, uri.Path()) } watchQueueMu.Unlock()
diff --git a/gopls/internal/cmd/prepare_rename.go b/gopls/internal/cmd/prepare_rename.go index cb4d035..72f42af 100644 --- a/gopls/internal/cmd/prepare_rename.go +++ b/gopls/internal/cmd/prepare_rename.go
@@ -11,7 +11,7 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // prepareRename implements the prepare_rename verb for gopls.
diff --git a/gopls/internal/cmd/references.go b/gopls/internal/cmd/references.go index 8fb2e72..a91925f 100644 --- a/gopls/internal/cmd/references.go +++ b/gopls/internal/cmd/references.go
@@ -11,7 +11,7 @@ "sort" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // references implements the references verb for gopls
diff --git a/gopls/internal/cmd/remote.go b/gopls/internal/cmd/remote.go index ae4aa55..c100164 100644 --- a/gopls/internal/cmd/remote.go +++ b/gopls/internal/cmd/remote.go
@@ -20,41 +20,26 @@ type remote struct { app *Application subcommands - - // For backward compatibility, allow aliasing this command (it was previously - // called 'inspect'). - // - // TODO(rFindley): delete this after allowing some transition time in case - // there were any users of 'inspect' (I suspect not). - alias string } -func newRemote(app *Application, alias string) *remote { +func newRemote(app *Application) *remote { return &remote{ app: app, subcommands: subcommands{ &listSessions{app: app}, &startDebugging{app: app}, }, - alias: alias, } } func (r *remote) Name() string { - if r.alias != "" { - return r.alias - } return "remote" } func (r *remote) Parent() string { return r.app.Name() } func (r *remote) ShortHelp() string { - short := "interact with the gopls daemon" - if r.alias != "" { - short += " (deprecated: use 'remote')" - } - return short + return "interact with the gopls daemon" } // listSessions is an inspect subcommand to list current sessions.
diff --git a/gopls/internal/cmd/rename.go b/gopls/internal/cmd/rename.go index 61f2ae4..1922f40 100644 --- a/gopls/internal/cmd/rename.go +++ b/gopls/internal/cmd/rename.go
@@ -10,7 +10,7 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // rename implements the rename verb for gopls.
diff --git a/gopls/internal/cmd/serve.go b/gopls/internal/cmd/serve.go index a56fc89..b87abda 100644 --- a/gopls/internal/cmd/serve.go +++ b/gopls/internal/cmd/serve.go
@@ -21,9 +21,9 @@ "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" - "golang.org/x/tools/internal/tool" ) // Serve is a struct that exposes the configurable parts of the LSP and MCP
diff --git a/gopls/internal/cmd/signature.go b/gopls/internal/cmd/signature.go index fd6d637..e1c5372 100644 --- a/gopls/internal/cmd/signature.go +++ b/gopls/internal/cmd/signature.go
@@ -10,7 +10,7 @@ "fmt" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // signature implements the signature verb for gopls
diff --git a/gopls/internal/cmd/stats.go b/gopls/internal/cmd/stats.go index 155686e..2fad301 100644 --- a/gopls/internal/cmd/stats.go +++ b/gopls/internal/cmd/stats.go
@@ -22,7 +22,7 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/settings" - bugpkg "golang.org/x/tools/gopls/internal/util/bug" + "golang.org/x/tools/gopls/internal/util/bug" versionpkg "golang.org/x/tools/gopls/internal/version" "golang.org/x/tools/internal/event" ) @@ -121,7 +121,7 @@ do("Gathering bug reports", func() error { stats.CacheDir, stats.BugReports = filecache.BugReports() if stats.BugReports == nil { - stats.BugReports = []bugpkg.Bug{} // non-nil for JSON + stats.BugReports = []bug.Bug{} // non-nil for JSON } return nil }) @@ -206,7 +206,7 @@ GOPACKAGESDRIVER string InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form CacheDir string - BugReports []bugpkg.Bug + BugReports []bug.Bug MemStats command.MemStatsResult `anon:"ok"` WorkspaceStats command.WorkspaceStatsResult `anon:"ok"` DirStats dirStats `anon:"ok"`
diff --git a/gopls/internal/cmd/subcommands.go b/gopls/internal/cmd/subcommands.go index e30c42b..47d577b 100644 --- a/gopls/internal/cmd/subcommands.go +++ b/gopls/internal/cmd/subcommands.go
@@ -10,7 +10,7 @@ "fmt" "text/tabwriter" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // subcommands is a helper that may be embedded for commands that delegate to
diff --git a/gopls/internal/cmd/symbols.go b/gopls/internal/cmd/symbols.go index b623463..4641b6a 100644 --- a/gopls/internal/cmd/symbols.go +++ b/gopls/internal/cmd/symbols.go
@@ -12,7 +12,7 @@ "sort" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // symbols implements the symbols verb for gopls
diff --git a/gopls/internal/cmd/usage/codeaction.hlp b/gopls/internal/cmd/usage/codeaction.hlp index d7bfe3e..6e6909c 100644 --- a/gopls/internal/cmd/usage/codeaction.hlp +++ b/gopls/internal/cmd/usage/codeaction.hlp
@@ -12,7 +12,7 @@ - changing the state of the server; or - requesting that the client open a document. -The -kind and and -title flags filter the list of actions. +The -kind and -title flags filter the list of actions. The -kind flag specifies a comma-separated list of LSP CodeAction kinds. Only actions of these kinds will be requested from the server.
diff --git a/gopls/internal/cmd/usage/fix.hlp b/gopls/internal/cmd/usage/fix.hlp deleted file mode 100644 index b681998..0000000 --- a/gopls/internal/cmd/usage/fix.hlp +++ /dev/null
@@ -1,5 +0,0 @@ -apply suggested fixes (obsolete) - -Usage: - gopls [flags] fix -No longer supported; use "gopls codeaction" instead. \ No newline at end of file
diff --git a/gopls/internal/cmd/usage/inspect.hlp b/gopls/internal/cmd/usage/inspect.hlp deleted file mode 100644 index 3d0a0f3..0000000 --- a/gopls/internal/cmd/usage/inspect.hlp +++ /dev/null
@@ -1,8 +0,0 @@ -interact with the gopls daemon (deprecated: use 'remote') - -Usage: - gopls [flags] inspect <subcommand> [arg]... - -Subcommand: - sessions print information about current gopls sessions - debug start the debug server
diff --git a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp index a1e497c..2cf77ba 100644 --- a/gopls/internal/cmd/usage/usage-v.hlp +++ b/gopls/internal/cmd/usage/usage-v.hlp
@@ -7,7 +7,7 @@ For documentation of all its features, see: - https://github.com/golang/tools/blob/master/gopls/doc/features + https://go.dev/gopls/features Usage: gopls help [<subject>] @@ -17,7 +17,6 @@ Main serve run a server for Go code using the Language Server Protocol version print the gopls version information - bug report a bug in gopls help print usage information for subcommands api-json print JSON describing gopls API licenses print licenses of included software @@ -29,7 +28,6 @@ codelens List or execute code lenses for a file definition show declaration of selected identifier execute Execute a gopls custom LSP command - fix apply suggested fixes (obsolete) folding_ranges display selected file's folding ranges format format the code according to the go standard mcp start the gopls MCP server in headless mode @@ -37,7 +35,6 @@ implementation display selected identifier's implementation imports updates import statements remote interact with the gopls daemon - inspect interact with the gopls daemon (deprecated: use 'remote') links list links in a file prepare_rename test validity of a rename operation at location references display selected identifier's references
diff --git a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp index a3dbbf2..c117ee8 100644 --- a/gopls/internal/cmd/usage/usage.hlp +++ b/gopls/internal/cmd/usage/usage.hlp
@@ -7,7 +7,7 @@ For documentation of all its features, see: - https://github.com/golang/tools/blob/master/gopls/doc/features + https://go.dev/gopls/features Usage: gopls help [<subject>] @@ -17,7 +17,6 @@ Main serve run a server for Go code using the Language Server Protocol version print the gopls version information - bug report a bug in gopls help print usage information for subcommands api-json print JSON describing gopls API licenses print licenses of included software @@ -29,7 +28,6 @@ codelens List or execute code lenses for a file definition show declaration of selected identifier execute Execute a gopls custom LSP command - fix apply suggested fixes (obsolete) folding_ranges display selected file's folding ranges format format the code according to the go standard mcp start the gopls MCP server in headless mode @@ -37,7 +35,6 @@ implementation display selected identifier's implementation imports updates import statements remote interact with the gopls daemon - inspect interact with the gopls daemon (deprecated: use 'remote') links list links in a file prepare_rename test validity of a rename operation at location references display selected identifier's references
diff --git a/gopls/internal/cmd/workspace_symbol.go b/gopls/internal/cmd/workspace_symbol.go index 38b961c..cd4ebc6 100644 --- a/gopls/internal/cmd/workspace_symbol.go +++ b/gopls/internal/cmd/workspace_symbol.go
@@ -12,7 +12,7 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/internal/tool" + "golang.org/x/tools/gopls/internal/tool" ) // workspaceSymbol implements the workspace_symbol verb for gopls.
diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 1d54cf2..4e61ede 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json
@@ -1462,7 +1462,7 @@ }, { "Name": "\"errorsas\"", - "Doc": "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analyzer reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.\nFor example:\n\n\tvar unwrappedErr net.DNSError\n\terrors.As(err, unwrappedErr) // should use \u0026unwrappedErr, DNSError.Error has a pointer reciever\n", + "Doc": "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analyzer reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.\nFor example:\n\n\tvar unwrappedErr net.DNSError\n\terrors.As(err, unwrappedErr) // should use \u0026unwrappedErr, DNSError.Error has a pointer receiver\n", "Default": "true", "Status": "" }, @@ -1473,8 +1473,8 @@ "Status": "" }, { - "Name": "\"errorsastype\"", - "Doc": "Reports misuse of errors.AsType[T] in if/else chains.\nFor example:\n\n\terr := f()\n\tif err, ok := errors.AsType[*FooErr](err); ok {\n\t useFoo(err)\n\t} else if err, ok := errors.AsType[*BarErr](err); ok {\n\t useBar(err)\n\t}\n\nIn this case, the second call to errors.AsType does not operate on the\noriginal error. Instead, its operand is the zero value of type *FooErr\nproduced by the first if statement; this is invariably a mistake.\n", + "Name": "\"errorsastypeshadow\"", + "Doc": "report shadowing of errors.AsType[T] in if/else chains\n\nFor example:\n\n\terr := f()\n\tif err, ok := errors.AsType[*FooErr](err); ok {\n\t useFoo(err)\n\t} else if err, ok := errors.AsType[*BarErr](err); ok {\n\t useBar(err)\n\t}\n\nIn this case, the second call to errors.AsType does not operate on the\noriginal error. Instead, its operand is the zero value of type *FooErr\nproduced by the first if statement; this is invariably a mistake.", "Default": "true", "Status": "" }, @@ -1534,7 +1534,7 @@ }, { "Name": "\"inline\"", - "Doc": "apply fixes based on 'go:fix inline' comment directives\n\nThe inline analyzer inlines functions and constants that are marked for inlining.\n\n## Functions\n\nGiven a function that is marked for inlining, like this one:\n\n\t//go:fix inline\n\tfunc Square(x int) int { return Pow(x, 2) }\n\nthis analyzer will recommend that calls to the function elsewhere, in the same\nor other packages, should be inlined.\n\nInlining can be used to move off of a deprecated function:\n\n\t// Deprecated: prefer Pow(x, 2).\n\t//go:fix inline\n\tfunc Square(x int) int { return Pow(x, 2) }\n\nIt can also be used to move off of an obsolete package,\nas when the import path has changed or a higher major version is available:\n\n\tpackage pkg\n\n\timport pkg2 \"pkg/v2\"\n\n\t//go:fix inline\n\tfunc F() { pkg2.F(nil) }\n\nReplacing a call pkg.F() by pkg2.F(nil) can have no effect on the program,\nso this mechanism provides a low-risk way to update large numbers of calls.\nWe recommend, where possible, expressing the old API in terms of the new one\nto enable automatic migration.\n\nThe inliner takes care to avoid behavior changes, even subtle ones,\nsuch as changes to the order in which argument expressions are\nevaluated. When it cannot safely eliminate all parameter variables,\nit may introduce a \"binding declaration\" of the form\n\n\tvar params = args\n\nto evaluate argument expressions in the correct order and bind them to\nparameter variables. Since the resulting code transformation may be\nstylistically suboptimal, such inlinings may be disabled by specifying\nthe -inline.allow_binding_decl=false flag to the analyzer driver.\n\n(In cases where it is not safe to \"reduce\" a call—that is, to replace\na call f(x) by the body of function f, suitably substituted—the\ninliner machinery is capable of replacing f by a function literal,\nfunc(){...}(). However, the inline analyzer discards all such\n\"literalizations\" unconditionally, again on grounds of style.)\n\nA call to a function F from its dedicated test (TestF) is not inlined,\nsince the purpose of the test is to exercise F itself, even when\nit's a deprecated function to which other calls should be inlined.\nThis is not true for type aliases; see https://go.dev/issue/79271.\nSee further discussion in https://go.dev/issue/79272.\n\n## Constants\n\nGiven a constant that is marked for inlining, like this one:\n\n\t//go:fix inline\n\tconst Ptr = Pointer\n\nthis analyzer will recommend that uses of Ptr should be replaced with Pointer.\n\nAs with functions, inlining can be used to replace deprecated constants and\nconstants in obsolete packages.\n\nA constant definition can be marked for inlining only if it refers to another\nnamed constant.\n\nThe \"//go:fix inline\" comment must appear before a single const declaration on its own,\nas above; before a const declaration that is part of a group, as in this case:\n\n\tconst (\n\t C = 1\n\t //go:fix inline\n\t Ptr = Pointer\n\t)\n\nor before a group, applying to every constant in the group:\n\n\t//go:fix inline\n\tconst (\n\t\tPtr = Pointer\n\t Val = Value\n\t)\n\nThe proposal https://go.dev/issue/32816 introduces the \"//go:fix inline\" directives.\n\nYou can use this command to apply inline fixes en masse:\n\n\t$ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./...", + "Doc": "apply fixes based on 'go:fix inline' comment directives\n\nThe inline analyzer inlines functions, constants, and type aliases\nthat are marked for inlining.\n\nUse this command to apply (just) inline fixes en masse:\n\n\t$ go fix -inline ./...\n\n## Functions\n\nGiven a function that is marked for inlining, like this one:\n\n\t//go:fix inline\n\tfunc Square(x int) int { return Pow(x, 2) }\n\nthis analyzer will recommend that calls to the function elsewhere, in the same\nor other packages, should be inlined.\n\nInlining can be used to move off of a deprecated function:\n\n\t// Deprecated: prefer Pow(x, 2).\n\t//go:fix inline\n\tfunc Square(x int) int { return Pow(x, 2) }\n\nIt can also be used to move off of an obsolete package,\nas when the import path has changed or a higher major version is available:\n\n\tpackage pkg\n\n\timport pkg2 \"pkg/v2\"\n\n\t//go:fix inline\n\tfunc F() { pkg2.F(nil) }\n\nReplacing a call pkg.F() by pkg2.F(nil) can have no effect on the program,\nso this mechanism provides a low-risk way to update large numbers of calls.\nWe recommend, where possible, expressing the old API in terms of the new one\nto enable automatic migration.\n\nThe inliner takes care to avoid behavior changes, even subtle ones,\nsuch as changes to the order in which argument expressions are\nevaluated. When it cannot safely eliminate all parameter variables,\nit may introduce a \"binding declaration\" of the form\n\n\tvar params = args\n\nto evaluate argument expressions in the correct order and bind them to\nparameter variables. Since the resulting code transformation may be\nstylistically suboptimal, such inlinings may be disabled by specifying\nthe -inline.allow_binding_decl=false flag to the analyzer driver.\n\n(In cases where it is not safe to \"reduce\" a call—that is, to replace\na call f(x) by the body of function f, suitably substituted—the\ninliner machinery is capable of replacing f by a function literal,\nfunc(){...}(). However, the inline analyzer discards all such\n\"literalizations\" unconditionally, again on grounds of style.)\n\n## Constants\n\nGiven a constant that is marked for inlining, like this one:\n\n\t//go:fix inline\n\tconst Ptr = Pointer\n\nthis analyzer will recommend that uses of Ptr should be replaced with Pointer.\n\nAs with functions, inlining can be used to replace deprecated constants and\nconstants in obsolete packages.\n\nA constant definition can be marked for inlining only if it refers to another\nnamed constant.\n\nThe \"//go:fix inline\" comment must appear before a single const declaration on its own,\nas above; before a const declaration that is part of a group, as in this case:\n\n\tconst (\n\t C = 1\n\t //go:fix inline\n\t Ptr = Pointer\n\t)\n\nor before a group, applying to every constant in the group:\n\n\t//go:fix inline\n\tconst (\n\t\tPtr = Pointer\n\t\tVal = Value\n\t)\n\n## Type aliases\n\nSimilar to named constants, a type alias can also be marked for inlining:\n\n\t//go:fix inline\n\ttype A = newpkg.A\n\nThe analyzer will replace all references to the annotated type\n(A) by the type on the right-hand side of the declaration (newpkg.A).\n\n## Tests\n\nA use of a function, named constant, or type alias X from its\ndedicated test (TestX), is not inlined, since the purpose of the test\nis to exercise X itself, even if it is deprecated and other uses of it\nshould be inlined.\nThis applies to benchmarks and examples too, and follows the usual\nconventions of test function naming.\n\nSimilarly, if the symbol X is declared in a file named foo.go, any use\nof it within a file named foo_test.go will also not be inlined.", "Default": "true", "Status": "" }, @@ -1677,6 +1677,12 @@ "Status": "" }, { + "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) { ... }", + "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", @@ -1707,6 +1713,12 @@ "Status": "" }, { + "Name": "\"sqlrowserr\"", + "Doc": "sqlrowserr: report failure to check sql.Rows.Err\n\nThis analyzer reports uses of sql.Rows in which the result of a query\nsuch as db.Query() is assigned to a local variable that is then used\nin a loop that calls Rows.Next, but lacks a final check of Rows.Err.\nThis causes row iteration errors to be discarded.\n\nFor example:\n\n\trows, err := db.Query(\"select ...\") // error: \"sql.Rows rows is used in Next loop without final check of rows.Err()\"\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close() // ignore error\n\tfor rows.Next() {\n\t\tvar x int\n\t\tif err := rows.Scan(\u0026x); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuse(x)\n\t}\n\t/* ...no use of rows.Err()... */\n\nCorrect usage of sql.Rows demands both a call to Rows.Close to release\nresources and a call to Rows.Err to report iteration errors. It is\nnot critical to report resource cleanup errors, but it is crucial to\nreport iteration errors as they would otherwise be indistinguishable\nfrom a smaller result.\n\nTo avoid false positives, the analyzer is silent if the Rows is passed\ninto or out of the function or assigned somewhere other than a local\nvariable.\n\nIt is not this analyzer's goal to ensure proper handling of errors in\nall cases, but merely the simple mistakes where the user may have been\noblivious to the existence of the Rows.Err method.\n", + "Default": "true", + "Status": "" + }, + { "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.", "Default": "true", @@ -2029,49 +2041,49 @@ "Keys": [ { "Name": "\"assignVariableTypes\"", - "Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```\n", + "Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti« int», j« int» := 0, len(r)-1\n```\n", "Default": "false", "Status": "" }, { "Name": "\"compositeLiteralFields\"", - "Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```\n", + "Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\tPoint2D{«X: »1, «Y: »2}\n\n\tOuter{«Embedded.»Field: 0}\n```\n", "Default": "false", "Status": "" }, { "Name": "\"compositeLiteralTypes\"", - "Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n", + "Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t«struct{ in string; want string }»{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n", "Default": "false", "Status": "" }, { "Name": "\"constantValues\"", - "Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```\n", + "Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota« = 0»\n\t\tKindPrint« = 1»\n\t\tKindPrintf« = 2»\n\t\tKindErrorf« = 3»\n\t)\n```\n", "Default": "false", "Status": "" }, { "Name": "\"functionTypeParameters\"", - "Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```\n", + "Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo«[int, string]»(1, \"hello\")\n```\n", "Default": "false", "Status": "" }, { "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\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", "Default": "false", "Status": "" }, { "Name": "\"parameterNames\"", - "Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n", + "Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(« str: » \"123\", « radix: » 8)\n```\n", "Default": "false", "Status": "" }, { "Name": "\"rangeVariableTypes\"", - "Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n", + "Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k« int», v« string» := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n", "Default": "false", "Status": "" } @@ -2149,7 +2161,7 @@ { "Name": "semanticTokens", "Type": "bool", - "Doc": "semanticTokens determines whether gopls will return a\nSemanticTokensProvider at initialization, or respond\nto request for semantic tokens.\n", + "Doc": "semanticTokens determines whether gopls will return a\nSemanticTokensProvider at initialization, or respond\nto requests for semantic tokens.\n\nThis setting being `false` won't necessary disable the client's calls\nfor semantic tokens. If you want that, it would need to be configured in\nthe client. For example, in VSCode, this would disable all Go semantic\ntoken calls to the LSP server:\n\n```json5\n\"[go]\": {\n \"editor.semanticHighlighting.enabled\": false,\n}\n```\n", "EnumKeys": { "ValueType": "", "Keys": null @@ -2245,6 +2257,20 @@ "DeprecationMessage": "" }, { + "Name": "moveType", + "Type": "bool", + "Doc": "moveType enables producing Move Type 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", @@ -2273,6 +2299,36 @@ "DeprecationMessage": "" }, { + "Name": "fileWatcher", + "Type": "enum", + "Doc": "fileWatcher specifies the server-side file watching strategy used by gopls.\n\nBy default, this is set to \"off\", meaning gopls relies exclusively on the\nlanguage client (e.g., the editor) to send file change notifications.\n\nAvailable options:\n - \"off\" : Client-driven watching (default)\n - \"fsnotify\" : OS-level event notifications\n - \"poll\" : Periodic directory scanning\n", + "EnumKeys": { + "ValueType": "", + "Keys": null + }, + "EnumValues": [ + { + "Value": "\"fsnotify\"", + "Doc": "", + "Status": "" + }, + { + "Value": "\"off\"", + "Doc": "", + "Status": "" + }, + { + "Value": "\"poll\"", + "Doc": "", + "Status": "" + } + ], + "Default": "\"off\"", + "Status": "experimental", + "Hierarchy": "", + "DeprecationMessage": "" + }, + { "Name": "maxFileCacheBytes", "Type": "int64", "Doc": "maxFileCacheBytes sets a soft limit on the file cache size in bytes.\nIf zero, the default budget is used.\n\nThe cache may temporarily use more than this amount.\nAlso, this parameter limits file contents; disk block usage\nas measured by du(1) may be significantly higher.\n", @@ -3421,7 +3477,7 @@ }, { "Name": "errorsas", - "Doc": "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analyzer reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.\nFor example:\n\n\tvar unwrappedErr net.DNSError\n\terrors.As(err, unwrappedErr) // should use \u0026unwrappedErr, DNSError.Error has a pointer reciever\n", + "Doc": "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analyzer reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.\nFor example:\n\n\tvar unwrappedErr net.DNSError\n\terrors.As(err, unwrappedErr) // should use \u0026unwrappedErr, DNSError.Error has a pointer receiver\n", "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/errorsas", "Default": true }, @@ -3432,9 +3488,9 @@ "Default": true }, { - "Name": "errorsastype", - "Doc": "Reports misuse of errors.AsType[T] in if/else chains.\nFor example:\n\n\terr := f()\n\tif err, ok := errors.AsType[*FooErr](err); ok {\n\t useFoo(err)\n\t} else if err, ok := errors.AsType[*BarErr](err); ok {\n\t useBar(err)\n\t}\n\nIn this case, the second call to errors.AsType does not operate on the\noriginal error. Instead, its operand is the zero value of type *FooErr\nproduced by the first if statement; this is invariably a mistake.\n", - "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/errorsastype", + "Name": "errorsastypeshadow", + "Doc": "report shadowing of errors.AsType[T] in if/else chains\n\nFor example:\n\n\terr := f()\n\tif err, ok := errors.AsType[*FooErr](err); ok {\n\t useFoo(err)\n\t} else if err, ok := errors.AsType[*BarErr](err); ok {\n\t useBar(err)\n\t}\n\nIn this case, the second call to errors.AsType does not operate on the\noriginal error. Instead, its operand is the zero value of type *FooErr\nproduced by the first if statement; this is invariably a mistake.", + "URL": "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/errorsastypeshadow", "Default": true }, { @@ -3493,7 +3549,7 @@ }, { "Name": "inline", - "Doc": "apply fixes based on 'go:fix inline' comment directives\n\nThe inline analyzer inlines functions and constants that are marked for inlining.\n\n## Functions\n\nGiven a function that is marked for inlining, like this one:\n\n\t//go:fix inline\n\tfunc Square(x int) int { return Pow(x, 2) }\n\nthis analyzer will recommend that calls to the function elsewhere, in the same\nor other packages, should be inlined.\n\nInlining can be used to move off of a deprecated function:\n\n\t// Deprecated: prefer Pow(x, 2).\n\t//go:fix inline\n\tfunc Square(x int) int { return Pow(x, 2) }\n\nIt can also be used to move off of an obsolete package,\nas when the import path has changed or a higher major version is available:\n\n\tpackage pkg\n\n\timport pkg2 \"pkg/v2\"\n\n\t//go:fix inline\n\tfunc F() { pkg2.F(nil) }\n\nReplacing a call pkg.F() by pkg2.F(nil) can have no effect on the program,\nso this mechanism provides a low-risk way to update large numbers of calls.\nWe recommend, where possible, expressing the old API in terms of the new one\nto enable automatic migration.\n\nThe inliner takes care to avoid behavior changes, even subtle ones,\nsuch as changes to the order in which argument expressions are\nevaluated. When it cannot safely eliminate all parameter variables,\nit may introduce a \"binding declaration\" of the form\n\n\tvar params = args\n\nto evaluate argument expressions in the correct order and bind them to\nparameter variables. Since the resulting code transformation may be\nstylistically suboptimal, such inlinings may be disabled by specifying\nthe -inline.allow_binding_decl=false flag to the analyzer driver.\n\n(In cases where it is not safe to \"reduce\" a call—that is, to replace\na call f(x) by the body of function f, suitably substituted—the\ninliner machinery is capable of replacing f by a function literal,\nfunc(){...}(). However, the inline analyzer discards all such\n\"literalizations\" unconditionally, again on grounds of style.)\n\nA call to a function F from its dedicated test (TestF) is not inlined,\nsince the purpose of the test is to exercise F itself, even when\nit's a deprecated function to which other calls should be inlined.\nThis is not true for type aliases; see https://go.dev/issue/79271.\nSee further discussion in https://go.dev/issue/79272.\n\n## Constants\n\nGiven a constant that is marked for inlining, like this one:\n\n\t//go:fix inline\n\tconst Ptr = Pointer\n\nthis analyzer will recommend that uses of Ptr should be replaced with Pointer.\n\nAs with functions, inlining can be used to replace deprecated constants and\nconstants in obsolete packages.\n\nA constant definition can be marked for inlining only if it refers to another\nnamed constant.\n\nThe \"//go:fix inline\" comment must appear before a single const declaration on its own,\nas above; before a const declaration that is part of a group, as in this case:\n\n\tconst (\n\t C = 1\n\t //go:fix inline\n\t Ptr = Pointer\n\t)\n\nor before a group, applying to every constant in the group:\n\n\t//go:fix inline\n\tconst (\n\t\tPtr = Pointer\n\t Val = Value\n\t)\n\nThe proposal https://go.dev/issue/32816 introduces the \"//go:fix inline\" directives.\n\nYou can use this command to apply inline fixes en masse:\n\n\t$ go run golang.org/x/tools/go/analysis/passes/inline/cmd/inline@latest -fix ./...", + "Doc": "apply fixes based on 'go:fix inline' comment directives\n\nThe inline analyzer inlines functions, constants, and type aliases\nthat are marked for inlining.\n\nUse this command to apply (just) inline fixes en masse:\n\n\t$ go fix -inline ./...\n\n## Functions\n\nGiven a function that is marked for inlining, like this one:\n\n\t//go:fix inline\n\tfunc Square(x int) int { return Pow(x, 2) }\n\nthis analyzer will recommend that calls to the function elsewhere, in the same\nor other packages, should be inlined.\n\nInlining can be used to move off of a deprecated function:\n\n\t// Deprecated: prefer Pow(x, 2).\n\t//go:fix inline\n\tfunc Square(x int) int { return Pow(x, 2) }\n\nIt can also be used to move off of an obsolete package,\nas when the import path has changed or a higher major version is available:\n\n\tpackage pkg\n\n\timport pkg2 \"pkg/v2\"\n\n\t//go:fix inline\n\tfunc F() { pkg2.F(nil) }\n\nReplacing a call pkg.F() by pkg2.F(nil) can have no effect on the program,\nso this mechanism provides a low-risk way to update large numbers of calls.\nWe recommend, where possible, expressing the old API in terms of the new one\nto enable automatic migration.\n\nThe inliner takes care to avoid behavior changes, even subtle ones,\nsuch as changes to the order in which argument expressions are\nevaluated. When it cannot safely eliminate all parameter variables,\nit may introduce a \"binding declaration\" of the form\n\n\tvar params = args\n\nto evaluate argument expressions in the correct order and bind them to\nparameter variables. Since the resulting code transformation may be\nstylistically suboptimal, such inlinings may be disabled by specifying\nthe -inline.allow_binding_decl=false flag to the analyzer driver.\n\n(In cases where it is not safe to \"reduce\" a call—that is, to replace\na call f(x) by the body of function f, suitably substituted—the\ninliner machinery is capable of replacing f by a function literal,\nfunc(){...}(). However, the inline analyzer discards all such\n\"literalizations\" unconditionally, again on grounds of style.)\n\n## Constants\n\nGiven a constant that is marked for inlining, like this one:\n\n\t//go:fix inline\n\tconst Ptr = Pointer\n\nthis analyzer will recommend that uses of Ptr should be replaced with Pointer.\n\nAs with functions, inlining can be used to replace deprecated constants and\nconstants in obsolete packages.\n\nA constant definition can be marked for inlining only if it refers to another\nnamed constant.\n\nThe \"//go:fix inline\" comment must appear before a single const declaration on its own,\nas above; before a const declaration that is part of a group, as in this case:\n\n\tconst (\n\t C = 1\n\t //go:fix inline\n\t Ptr = Pointer\n\t)\n\nor before a group, applying to every constant in the group:\n\n\t//go:fix inline\n\tconst (\n\t\tPtr = Pointer\n\t\tVal = Value\n\t)\n\n## Type aliases\n\nSimilar to named constants, a type alias can also be marked for inlining:\n\n\t//go:fix inline\n\ttype A = newpkg.A\n\nThe analyzer will replace all references to the annotated type\n(A) by the type on the right-hand side of the declaration (newpkg.A).\n\n## Tests\n\nA use of a function, named constant, or type alias X from its\ndedicated test (TestX), is not inlined, since the purpose of the test\nis to exercise X itself, even if it is deprecated and other uses of it\nshould be inlined.\nThis applies to benchmarks and examples too, and follows the usual\nconventions of test function naming.\n\nSimilarly, if the symbol X is declared in a file named foo.go, any use\nof it within a file named foo_test.go will also not be inlined.", "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/inline", "Default": true }, @@ -3636,6 +3692,12 @@ "Default": true }, { + "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", + "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", @@ -3666,6 +3728,12 @@ "Default": true }, { + "Name": "sqlrowserr", + "Doc": "sqlrowserr: report failure to check sql.Rows.Err\n\nThis analyzer reports uses of sql.Rows in which the result of a query\nsuch as db.Query() is assigned to a local variable that is then used\nin a loop that calls Rows.Next, but lacks a final check of Rows.Err.\nThis causes row iteration errors to be discarded.\n\nFor example:\n\n\trows, err := db.Query(\"select ...\") // error: \"sql.Rows rows is used in Next loop without final check of rows.Err()\"\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close() // ignore error\n\tfor rows.Next() {\n\t\tvar x int\n\t\tif err := rows.Scan(\u0026x); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuse(x)\n\t}\n\t/* ...no use of rows.Err()... */\n\nCorrect usage of sql.Rows demands both a call to Rows.Close to release\nresources and a call to Rows.Err to report iteration errors. It is\nnot critical to report resource cleanup errors, but it is crucial to\nreport iteration errors as they would otherwise be indistinguishable\nfrom a smaller result.\n\nTo avoid false positives, the analyzer is silent if the Rows is passed\ninto or out of the function or assigned somewhere other than a local\nvariable.\n\nIt is not this analyzer's goal to ensure proper handling of errors in\nall cases, but merely the simple mistakes where the user may have been\noblivious to the existence of the Rows.Err method.\n", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sqlrowserr", + "Default": true + }, + { "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", @@ -3825,49 +3893,49 @@ "Hints": [ { "Name": "assignVariableTypes", - "Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```\n", + "Doc": "`\"assignVariableTypes\"` controls inlay hints for variable types in assign statements:\n```go\n\ti« int», j« int» := 0, len(r)-1\n```\n", "Default": false, "Status": "" }, { "Name": "compositeLiteralFields", - "Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```\n", + "Doc": "`\"compositeLiteralFields\"` inlay hints for composite literal field names:\n```go\n\tPoint2D{«X: »1, «Y: »2}\n\n\tOuter{«Embedded.»Field: 0}\n```\n", "Default": false, "Status": "" }, { "Name": "compositeLiteralTypes", - "Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n", + "Doc": "`\"compositeLiteralTypes\"` controls inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t«struct{ in string; want string }»{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```\n", "Default": false, "Status": "" }, { "Name": "constantValues", - "Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```\n", + "Doc": "`\"constantValues\"` controls inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota« = 0»\n\t\tKindPrint« = 1»\n\t\tKindPrintf« = 2»\n\t\tKindErrorf« = 3»\n\t)\n```\n", "Default": false, "Status": "" }, { "Name": "functionTypeParameters", - "Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```\n", + "Doc": "`\"functionTypeParameters\"` inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo«[int, string]»(1, \"hello\")\n```\n", "Default": false, "Status": "" }, { "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\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", "Default": false, "Status": "" }, { "Name": "parameterNames", - "Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```\n", + "Doc": "`\"parameterNames\"` controls inlay hints for parameter names:\n```go\n\tparseInt(« str: » \"123\", « radix: » 8)\n```\n", "Default": false, "Status": "" }, { "Name": "rangeVariableTypes", - "Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n", + "Doc": "`\"rangeVariableTypes\"` controls inlay hints for variable types in range statements:\n```go\n\tfor k« int», v« string» := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```\n", "Default": false, "Status": "" }
diff --git a/gopls/internal/doc/generate/generate.go b/gopls/internal/doc/generate/generate.go index 5b31ce9..da3a04e 100644 --- a/gopls/internal/doc/generate/generate.go +++ b/gopls/internal/doc/generate/generate.go
@@ -23,6 +23,7 @@ "go/doc/comment" "go/token" "go/types" + "log" "maps" "os" "os/exec" @@ -754,8 +755,15 @@ } func rewriteAnalyzers(prevContent []byte, api *doc.API) ([]byte, error) { + seen := make(map[string]string) // name->doc, for reporting dups + var buf bytes.Buffer for _, analyzer := range api.Analyzers { + if prevDoc, ok := seen[analyzer.Name]; ok { + log.Fatalf("duplicate analyzer name %q\n===1===\n%s\n===2===\n%s", analyzer.Name, analyzer.Doc, prevDoc) + } + seen[analyzer.Name] = analyzer.Doc + fmt.Fprintf(&buf, "<a id='%s'></a>\n", analyzer.Name) title, doc, _ := strings.Cut(analyzer.Doc, "\n") title = strings.TrimPrefix(title, analyzer.Name+": ") @@ -790,7 +798,6 @@ fmt.Fprintf(&buf, "Package documentation: [%s](%s)\n\n", analyzer.Name, analyzer.URL) } - } return replaceSection(prevContent, "Analyzers", buf.Bytes()) }
diff --git a/gopls/internal/filecache/filecache.go b/gopls/internal/filecache/filecache.go index 0857dc0..c668a3a 100644 --- a/gopls/internal/filecache/filecache.go +++ b/gopls/internal/filecache/filecache.go
@@ -47,7 +47,7 @@ // Start is automatically called by the first call to Get, but may be called // explicitly to pre-initialize the cache. func Start() { - go getCacheDir() + go getCacheDir() // ignore error } // memCache is a 100MB in-memory LRU cache in front of filecache @@ -66,6 +66,15 @@ // returned by Get when the key is not found. var ErrNotFound = fmt.Errorf("not found") +// ErrNoCache is the type of errors returned by Get when the cache +// cannot be created at all (e.g. due to disk space, lack of +// permission, deletion of the gopls executable, or hardware fault). +// +// The appropriate action in this case is typically to log.Fatal since +// there is little the application can do and performance will +// inevitably be terrible; See [GetOrFatal]. +type ErrNoCache struct{ error } + // Bytes is the identity decoder, for use with [Get] when the caller // wants the raw bytes. func Bytes(data []byte) []byte { return data } @@ -77,11 +86,14 @@ // holding raw bytes (from a recent [Set]), the entry is decoded once // and upgraded in place from raw bytes to the decoded value. // -// Get returns ErrNotFound if the value was not found. The first call -// to Get may fail due to ENOSPC or deletion of the process's -// executable. Other causes of failure include deletion or corruption -// of the cache (by external meddling) while gopls is running, or -// faulty hardware; see issue #67433. +// Get returns [ErrNotFound] if the value was not found. +// +// Get returns [ErrNoCache] if the cache did not exist and could not +// be created. This may be due to ENOSPC, deletion of the process's +// executable, deletion or corruption of the cache by external +// meddling while gopls is running, or by faulty hardware (see issue +// #67433). In this case, terminating the application is likely the +// best course; see [GetOrFatal]. // // Each kind must be used with exactly one decoded type T; mixing types // for the same kind is a programming error and will panic. @@ -121,6 +133,23 @@ return result, nil } +// GetOrFatal retrieves from the cache like [Get], but if it +// encounters an unrecoverable [ErrNoCache] error, it calls log.Fatal. +// Any unexpected error (not [ErrNotFound]) is logged. +func GetOrFatal[T any](kind string, key [32]byte, decode func([]byte) T) (T, bool) { + val, err := Get(kind, key, decode) + if err == nil { + return val, true // hit + } + if errNoCache, ok := errors.AsType[ErrNoCache](err); ok { + log.Fatal(errNoCache) // unrecoverable + } + if err != ErrNotFound { + log.Printf("unexpected error reading %s from filecache: %v", kind, err) + } + return *new(T), false // miss +} + // get reads the value for (kind, key) from the file system, bypassing // the in-memory cache. func get(kind string, key [32]byte) ([]byte, error) { @@ -130,9 +159,7 @@ // Read the index file, which provides the name of the CAS file. indexName, err := filename(kind, key) if err != nil { - // e.g. ENOSPC, deletion of executable (first time only); - // deletion of cache (at any time). - return nil, err + return nil, err // (ErrNoCache) } indexData, err := os.ReadFile(indexName) if err != nil { @@ -153,7 +180,7 @@ // engineered hash collision, which is infeasible. casName, err := filename(casKind, valueHash) if err != nil { - return nil, err // see above for possible causes + return nil, err // (ErrNoCache) } value, _ := os.ReadFile(casName) // ignore error if sha256.Sum256(value) != valueHash { @@ -201,7 +228,7 @@ hash := sha256.Sum256(value) casName, err := filename(casKind, hash) if err != nil { - return err + return err // (ErrNoCache) } // Does CAS file exist and have correct (complete) content? // TODO(adonovan): opt: use mmap for this check. @@ -220,10 +247,10 @@ // Now write an index entry that refers to the CAS file. indexName, err := filename(kind, key) if err != nil { - return err + return err // (ErrNoCache) } if err := os.MkdirAll(filepath.Dir(indexName), 0700); err != nil { - return err + return err // e.g. disk full } if err := writeFileNoTrunc(indexName, hash[:], 0600); err != nil { os.Remove(indexName) // ignore error @@ -367,7 +394,7 @@ base := fmt.Sprintf("%x-%s", key, kind) dir, err := getCacheDir() if err != nil { - return "", err + return "", err // (ErrNoCache) } // Keep the BugReports function consistent with this one. return filepath.Join(dir, base[:2], base), nil @@ -412,13 +439,13 @@ // Compute the hash of this executable (~20ms) and create a subdirectory. hash, err := hashExecutable() if err != nil { - cacheDirErr = fmt.Errorf("can't hash gopls executable: %w", err) + cacheDirErr = ErrNoCache{fmt.Errorf("can't hash gopls executable: %w", err)} } // Use only 32 bits of the digest to avoid unwieldy filenames. // It's not an adversarial situation. cacheDir = filepath.Join(goplsDir, fmt.Sprintf("%x", hash[:4])) if err := os.MkdirAll(cacheDir, 0700); err != nil { - cacheDirErr = fmt.Errorf("can't create cache: %w", err) + cacheDirErr = ErrNoCache{fmt.Errorf("can't create cache: %w", err)} } }) return cacheDir, cacheDirErr
diff --git a/gopls/internal/filewatcher/filewatcher_test.go b/gopls/internal/filewatcher/filewatcher_test.go index 57e4572..2c8f561 100644 --- a/gopls/internal/filewatcher/filewatcher_test.go +++ b/gopls/internal/filewatcher/filewatcher_test.go
@@ -356,7 +356,7 @@ } { - // Prepare a dir with with broken symbolic link. + // Prepare a dir with broken symbolic link. // foo <- 1st // ├── from.go -> root/to.go <- 1st // ├── a.go <- 1st
diff --git a/gopls/internal/filewatcher/fsnotify_watcher.go b/gopls/internal/filewatcher/fsnotify_watcher.go index a73bf04..8e06a00 100644 --- a/gopls/internal/filewatcher/fsnotify_watcher.go +++ b/gopls/internal/filewatcher/fsnotify_watcher.go
@@ -59,7 +59,6 @@ log *slog.Logger onEvents func([]protocol.FileEvent) onError func(error) - // interval time.Duration stop chan struct{} // closed by Close to terminate run and process loop wg sync.WaitGroup // counts the number of active run and process goroutines (max 2)
diff --git a/gopls/internal/golang/addtest.go b/gopls/internal/golang/addtest.go index 51664c5..cef26a4 100644 --- a/gopls/internal/golang/addtest.go +++ b/gopls/internal/golang/addtest.go
@@ -30,6 +30,7 @@ "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/versions" ) const testTmplString = ` @@ -481,6 +482,8 @@ return nil, nil, err } + fileVersion := pkg.TypesInfo().FileVersions[pgf.File] + data := testInfo{ TestingPackageName: qual(types.NewPackage("testing", "testing")), PackageName: qual(pkg.Types()), @@ -491,33 +494,46 @@ }, } - isContextType := func(t types.Type) bool { - return typesinternal.IsTypeNamed(t, "context", "Context") - } - - isUnusedParameter := func(name string) bool { - return name == "" || name == "_" - } - - for i := range sig.Params().Len() { - param := sig.Params().At(i) - name, typ := param.Name(), param.Type() - f := field{Type: types.TypeString(typ, qual)} - if i == 0 && isContextType(typ) { - f.Value = qual(types.NewPackage("context", "context")) + ".Background()" - } else if isUnusedParameter(name) && data.Func.IsVariadic && sig.Params().Len()-1 == i { - // The last argument is the variadic argument, and it's not used in the function body, - // so we don't need to render it in the test case struct. - data.Func.IsUnusedVariadic = true - continue - } else if isUnusedParameter(name) { - f.Value, _ = typesinternal.ZeroString(typ, qual) - } else { - f.Name = name + // populateArgs fills fn.Args from sig's parameters, applying the conventions + // used by the test-generation template: + // - a leading context.Context parameter becomes t.Context() (Go 1.24+) or + // context.Background() (older modules), gated by fileVersion; + // - an unused trailing variadic parameter is dropped and recorded via + // fn.IsUnusedVariadic; + // - other unused ("" or "_") parameters get a zero-value literal; + // - named parameters keep their name and type for use as a test-table field. + populateArgs := func(fn *function, sig *types.Signature) { + for i := range sig.Params().Len() { + param := sig.Params().At(i) + name, typ := param.Name(), param.Type() + var f field + switch { + case i == 0 && typesinternal.IsTypeNamed(typ, "context", "Context"): + if versions.AtLeast(fileVersion, versions.Go1_24) { + f.Value = "t.Context()" + } else { + f.Type = types.TypeString(typ, qual) + f.Value = qual(types.NewPackage("context", "context")) + ".Background()" + } + case name == "" || name == "_": + if fn.IsVariadic && i == sig.Params().Len()-1 { + // The last argument is the variadic argument, and it's not used in the function body, + // so we don't need to render it in the test case struct. + fn.IsUnusedVariadic = true + continue + } + f.Type = types.TypeString(typ, qual) + f.Value, _ = typesinternal.ZeroString(typ, qual) + default: + f.Type = types.TypeString(typ, qual) + f.Name = name + } + fn.Args = append(fn.Args, f) } - data.Func.Args = append(data.Func.Args, f) } + populateArgs(&data.Func, sig) + for i := range sig.Results().Len() { typ := sig.Results().At(i).Type() var name string @@ -641,24 +657,7 @@ Name: constructor.Name(), IsVariadic: constructor.Signature().Variadic(), } - for i := range constructor.Signature().Params().Len() { - param := constructor.Signature().Params().At(i) - name, typ := param.Name(), param.Type() - f := field{Type: types.TypeString(typ, qual)} - if i == 0 && isContextType(typ) { - f.Value = qual(types.NewPackage("context", "context")) + ".Background()" - } else if isUnusedParameter(name) && data.Receiver.Constructor.IsVariadic && constructor.Signature().Params().Len()-1 == i { - // The last argument is the variadic argument, and it's not used in the function body, - // so we don't need to render it in the test case struct. - data.Receiver.Constructor.IsUnusedVariadic = true - continue - } else if isUnusedParameter(name) { - f.Value, _ = typesinternal.ZeroString(typ, qual) - } else { - f.Name = name - } - data.Receiver.Constructor.Args = append(data.Receiver.Constructor.Args, f) - } + populateArgs(data.Receiver.Constructor, constructor.Signature()) for i := range constructor.Signature().Results().Len() { typ := constructor.Signature().Results().At(i).Type() var name string
diff --git a/gopls/internal/golang/assembly.go b/gopls/internal/golang/assembly.go index c91482f..00f62a9 100644 --- a/gopls/internal/golang/assembly.go +++ b/gopls/internal/golang/assembly.go
@@ -18,8 +18,7 @@ "bytes" "context" "fmt" - "html" - "io" + "html/template" "net/http" "os" "regexp" @@ -54,53 +53,28 @@ } defer cleanupInvocation() - escape := html.EscapeString - // Emit the start of the report. - titleHTML := fmt.Sprintf("%s assembly for %s", - escape(snapshot.View().GOARCH()), - escape(symbol)) - io.WriteString(w, `<!DOCTYPE html> -<html> -<head> - <meta charset="UTF-8"> - <title>`+titleHTML+`</title> - <link rel="stylesheet" href="/assets/common.css"> - <script src="/assets/common.js"></script> -</head> -<body> -<h1>`+titleHTML+`</h1> -<p> - <a href='https://go.dev/doc/asm'>A Quick Guide to Go's Assembler</a> -</p> -<p> - Experimental. <a href='https://github.com/golang/go/issues/67478'>Contributions welcome!</a> -</p> -<p> - Click on a source line marker <code>L1234</code> to navigate your editor there. - (VS Code users: please upvote <a href='https://github.com/microsoft/vscode/issues/208093'>#208093</a>) -</p> -<p id='compiling'>Compiling...</p> -<pre> -`) + + if err := asmHeader.Execute(w, asmHeaderData{ + Title: fmt.Sprintf("%s assembly for %s", snapshot.View().GOARCH(), symbol), + }); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } if flusher, ok := w.(http.Flusher); ok { flusher.Flush() } // At this point errors must be reported by writing HTML. - // To do this, set "status" return early. + // To do this, set "status" and return early. - var buf bytes.Buffer + var buf bytes.Buffer // properly quoted content of <pre> followed by footer (</pre> etc). status := "Reload the page to recompile." defer func() { - // Update the "Compiling..." message. - fmt.Fprintf(&buf, ` -</pre> -<script> -document.getElementById('compiling').innerText = %q; -</script> -</body>`, status) - w.Write(buf.Bytes()) + // Append the footer to close the <pre> and update the "Compiling..." message. + _ = asmFooter.Execute(&buf, asmFooterData{Status: status}) + + w.Write(buf.Bytes()) // ignore error }() // Compile the package. @@ -125,6 +99,7 @@ // ... // // Allow matches of symbol, symbol.func1, symbol.deferwrap, etc. + escape := template.HTMLEscapeString on := false for line := range strings.SplitSeq(content, "\n") { // start of function symbol? @@ -155,3 +130,42 @@ buf.WriteByte('\n') } } + +type asmHeaderData struct { + Title string +} + +var asmHeader = template.Must(template.New("header").Parse(`<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>{{.Title}}</title> + <link rel="stylesheet" href="/assets/common.css"> + <script src="/assets/common.js"></script> +</head> +<body> +<h1>{{.Title}}</h1> +<p> + <a href='https://go.dev/doc/asm'>A Quick Guide to Go's Assembler</a> +</p> +<p> + Experimental. <a href='https://github.com/golang/go/issues/67478'>Contributions welcome!</a> +</p> +<p> + Click on a source line marker <code>L1234</code> to navigate your editor there. + (VS Code users: please upvote <a href='https://github.com/microsoft/vscode/issues/208093'>#208093</a>) +</p> +<p id='compiling'>Compiling...</p> +<pre> +`)) + +type asmFooterData struct { + Status string +} + +var asmFooter = template.Must(template.New("footer").Parse(` +</pre> +<script> +document.getElementById('compiling').innerText = {{.Status}}; +</script> +</body>`))
diff --git a/gopls/internal/golang/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go index f37c20e..9378438 100644 --- a/gopls/internal/golang/call_hierarchy.go +++ b/gopls/internal/golang/call_hierarchy.go
@@ -204,7 +204,9 @@ return nil, err } - declNode, _, _, _ := findDeclInfo([]*ast.File{declPGF.File}, declPos) + // TODO(adonovan): shouldn't this be simply the enclosing FuncDecl? + // Or outermost Enclosing(FuncDecl | GenDecl)? + declNode, _, _, _ := findDeclInfo(declPGF, declPos) if declNode == nil { // TODO(rfindley): why don't we return an error here, or even bug.Errorf? return nil, nil
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index c87193e..9e9c79b 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go
@@ -256,7 +256,7 @@ {kind: settings.RefactorExtractVariableAll, fn: refactorExtractVariableAll, needPkg: true}, {kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true}, {kind: settings.RefactorInlineVariable, fn: refactorInlineVariable, needPkg: true}, - // {kind: settings.RefactorMoveType, fn: refactorMoveType, needPkg: true}, + {kind: settings.RefactorMoveType, fn: refactorMoveType, needPkg: true}, {kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote}, {kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true}, {kind: settings.RefactorRewriteFillSwitch, fn: refactorRewriteFillSwitch, needPkg: true}, @@ -339,7 +339,7 @@ si := stubmethods.GetIfaceStubInfo(req.pkg.FileSet(), info, req.pgf, start, end) if si != nil { qual := typesinternal.FileQualifier(req.pgf.File, si.Concrete.Obj().Pkg()) - iface := types.TypeString(si.Interface.Type(), qual) + iface := types.TypeString(si.Interface, qual) msg := fmt.Sprintf("Declare missing methods of %s", iface) req.addApplyFixAction(msg, fixMissingInterfaceMethods, req.loc) } @@ -815,10 +815,10 @@ // refactorRewriteFillStruct produces "Fill STRUCT" code actions. // See [fillstruct.SuggestedFix] for command implementation. func refactorRewriteFillStruct(ctx context.Context, req *codeActionsRequest) error { - // fillstruct.Diagnose is a lazy analyzer: all it gives us is - // the (start, end, message) of each SuggestedFix; the actual - // edit is computed only later by ApplyFix, which calls fillstruct.SuggestedFix. - for _, diag := range fillstruct.Diagnose(req.pgf.File, req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) { + // [fillstruct.Diagnose] is a lazy analyzer: all it gives us is the + // (start, end, message) of each SuggestedFix; the actual edit is + // computed only later by ApplyFix, which calls [fillstruct.SuggestedFix]. + for _, diag := range fillstruct.Diagnose(req.pgf.Cursor(), req.start, req.end, req.pkg.Types(), req.pkg.TypesInfo()) { loc, err := req.pgf.Mapper.PosLocation(req.pgf.Tok, diag.Pos, diag.End) if err != nil { return err @@ -827,6 +827,7 @@ req.addApplyFixAction(fix.Message, diag.Category, loc) } } + return nil } @@ -1245,11 +1246,14 @@ return nil } -// (this function is unused) func refactorMoveType(_ context.Context, req *codeActionsRequest) error { + if !req.snapshot.Options().MoveType { + return nil + } curSel, _ := req.pgf.Cursor().FindByPos(req.start, req.end) - if _, _, _, typeName, ok := selectionContainsType(curSel); ok { - cmd := command.NewMoveTypeCommand(fmt.Sprintf("Move type %s", typeName), command.MoveTypeArgs{Location: req.loc}) + if specCur, ok := selectionContainsTypeSpec(curSel); ok { + spec := specCur.Node().(*ast.TypeSpec) + cmd := command.NewMoveTypeCommand(fmt.Sprintf("Move type %s", spec.Name.Name), command.MoveTypeArgs{Location: req.loc}) req.addCommandAction(cmd, false) } return nil
diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go index e749db5..411e708 100644 --- a/gopls/internal/golang/completion/completion.go +++ b/gopls/internal/golang/completion/completion.go
@@ -11,23 +11,20 @@ "fmt" "go/ast" "go/constant" - "go/parser" - "go/printer" "go/scanner" "go/token" "go/types" + "iter" "math" "slices" "sort" "strconv" "strings" "sync" - "sync/atomic" "time" "unicode" "unicode/utf8" - "golang.org/x/sync/errgroup" goastutil "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" @@ -43,7 +40,6 @@ "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" @@ -1337,7 +1333,6 @@ // See https://golang.org/issue/36001. Unimported completions are expensive. const ( maxUnimportedPackageNames = 5 - unimportedMemberTarget = 100 ) // selector finds completions for the specified selector expression. @@ -1360,8 +1355,6 @@ } // Treat sel as a qualified identifier. - var filter func(*metadata.Package) bool - needImport := false if pkgName, ok := c.pkg.TypesInfo().Uses[id].(*types.PkgName); ok { // Qualified identifier with import declaration. imp := pkgName.Imported() @@ -1372,19 +1365,6 @@ return nil } - // Imported declaration with missing type information. - // Fall through to shallow completion of unimported package members. - // Match candidate packages by path. - filter = func(mp *metadata.Package) bool { - return strings.TrimPrefix(string(mp.PkgPath), "vendor/") == imp.Path() - } - } else { - // Qualified identifier without import declaration. - // Match candidate packages by name. - filter = func(mp *metadata.Package) bool { - return string(mp.Name) == id.Name - } - needImport = true } // Search unimported packages. @@ -1394,287 +1374,26 @@ // -- completion of symbols in unimported packages -- - // use new code for unimported completions, if flag allows it - if c.snapshot.Options().ImportsSource == settings.ImportsSourceGopls { - // The user might have typed strings.TLower, so id.Name==strings, sel.Sel.Name == TLower, - // but the cursor might be inside TLower, so adjust the prefix - prefix := sel.Sel.Name - if c.surrounding != nil { - if c.surrounding.content != sel.Sel.Name { - bug.Reportf("unexpected surrounding: %q != %q", c.surrounding.content, sel.Sel.Name) + // The user might have typed strings.TLower, so id.Name==strings, sel.Sel.Name == TLower, + // but the cursor might be inside TLower, so adjust the prefix + 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 { - prefix = sel.Sel.Name[:c.surrounding.cursor-c.surrounding.start] + bug.Reportf("unexpected surrounding: %q != %q", c.surrounding.content, sel.Sel.Name) } - } - c.unimported(ctx, metadata.PackageName(id.Name), prefix) - return nil - - } - - // The deep completion algorithm is exceedingly complex and - // deeply coupled to the now obsolete notions that all - // token.Pos values can be interpreted by as a single FileSet - // belonging to the Snapshot and that all types.Object values - // are canonicalized by a single types.Importer mapping. - // These invariants are no longer true now that gopls uses - // an incremental approach, parsing and type-checking each - // package separately. - // - // Consequently, completion of symbols defined in packages that - // are not currently imported by the query file cannot use the - // deep completion machinery which is based on type information. - // Instead it must use only syntax information from a quick - // parse of top-level declarations (but not function bodies). - // - // TODO(adonovan): rewrite the deep completion machinery to - // not assume global Pos/Object realms and then use export - // data instead of the quick parse approach taken here. - - // First, we search among packages in the forward transitive - // closure of the workspace. - // We'll use a fast parse to extract package members - // from those that match the name/path criterion. - all, err := c.snapshot.AllMetadata(ctx) - if err != nil { - return err - } - known := make(map[golang.PackagePath]*metadata.Package) - for _, mp := range all { - if mp.Name == "main" { - continue // not importable - } - if mp.IsIntermediateTestVariant() { - continue - } - // The only test variant we admit is "p [p.test]" - // when we are completing within "p_test [p.test]", - // as in that case we would like to offer completions - // of the test variants' additional symbols. - if mp.ForTest != "" && c.pkg.Metadata().PkgPath != mp.ForTest+"_test" { - continue - } - if !filter(mp) { - continue - } - // Prefer previous entry unless this one is its test variant. - if mp.ForTest != "" || known[mp.PkgPath] == nil { - known[mp.PkgPath] = mp + } else { + prefix = sel.Sel.Name[:c.surrounding.cursor-c.surrounding.start] } } - - paths := make([]string, 0, len(known)) - for path := range known { - paths = append(paths, string(path)) - } - - // Rank import paths as goimports would. - var relevances map[string]float64 - if len(paths) > 0 { - if err := c.snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error { - var err error - relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths) - return err - }); err != nil { - return err - } - sort.Slice(paths, func(i, j int) bool { - return relevances[paths[i]] > relevances[paths[j]] - }) - } - - // quickParse does a quick parse of a single file of package m, - // extracts exported package members and adds candidates to c.items. - // TODO(rfindley): synchronizing access to c here does not feel right. - // Consider adding a concurrency-safe API for completer. - var cMu sync.Mutex // guards c.items and c.matcher - var enough int32 // atomic bool - quickParse := func(uri protocol.DocumentURI, mp *metadata.Package, tooNew map[string]bool) error { - if atomic.LoadInt32(&enough) != 0 { - return nil - } - - fh, err := c.snapshot.ReadFile(ctx, uri) - if err != nil { - return err - } - content, err := fh.Content() - if err != nil { - return err - } - path := string(mp.PkgPath) - forEachPackageMember(content, func(tok token.Token, id *ast.Ident, fn *ast.FuncDecl) { - if atomic.LoadInt32(&enough) != 0 { - return - } - - if !id.IsExported() { - return - } - - if tooNew[id.Name] { - return // symbol too new for requesting file's Go's version - } - - cMu.Lock() - score := c.matcher.Score(id.Name) - cMu.Unlock() - - if sel.Sel.Name != "_" && score == 0 { - return // not a match; avoid constructing the completion item below - } - - // The only detail is the kind and package: `var (from "example.com/foo")` - // TODO(adonovan): pretty-print FuncDecl.FuncType or TypeSpec.Type? - // TODO(adonovan): should this score consider the actual c.matcher.Score - // of the item? How does this compare with the deepState.enqueue path? - item := CompletionItem{ - Label: id.Name, - Detail: fmt.Sprintf("%s (from %q)", strings.ToLower(tok.String()), mp.PkgPath), - InsertText: id.Name, - Score: float64(score) * unimportedScore(relevances[path]), - } - switch tok { - case token.FUNC: - item.Kind = protocol.FunctionCompletion - case token.VAR: - item.Kind = protocol.VariableCompletion - case token.CONST: - item.Kind = protocol.ConstantCompletion - case token.TYPE: - // Without types, we can't distinguish Class from Interface. - item.Kind = protocol.ClassCompletion - } - - if needImport { - imp := &importInfo{importPath: path} - if imports.ImportPathToAssumedName(path) != string(mp.Name) { - imp.name = string(mp.Name) - } - item.AdditionalTextEdits, _ = c.importEdits(imp) - } - - // For functions, add a parameter snippet. - if fn != nil { - paramList := func(list *ast.FieldList) []string { - var params []string - if list != nil { - var cfg printer.Config // slight overkill - param := func(name string, typ ast.Expr) { - var buf strings.Builder - buf.WriteString(name) - buf.WriteByte(' ') - cfg.Fprint(&buf, token.NewFileSet(), typ) // ignore error - params = append(params, buf.String()) - } - - for _, field := range list.List { - if field.Names != nil { - for _, name := range field.Names { - param(name.Name, field.Type) - } - } else { - param("_", field.Type) - } - } - } - return params - } - - // Ideally we would eliminate the suffix of type - // parameters that are redundant with inference - // from the argument types (#51783), but it's - // quite fiddly to do using syntax alone. - // (See inferableTypeParams in format.go.) - tparams := paramList(fn.Type.TypeParams) - params := paramList(fn.Type.Params) - var sn snippet.Builder - c.functionCallSnippet(id.Name, tparams, params, &sn) - item.snippet = &sn - } - - cMu.Lock() - c.items = append(c.items, item) - if len(c.items) >= unimportedMemberTarget { - atomic.StoreInt32(&enough, 1) - } - cMu.Unlock() - }) - return nil - } - - goversion := c.pkg.TypesInfo().FileVersions[c.pgf.File] - - // Extract the package-level candidates using a quick parse. - var g errgroup.Group - for _, path := range paths { - mp := known[golang.PackagePath(path)] - - // For standard packages, build a filter of symbols that - // are too new for the requesting file's Go version. - var tooNew map[string]bool - if syms, ok := stdlib.PackageSymbols[path]; ok && goversion != "" { - tooNew = make(map[string]bool) - for _, sym := range syms { - if versions.Before(goversion, sym.Version.String()) { - tooNew[sym.Name] = true - } - } - } - - for _, uri := range mp.CompiledGoFiles { - g.Go(func() error { - return quickParse(uri, mp, tooNew) - }) - } - } - if err := g.Wait(); err != nil { - return err - } - - // In addition, we search in the module cache using goimports. - ctx, cancel := context.WithCancel(ctx) - var mu sync.Mutex - add := func(pkgExport imports.PackageExport) { - if ignoreUnimportedCompletion(pkgExport.Fix) { - return - } - - mu.Lock() - defer mu.Unlock() - // TODO(adonovan): what if the actual package has a vendor/ prefix? - if _, ok := known[golang.PackagePath(pkgExport.Fix.StmtInfo.ImportPath)]; ok { - return // We got this one above. - } - - // Continue with untyped proposals. - pkg := types.NewPackage(pkgExport.Fix.StmtInfo.ImportPath, pkgExport.Fix.IdentName) - for _, symbol := range pkgExport.Exports { - if goversion != "" && versions.Before(goversion, symbol.Version.String()) { - continue // symbol too new for this file - } - score := unimportedScore(pkgExport.Fix.Relevance) - c.deepState.enqueue(candidate{ - obj: types.NewVar(0, pkg, symbol.Name, nil), - score: score, - imp: &importInfo{ - importPath: pkgExport.Fix.StmtInfo.ImportPath, - name: pkgExport.Fix.StmtInfo.Name, - }, - }) - } - if len(c.items) >= unimportedMemberTarget { - cancel() - } - } - - c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { - defer cancel() - if err := imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.Types().Name(), opts.Env); err != nil { - return fmt.Errorf("getting package exports: %v", err) - } - return nil - }) + c.unimported(ctx, metadata.PackageName(id.Name), prefix) return nil + } // unimportedScore returns a score for an unimported package that is generally @@ -2091,38 +1810,81 @@ func (c *completer) structLiteralFieldName(ctx context.Context) error { clInfo := c.enclosingCompositeLiteral - // Mark fields of the composite literal that have already been set, - // except for the current field. - addedFields := make(map[*types.Var]bool) - for _, el := range clInfo.cl.Elts { - if kvExpr, ok := el.(*ast.KeyValueExpr); ok { - if clInfo.kv == kvExpr { - continue - } + if t, ok := clInfo.clType.Underlying().(*types.Struct); ok { - if key, ok := kvExpr.Key.(*ast.Ident); ok { - if used, ok := c.pkg.TypesInfo().Uses[key]; ok { - if usedVar, ok := used.(*types.Var); ok { - addedFields[usedVar] = true + // Collect selection indices of all existing + // fields specified by the struct literal. + existing := make(map[types.Object][]int) + for _, elt := range clInfo.cl.Elts { + if kv, ok := elt.(*ast.KeyValueExpr); ok { + if key, ok := kv.Key.(*ast.Ident); ok && clInfo.kv != kv { + seln, ok := types.LookupSelection(clInfo.clType, true, c.pkg.Types(), key.Name) + if ok { + existing[seln.Obj()] = seln.Index() } } } } - } - // Add struct fields. - if t, ok := types.Unalias(clInfo.clType).(*types.Struct); ok { - const deltaScore = 0.0001 - for i := range t.NumFields() { - field := t.Field(i) - if !addedFields[field] { - c.deepState.enqueue(candidate{ - obj: field, - score: highScore - float64(i)*deltaScore, - }) + // fields returns the sequence of candidate fields of struct type t. + fields := func(t *types.Struct) iter.Seq[*types.Var] { + // go1.27 permits promoted fields in struct literals. + deep := versions.AtLeast(c.goversion, versions.Go1_27) + + return func(yield func(*types.Var) bool) { + var collect func(t *types.Struct) bool + collect = func(t *types.Struct) bool { + for f := range t.Fields() { + if !yield(f) { + return false + } + if deep && f.Anonymous() { + if inner, ok := f.Type().Underlying().(*types.Struct); ok && !collect(inner) { + return false + } + } + } + return true + } + collect(t) } } + // conflict reports whether one field index + // sequence is a prefix (ancestor) of the other. + conflict := func(a, b []int) bool { + for i := range min(len(a), len(b)) { + if a[i] != b[i] { + return false + } + } + return true + } + + fieldloop: + for f := range fields(t) { + seln, ok := types.LookupSelection(clInfo.clType, true, c.pkg.Types(), f.Name()) + if !ok || seln.Obj() != f { + continue // candidate's name is shadowed or ambiguous here + } + + // Reject candidates that conflict with existing fields. + for _, indices := range existing { + if conflict(seln.Index(), indices) { + continue fieldloop + } + } + + const deltaScore = 0.0001 + const depthPenalty = 0.01 + depth := len(seln.Index()) + fieldIdx := seln.Index()[depth-1] + c.deepState.enqueue(candidate{ + obj: seln.Obj(), + score: highScore - float64(depth-1)*depthPenalty - float64(fieldIdx)*deltaScore, + }) + } + // Fall through and add lexical completions if we aren't // certain we are in the key part of a key-value pair. if !clInfo.maybeInFieldName { @@ -2380,7 +2142,7 @@ convertibleTo types.Type // needsExactType is true if the candidate type must be exactly the type of - // the objType, e.g. an interface rather than it's implementors. + // the objType, e.g. an interface rather than its implementors. // // This is necessary when objType is derived using reverse type inference: // any different (but assignable) type may lead to different type inference, @@ -3844,34 +3606,6 @@ return false } -// forEachPackageMember calls f(tok, id, fn) for each package-level -// TYPE/VAR/CONST/FUNC declaration in the Go source file, based on a -// quick partial parse. fn is non-nil only for function declarations. -// The AST position information is garbage. -func forEachPackageMember(content []byte, f func(tok token.Token, id *ast.Ident, fn *ast.FuncDecl)) { - purged := astutil.PurgeFuncBodies(content) - file, _ := parser.ParseFile(token.NewFileSet(), "", purged, parser.SkipObjectResolution) - for _, decl := range file.Decls { - switch decl := decl.(type) { - case *ast.GenDecl: - for _, spec := range decl.Specs { - switch spec := spec.(type) { - case *ast.ValueSpec: // var/const - for _, id := range spec.Names { - f(decl.Tok, id, nil) - } - case *ast.TypeSpec: - f(decl.Tok, spec.Name, nil) - } - } - case *ast.FuncDecl: - if decl.Recv == nil { - f(token.FUNC, decl.Name, decl) - } - } - } -} - func is[T any](x any) bool { _, ok := x.(T) return ok
diff --git a/gopls/internal/golang/completion/deep_completion.go b/gopls/internal/golang/completion/deep_completion.go index 64e7381..6987fd7 100644 --- a/gopls/internal/golang/completion/deep_completion.go +++ b/gopls/internal/golang/completion/deep_completion.go
@@ -256,6 +256,9 @@ // its members for more candidates. func (c *completer) addCandidate(ctx context.Context, cand *candidate) { obj := cand.obj + if obj != nil && obj.Name() == "_" { + return + } if c.matchingCandidate(cand) { cand.score *= highScore
diff --git a/gopls/internal/golang/extracttofile.go b/gopls/internal/golang/extracttofile.go index 6aef663..9bd3d5b 100644 --- a/gopls/internal/golang/extracttofile.go +++ b/gopls/internal/golang/extracttofile.go
@@ -135,21 +135,7 @@ return nil, err } - var importDeletes []protocol.TextEdit - // For unparenthesised declarations like `import "fmt"` we remove - // the whole declaration because simply removing importSpec leaves - // `import \n`, which does not compile. - // For parenthesised declarations like `import ("fmt"\n "log")` - // we only remove the ImportSpec, because removing the whole declaration - // might remove other ImportsSpecs we don't want to touch. - unparenthesizedImports := unparenthesizedImports(pgf) - for _, importSpec := range deletes { - if decl := unparenthesizedImports[importSpec]; decl != nil { - importDeletes = append(importDeletes, removeNode(pgf, decl)) - } else { - importDeletes = append(importDeletes, removeNode(pgf, importSpec)) - } - } + importDeletes := importDeletesEdits(pgf, deletes) var buf bytes.Buffer if c := CopyrightComment(pgf.File); c != nil { @@ -208,6 +194,25 @@ })}, nil } +// importDeletesEdits returns a list of [protocol.TextEdit] for each import deletion. +func importDeletesEdits(pgf *parsego.File, deletes []*ast.ImportSpec) (edits []protocol.TextEdit) { + // For unparenthesised declarations like `import "fmt"` we remove + // the whole declaration because simply removing importSpec leaves + // `import \n`, which does not compile. + // For parenthesised declarations like `import ("fmt"\n "log")` + // we only remove the ImportSpec, because removing the whole declaration + // might remove other ImportsSpecs we don't want to touch. + unparenthesizedImports := unparenthesizedImports(pgf) + for _, importSpec := range deletes { + var n ast.Node = importSpec + if decl := unparenthesizedImports[importSpec]; decl != nil { + n = decl + } + edits = append(edits, removeNode(pgf, n)) + } + return edits +} + // chooseNewFile chooses a new filename in dir, based on the name of the // first extracted symbol, and if necessary to disambiguate, a numeric suffix. func chooseNewFile(ctx context.Context, snapshot *cache.Snapshot, dir string, firstSymbol string) (file.Handle, error) {
diff --git a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go index f17ce8b..222f45c 100644 --- a/gopls/internal/golang/format.go +++ b/gopls/internal/golang/format.go
@@ -21,7 +21,6 @@ "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/settings" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/gopls/internal/util/tokeninternal" "golang.org/x/tools/internal/diff" @@ -131,23 +130,7 @@ goroot := snapshot.View().Folder().Env.GOROOT filename := pgf.URI.Path() - // Build up basic information about the original file. - isource, err := imports.NewProcessEnvSource(options.Env, filename, pgf.File.Name.Name) - if err != nil { - return nil, nil, err - } - var source imports.Source - - // Keep this in sync with [cache.Session.createView] (see the TODO there: we - // should factor out the handling of the ImportsSource setting). - switch snapshot.Options().ImportsSource { - case settings.ImportsSourceGopls: - source = snapshot.NewGoplsSource(isource) - case settings.ImportsSourceOff: // for cider, which has no file system - source = nil - case settings.ImportsSourceGoimports: - source = isource - } + source := snapshot.NewGoplsSource() // imports require a current metadata graph // TODO(rfindley): improve the API snapshot.WorkspaceMetadata(ctx) // ignore error
diff --git a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go index 6c46bf1..f8aebc1 100644 --- a/gopls/internal/golang/highlight.go +++ b/gopls/internal/golang/highlight.go
@@ -142,8 +142,8 @@ if l, ok := curLoop.Parent().Node().(*ast.LabeledStmt); ok { label = l.Label } + highlightLoopControlFlow(stmt, label, info, result) } - highlightLoopControlFlow(stmt, label, info, result) } } }
diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index 60b7b69..55d07e1 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go
@@ -30,6 +30,7 @@ "golang.org/x/text/unicode/runenames" goastutil "golang.org/x/tools/go/ast/astutil" + "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/gopls/internal/cache" @@ -402,7 +403,7 @@ return protocol.Range{}, nil, err } - decl, spec, field, assign := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^4 + decl, spec, field, assign := findDeclInfo(declPGF, declPos) // may be nil^4 var docText string if docComment := chooseDocComment(declPGF, decl, spec, field, assign); docComment != nil { @@ -467,21 +468,17 @@ if sel, ok := pkg.TypesInfo().Selections[selExpr]; ok && len(sel.Index()) > 1 { var s strings.Builder s.WriteString(" // through ") - t := typesinternal.Unpointer(sel.Recv()) + t := sel.Recv() for i, index := range sel.Index()[:len(sel.Index())-1] { - if _, ok := t.Underlying().(*types.Struct); !ok { + structType, ok := typesinternal.Unpointer(t).Underlying().(*types.Struct) + if !ok { break } if i > 0 { s.WriteString(", ") } - field := typesinternal.Unpointer(t.Underlying()).(*types.Struct).Field(index) + field := structType.Field(index) t = field.Type() - // Inv: fieldType is N or *N for some NamedOrAlias type N. - if ptr, ok := t.(*types.Pointer); ok { - s.WriteString("*") - t = ptr.Elem() - } s.WriteString(types.TypeString(t, qual)) } // Update signature to include embedded struct info. @@ -497,11 +494,24 @@ signature += " " + field.Tag.Value } - // TODO(rfindley): we could do much better for inferred signatures. - // TODO(adonovan): fuse the two calls below. - if inferred := inferredSignature(pkg.TypesInfo(), ident); inferred != nil { - if s := inferredSignatureString(obj, qual, inferred); s != "" { - signature = s + // For references to generic functions or methods, + // show the inferred signature type in a comment: + // + // func Method(C[string]) // func[U any]() + // + // For all others, do nothing; signature will be computed below. + // + // TODO(adonovan): show TypeParam/TypeArg correspondence, e.g. [T=string, U=int]. + if inferred, ok := types.Unalias(pkg.TypesInfo().Instances[ident].Type).(*types.Signature); ok { + if sig, ok := obj.Type().Underlying().(*types.Signature); ok && sig.TypeParams().Len() > 0 { // (can't fail?) + obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred) + str := types.ObjectString(obj2, qual) + // Try to avoid overly long lines. + space := " " + if len(str) > 60 { + space = "\n" + } + signature = str + space + "// " + types.TypeString(sig, qual) } } @@ -803,7 +813,7 @@ // typeDeclContent returns a well formatted type definition. func typeDeclContent(declPGF *parsego.File, declPos token.Pos, name string) (string, *ast.TypeSpec, error) { - _, spec, _, _ := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^4 + _, spec, _, _ := findDeclInfo(declPGF, declPos) // may be nil^4 // Don't duplicate comments. spec1, ok := spec.(*ast.TypeSpec) if !ok { @@ -811,12 +821,9 @@ // (that is not a type parameter or a built-in). // This should be impossible even for ill-formed trees; // we suspect that AST repair may be creating inconsistent - // positions. Don't report a bug in that case. (#64241) - errorf := fmt.Errorf - if !declPGF.Fixed() { - errorf = bug.Errorf - } - return "", nil, errorf("type name %q without type spec", name) + // positions, but have struggled to reproduce the problem (#64241), + // so we'll just return an error here without recording a bug. + return "", nil, fmt.Errorf("cannot locate TypeSpec for declaration of type %q", name) } spec2 := *spec1 spec2.Doc = nil @@ -1224,27 +1231,6 @@ return rng, res, nil } -// inferredSignatureString is a wrapper around the types.ObjectString function -// that adds more information to inferred signatures. It will return an empty string -// if the passed types.Object is not a signature. -func inferredSignatureString(obj types.Object, qual types.Qualifier, inferred *types.Signature) string { - // If the signature type was inferred, prefer the inferred signature with a - // comment showing the generic signature. - if sig, _ := obj.Type().Underlying().(*types.Signature); sig != nil && sig.TypeParams().Len() > 0 && inferred != nil { - obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred) - str := types.ObjectString(obj2, qual) - // Try to avoid overly long lines. - if len(str) > 60 { - str += "\n" - } else { - str += " " - } - str += "// " + types.TypeString(sig, qual) - return str - } - return "" -} - // objectString is a wrapper around the types.ObjectString function. // It handles adding more information to the object string. // If spec is non-nil, it may be used to format additional declaration @@ -1358,14 +1344,14 @@ return nil, fmt.Errorf("re-parsing: %v", err) } - decl, spec, field, assign := findDeclInfo([]*ast.File{pgf.File}, pos) + decl, spec, field, assign := findDeclInfo(pgf, pos) return chooseDocComment(pgf, decl, spec, field, assign), nil } // chooseDocComment returns the best doc comment for the given declaration // information. func chooseDocComment(pgf *parsego.File, decl ast.Decl, spec ast.Spec, field *ast.Field, assign *ast.AssignStmt) *ast.CommentGroup { - if assign != nil { + if assign != nil && len(assign.Lhs) == 1 { // AssignStmt lacks a Doc field; locate the comment on the line above. tokFile := pgf.Tok compare := func(cg *ast.CommentGroup, line int) int { @@ -1668,13 +1654,13 @@ } } -// findDeclInfo returns the syntax nodes involved in the declaration of the -// types.Object with position pos, searching the given list of file syntax -// trees. +// findDeclInfo returns the syntax nodes involved in the declaration +// of the types.Object with position pos, searching the ancestors of +// the node at that position. // -// Pos may be the position of the name-defining identifier in an AssignStmt, -// FuncDecl, ValueSpec, TypeSpec, Field, or as a special case the position of -// Ellipsis.Elt in an ellipsis field. +// Pos may be the position of any name-defining identifier in an AssignStmt, +// FuncDecl, ValueSpec, TypeSpec, Field, or as special cases the position of +// Ellipsis.Elt in an ellipsis field, or the ImportSpec.Path of an import. // // If found, the resulting decl, spec, field and assign will be the inner-most // instance of each node type surrounding pos. @@ -1687,117 +1673,79 @@ // // It returns a nil decl if no object-defining node is found at pos. // -// TODO(rfindley): this function has tricky semantics, and may be worth unit -// testing and/or refactoring. -func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field, assign *ast.AssignStmt) { - found := false - - // Visit the files in search of the node at pos. - stack := make([]ast.Node, 0, 20) - - // Allocate the closure once, outside the loop. - f := func(n ast.Node, stack []ast.Node) bool { - if found { - return false - } - - // Skip subtrees (incl. files) that don't contain the search point. - if !(n.Pos() <= pos && pos < n.End()) { - return false - } - - switch n := n.(type) { - case *ast.AssignStmt: - if len(n.Lhs) != 1 { - return false - } - lhs := n.Lhs[0] - if lhs.Pos() == pos { - assign = n - found = true - return false - } - - case *ast.Field: - findEnclosingDeclAndSpec := func() { - for _, n := range slices.Backward(stack) { - switch n := n.(type) { - case ast.Spec: - spec = n - case ast.Decl: - decl = n - return - } - } - } - - // Check each field name since you can have - // multiple names for the same type expression. - for _, id := range n.Names { - if id.Pos() == pos { - field = n - findEnclosingDeclAndSpec() - found = true - return false - } - } - - // Check *ast.Field itself. This handles embedded - // fields which have no associated *ast.Ident name. - if n.Pos() == pos { - field = n - findEnclosingDeclAndSpec() - found = true - return false - } - - // Also check "X" in "...X". This makes it easy to format variadic - // signature params properly. - // - // TODO(rfindley): I don't understand this comment. How does finding the - // field in this case make it easier to format variadic signature params? - if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil && ell.Elt.Pos() == pos { - field = n - findEnclosingDeclAndSpec() - found = true - return false - } - - case *ast.FuncDecl: - if n.Name.Pos() == pos { - decl = n - found = true - return false - } - - case *ast.GenDecl: - for _, s := range n.Specs { - switch s := s.(type) { - case *ast.TypeSpec: - if s.Name.Pos() == pos { - decl = n - spec = s - found = true - return false - } - case *ast.ValueSpec: - for _, id := range s.Names { - if id.Pos() == pos { - decl = n - spec = s - found = true - return false - } - } - } - } - } - return true +// TODO(adonovan): this abstraction is frankly weird. Now with +// Cursor.Parent (the ability to ascend the tree) and Var.Kind (the +// ability to discriminate all object kinds) it should be possible to +// write clearer direct code in most cases. But it least this logic +// now has tests: see TestFindDeclInfo. +func findDeclInfo(pgf *parsego.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field, assign *ast.AssignStmt) { + cur, ok := pgf.Cursor().FindByPos(pos, pos) + if !ok { + return nil, nil, nil, nil } - for _, file := range files { - ast.PreorderStack(file, stack, f) - if found { - return decl, spec, field, assign + + for p := cur; p.Node() != nil; p = p.Parent() { + switch p.ParentEdgeKind() { + case edge.FuncDecl_Name: + // func f() + // ^ + decl = p.Parent().Node().(*ast.FuncDecl) + return decl, nil, nil, nil + + case edge.TypeSpec_Name, edge.ValueSpec_Names, edge.ImportSpec_Name, edge.ImportSpec_Path: + // import "..." + // import p "..." + // type T ... + // var v ... + // ^ + spec = p.Parent().Node().(ast.Spec) + decl = p.Parent().Parent().Node().(*ast.GenDecl) + return decl, spec, nil, nil + + case edge.AssignStmt_Lhs: + // v := ... + // ^ + assign = p.Parent().Node().(*ast.AssignStmt) + return nil, nil, nil, assign + + case edge.Field_Names: + // func f[T any]() + // func(a, b, c int) + // func() (d int) + // func (r T) f() + // type T struct{ f int } + // type T[P any] struct{} + // ^ + field = p.Parent().Node().(*ast.Field) + + case edge.Field_Type: + // func(T) (param Var) + // func() T (result Var + // type T struct{ T } (field Var) + // type T interface{ f() } (method Func) + // ^ + f := p.Parent().Node().(*ast.Field) + if len(f.Names) == 0 { + field = f + } + } + if field != nil { + break + } + } + + if field != nil { + for p := cur.Parent(); p.Node() != nil; p = p.Parent() { + switch n := p.Node().(type) { + case *ast.TypeSpec: + spec = n + case *ast.FuncDecl: + decl = n + return decl, spec, field, nil + case *ast.GenDecl: + decl = n + return decl, spec, field, nil + } } }
diff --git a/gopls/internal/golang/hover_test.go b/gopls/internal/golang/hover_test.go index 3d55bfe..0990e1a 100644 --- a/gopls/internal/golang/hover_test.go +++ b/gopls/internal/golang/hover_test.go
@@ -4,7 +4,17 @@ package golang -import "testing" +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "reflect" + "strings" + "testing" + + "golang.org/x/tools/gopls/internal/cache/parsego" +) func TestSizeClass(t *testing.T) { // See GOROOT/src/runtime/msize.go for details. @@ -20,3 +30,80 @@ } } } + +func TestFindDeclInfo(t *testing.T) { + // Each comment names the types of each non-nil component of + // the 4-tuple returned by findDeclInfo at that point. + for _, src := range []string{ + `func /*FuncDecl,-,-,-*/F() {}`, // FuncDecl_Name + `type /*GenDecl,TypeSpec,-,-*/T struct{}`, // TypeSpec_Name + `var /*GenDecl,ValueSpec,-,-*/x int`, // ValueSpec_Names (simple var) + `const /*GenDecl,ValueSpec,-,-*/C = 1`, // ValueSpec_Names (const) + `var a, /*GenDecl,ValueSpec,-,-*/b int`, // ValueSpec_Names (multi var) + `func f() { /*-,-,-,AssignStmt*/y := 1 }`, // AssignStmt_Lhs + `func f() { /*-,-,-,AssignStmt*/x, y := 1, 2 }`, // AssignStmt_Lhs (first of two vars) + `func f() { x, /*-,-,-,AssignStmt*/y := 1, 2 }`, // AssignStmt_Lhs (second of two vars) + `type T struct { /*GenDecl,TypeSpec,Field,-*/f int }`, // Field_Names (struct field) + `func F(/*FuncDecl,-,Field,-*/p int)`, // Field_Names (parameter) + `func F(a, /*FuncDecl,-,Field,-*/b, c int)`, // Field_Names (one of many params) + `func F() (/*FuncDecl,-,Field,-*/result int)`, // Field_Names (result) + `func F() (x, /*FuncDecl,-,Field,-*/y int)`, // Field_Names (one of many results) + `func (/*FuncDecl,-,Field,-*/r int) M() {}`, // Field_Names (receiver) + `type I interface { /*GenDecl,TypeSpec,Field,-*/f() }`, // Field_Names (interface method) + `type T struct { /*GenDecl,TypeSpec,Field,-*/S }; type S struct{}`, // Field_Type (anon struct field) + `type T struct { */*GenDecl,TypeSpec,Field,-*/S }; type S struct{}`, // Field_Type (anon struct field pointer) + `import "pkg"; type T struct { pkg. /*GenDecl,TypeSpec,Field,-*/S }`, // Field_Type (anon struct field selector) + `type T[X any] struct { /*GenDecl,TypeSpec,Field,-*/S[X] }; type S[Y any] struct{}`, // Field_Type (anon generic struct field) + `type T[X any] struct { */*GenDecl,TypeSpec,Field,-*/S[X] }; type S[Y any] struct{}`, // Field_Type (anon generic struct field pointer) + `import "pkg"; type T[X any] struct { pkg. /*GenDecl,TypeSpec,Field,-*/S[X] }`, // Field_Type (anon generic struct field selector) + `import "pkg"; type T[X, Y any] struct { pkg. /*GenDecl,TypeSpec,Field,-*/S[X, Y] }`, // Field_Type (anon generic struct field multiple type args) + `import "pkg"; type T[X, Y any] struct { *pkg. /*GenDecl,TypeSpec,Field,-*/S[X, Y] }`, // Field_Type (anon generic struct field pointer multiple type args) + `import /*GenDecl,ImportSpec,-,-*/"pkg"`, // ImportSpec_Path + `import /*GenDecl,ImportSpec,-,-*/p "pkg"`, // ImportSpec_Name + `import p /*GenDecl,ImportSpec,-,-*/"pkg"`, // ImportSpec_Path, with name + `func F(/*FuncDecl,-,Field,-*/int) {}`, // Field_Type (anon parameter) + `func F() /*FuncDecl,-,Field,-*/int {}`, // Field_Type (anon result) + `type T interface { /*GenDecl,TypeSpec,Field,-*/error }`, // Field_Type (interface embedding) + `func F(/*FuncDecl,-,Field,-*/...int) {}`, // Field_Type (anon variadic parameter, pos at ...) + `func F(... /*FuncDecl,-,Field,-*/int) {}`, // Field_Type (anon variadic parameter, pos at Elt) + `func F(/*FuncDecl,-,Field,-*/args ...int) {}`, // Field_Names (named variadic parameter) + `func F(args /*-,-,-,-*/...int) {}`, // pos at ... of named variadic parameter + `func F(args ... /*-,-,-,-*/int) {}`, // pos at Elt of named variadic parameter + `func F[/*FuncDecl,-,Field,-*/T any]() {}`, // Field_Names (type parameter of func) + `type T[/*GenDecl,TypeSpec,Field,-*/P any] struct{}`, // Field_Names (type parameter of type) + `type I interface { f(/*GenDecl,TypeSpec,Field,-*/int) }`, // Field_Type (parameter of interface method) + `var ( a = 1; /*GenDecl,ValueSpec,-,-*/b = 2 )`, // ValueSpec_Names in var block + } { + t.Run("", func(t *testing.T) { + src := "package p; " + src + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "test.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + if len(file.Comments) != 1 { + t.Fatalf("got %d File.Comments, want exactly 1", len(file.Comments)) + } + comment := file.Comments[0] + + pgf := &parsego.File{ + File: file, + Tok: fset.File(file.Pos()), + } + decl, spec, field, assign := findDeclInfo(pgf, comment.End()) + + format := func(n ast.Node) string { + if n == nil || reflect.ValueOf(n).IsNil() { + return "-" + } + return strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.") + } + + got := fmt.Sprintf("%s,%s,%s,%s", format(decl), format(spec), format(field), format(assign)) + want := strings.TrimSpace(comment.Text()) + if got != want { + t.Errorf("%s:\nfindDeclInfo = %s, want %s", src, got, want) + } + }) + } +}
diff --git a/gopls/internal/golang/identifier.go b/gopls/internal/golang/identifier.go index 569a501..6c9c179 100644 --- a/gopls/internal/golang/identifier.go +++ b/gopls/internal/golang/identifier.go
@@ -17,16 +17,6 @@ // ErrNoIdentFound is error returned when no identifier is found at a particular position var ErrNoIdentFound = errors.New("no identifier found") -// inferredSignature determines the resolved non-generic signature for an -// identifier in an instantiation expression. -// -// If no such signature exists, it returns nil. -func inferredSignature(info *types.Info, id *ast.Ident) *types.Signature { - inst := info.Instances[id] - sig, _ := types.Unalias(inst.Type).(*types.Signature) - return sig -} - // searchForEnclosing returns, given the AST path to a SelectorExpr, // the exported named type of the innermost implicit field selection. //
diff --git a/gopls/internal/golang/implement_interface.go b/gopls/internal/golang/implement_interface.go index 1aa5980..91e3c7e 100644 --- a/gopls/internal/golang/implement_interface.go +++ b/gopls/internal/golang/implement_interface.go
@@ -173,7 +173,7 @@ // var _ Interface = (*Type)(nil) si := stubmethods.IfaceStubInfo{ Fset: pkg.FileSet(), - Interface: iface, + Interface: iface.Type(), Concrete: named, // TODO(hxjiang): consider make it question and let the user decide // whether to use pointer receiver or not.
diff --git a/gopls/internal/golang/implementation.go b/gopls/internal/golang/implementation.go index 49963e5..220ba0a 100644 --- a/gopls/internal/golang/implementation.go +++ b/gopls/internal/golang/implementation.go
@@ -548,11 +548,23 @@ for method := range ymset.Methods() { ym := method.Obj().(*types.Func) + // Ignore generic methods, for safety. + // This is probably unnecessary since they cannot + // appear in a true interface type. + // (They can appear in a constraint interface type, + // but we shouldn't be called in that case.) + if ym.Signature().TypeParams().Len() > 0 { + continue + } + xobj, _, _ := types.LookupFieldOrMethod(x, false, ym.Pkg(), ym.Name()) xm, ok := xobj.(*types.Func) if !ok { return false // x lacks a method of y } + if xm.Signature().TypeParams().Len() > 0 { + return false // generic methods do not satisfy interface methods + } if !unify(xm.Signature(), ym.Signature(), nil) { return false // signatures do not match }
diff --git a/gopls/internal/golang/inlay_hint.go b/gopls/internal/golang/inlay_hint.go index 9e23e76..a9e7027 100644 --- a/gopls/internal/golang/inlay_hint.go +++ b/gopls/internal/golang/inlay_hint.go
@@ -166,7 +166,8 @@ if typesinternal.IsFunctionNamed(obj, "fmt", "Print", "Printf", "Println", "Fprint", "Fprintf", "Fprintln") || typesinternal.IsMethodNamed(obj, "bytes", "Buffer", "Write", "WriteByte", "WriteRune", "WriteString") || typesinternal.IsMethodNamed(obj, "strings", "Builder", "Write", "WriteByte", "WriteRune", "WriteString") || - typesinternal.IsFunctionNamed(obj, "io", "WriteString") { + typesinternal.IsFunctionNamed(obj, "io", "WriteString") || + typesinternal.IsMethodNamed(obj, "hash/maphash", "Hash", "Write", "WriteByte", "WriteString") { continue } @@ -199,8 +200,14 @@ func funcTypeParams(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur inspector.Cursor, add func(protocol.InlayHint)) { for curCall := range cur.Preorder((*ast.CallExpr)(nil)) { call := curCall.Node().(*ast.CallExpr) - id, ok := call.Fun.(*ast.Ident) - if !ok { + var id *ast.Ident + switch fun := call.Fun.(type) { + case *ast.Ident: + id = fun + case *ast.SelectorExpr: // imported function + id = fun.Sel + } + if id == nil { continue } inst := info.Instances[id] @@ -213,7 +220,7 @@ } var args []string for t := range inst.TypeArgs.Types() { - args = append(args, t.String()) + args = append(args, types.TypeString(t, qual)) } if len(args) == 0 { continue @@ -227,31 +234,18 @@ } func assignVariableTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur inspector.Cursor, add func(protocol.InlayHint)) { - for node := range cur.Preorder((*ast.AssignStmt)(nil), (*ast.ValueSpec)(nil)) { - switch n := node.Node().(type) { + for cur := range cur.Preorder((*ast.AssignStmt)(nil), (*ast.ValueSpec)(nil)) { + switch node := cur.Node().(type) { case *ast.AssignStmt: - if n.Tok != token.DEFINE { - continue - } - for _, v := range n.Lhs { - variableType(info, pgf, qual, v, add) - } - case *ast.GenDecl: - if n.Tok != token.VAR { - continue - } - for _, v := range n.Specs { - spec := v.(*ast.ValueSpec) - // The type of the variable is written, skip showing type of this var. - // ```go - // var foo string - // ``` - if spec.Type != nil { - continue + if node.Tok == token.DEFINE { + for _, lhs := range node.Lhs { + variableType(info, pgf, qual, lhs, add) } - - for _, v := range spec.Names { - variableType(info, pgf, qual, v, add) + } + case *ast.ValueSpec: + if cur.Parent().Node().(*ast.GenDecl).Tok == token.VAR && node.Type == nil { // type not already explicit + for _, id := range node.Names { + variableType(info, pgf, qual, id, add) } } } @@ -336,43 +330,71 @@ func compositeLiteralFields(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur inspector.Cursor, add func(protocol.InlayHint)) { for curCompLit := range cur.Preorder((*ast.CompositeLit)(nil)) { - compLit, ok := curCompLit.Node().(*ast.CompositeLit) - if !ok { - continue - } + compLit := curCompLit.Node().(*ast.CompositeLit) typ := info.TypeOf(compLit) if typ == nil { continue } - typ = typesinternal.Unpointer(typ) - strct, ok := typeparams.CoreType(typ).(*types.Struct) + strct, ok := typeparams.CoreType(typesinternal.Unpointer(typ)).(*types.Struct) if !ok { continue } - var hints []protocol.InlayHint - var allEdits []protocol.TextEdit + var ( + hints []protocol.InlayHint + allEdits []protocol.TextEdit + label strings.Builder + ) for i, v := range compLit.Elts { - if _, ok := v.(*ast.KeyValueExpr); !ok { - start, err := pgf.PosPosition(v.Pos()) - if err != nil { - continue + label.Reset() + pad := false + if kv, ok := v.(*ast.KeyValueExpr); ok { + // keyed field: show implicit field selections + if id, ok := kv.Key.(*ast.Ident); ok { + // For some reason an inlayHintFunc + // doesn't get the current types.Package. + // Use the package of the explicit field. + var pkg *types.Package + if obj, ok := info.Uses[id]; ok { + pkg = obj.Pkg() + } + + if seln, ok := types.LookupSelection(strct, true, pkg, id.Name); ok { + for field := range typesinternal.ImplicitFieldSelections(seln) { + label.WriteString(field.Name()) + label.WriteByte('.') + } + } } - if i > strct.NumFields()-1 { - break + } else { + // unkeyed field: show key based on index + if i < strct.NumFields() { + label.WriteString(strct.Field(i).Name()) + label.WriteByte(':') + pad = true } - hints = append(hints, protocol.InlayHint{ - Position: start, - Label: labelPart(strct.Field(i).Name() + ":"), - Kind: protocol.Parameter, - PaddingRight: true, - }) - allEdits = append(allEdits, protocol.TextEdit{ - Range: protocol.Range{Start: start, End: start}, - NewText: strct.Field(i).Name() + ": ", - }) } + if label.Len() == 0 { + continue + } + labelStr := label.String() + + start, err := pgf.PosPosition(v.Pos()) + if err != nil { + continue + } + hints = append(hints, protocol.InlayHint{ + Position: start, + Label: labelPart(labelStr), + Kind: protocol.Parameter, + PaddingRight: pad, + }) + allEdits = append(allEdits, protocol.TextEdit{ + Range: protocol.Range{Start: start, End: start}, + NewText: labelStr, + }) } + // It is not allowed to have a mix of keyed and unkeyed fields, so // have the text edits add keys to all fields. for i := range hints {
diff --git a/gopls/internal/golang/inline_all.go b/gopls/internal/golang/inline_all.go index 9b8697f..c7a1dbb 100644 --- a/gopls/internal/golang/inline_all.go +++ b/gopls/internal/golang/inline_all.go
@@ -17,6 +17,7 @@ "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/refactor" @@ -87,10 +88,7 @@ pkgForRef[ref] = md.ID needPkgs[md.ID] = struct{}{} } - var pkgIDs []PackageID - for id := range needPkgs { // TODO: use maps.Keys once it is available to us - pkgIDs = append(pkgIDs, id) - } + pkgIDs := moremaps.KeySlice(needPkgs) refPkgs, err := snapshot.TypeCheck(ctx, pkgIDs...) if err != nil {
diff --git a/gopls/internal/golang/movetype.go b/gopls/internal/golang/movetype.go index 8aefc9f..80efa79 100644 --- a/gopls/internal/golang/movetype.go +++ b/gopls/internal/golang/movetype.go
@@ -5,41 +5,193 @@ package golang import ( + "bytes" "context" "fmt" "go/ast" + "go/format" + "go/printer" "go/token" + "strconv" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/moreiters" + "golang.org/x/tools/gopls/internal/util/cursorutil" + "golang.org/x/tools/internal/astutil" + "golang.org/x/tools/internal/refactor" ) -// MoveType moves the selected type declaration into a new package and updates all references. -func MoveType(ctx context.Context, fh file.Handle, snapshot *cache.Snapshot, loc protocol.Location, newPkgDir string) ([]protocol.DocumentChange, error) { - return nil, fmt.Errorf("MoveType: not yet supported") +// MoveType moves the selected type declaration into the given file, which must already exist. +func MoveType(ctx context.Context, fh file.Handle, snapshot *cache.Snapshot, loc protocol.Location, destURI protocol.DocumentURI) ([]protocol.DocumentChange, protocol.Location, error) { + curPkg, curPGF, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) + if err != nil { + return nil, protocol.Location{}, err + } + destPkg, destPGF, err := NarrowestPackageForFile(ctx, snapshot, destURI) + if err != nil { + // TODO(mkalil): Handle move type to new file. + return nil, protocol.Location{}, err + } + + var ( + spec *ast.TypeSpec + decl *ast.GenDecl + ) + { + start, end, err := curPGF.RangePos(loc.Range) + if err != nil { + return nil, protocol.Location{}, err + } + + curSel, ok := curPGF.Cursor().FindByPos(start, end) + if !ok { + return nil, protocol.Location{}, err + } + specCur, ok := selectionContainsTypeSpec(curSel) + if !ok { + return nil, protocol.Location{}, fmt.Errorf("no type spec at cursor") + } + spec = specCur.Node().(*ast.TypeSpec) // can't fail + decl = specCur.Parent().Node().(*ast.GenDecl) + } + // TODO(mkalil): check if type move is legal. + + // Capture floating comments so they can be moved to the + // new file along with the type spec. + var comments []*ast.CommentGroup + { + var enclosed ast.Node + // If the moving type spec is the only one in the decl, we want comments + // enclosed by the decl, otherwise we want comments enclosed by just the + // individual type spec. + if len(decl.Specs) == 1 { + enclosed = decl + } else { + enclosed = spec + } + for _, comment := range curPGF.File.Comments { + if astutil.NodeContains(enclosed, astutil.NodeRange(comment)) { + comments = append(comments, comment) + } + } + } + + changes, destRng, err := addTypeToFile(ctx, snapshot, curPkg, destPkg, curPGF, destPGF, spec, comments) + if err != nil { + return nil, protocol.Location{}, err + } + // Get the range to delete the type from its current location. If the type spec + // in question is the only spec in the decl, delete the entire decl + // including any comments. Otherwise, just delete the type spec. + var n ast.Node = spec + if len(decl.Specs) == 1 { + n = decl // delete entire decl + } + typStart, typEnd := n.Pos(), n.End() + if doc := astutil.DocComment(n); doc != nil { + typStart = doc.Pos() // include doc comment in deletion range + } + rng, err := curPGF.PosRange(typStart, typEnd+1) // include probable newline + if err != nil { + return nil, protocol.Location{}, err + } + + changes = append(changes, protocol.DocumentChangeEdit(fh, []protocol.TextEdit{ + {Range: rng}, + })) + return changes, protocol.Location{URI: destURI, Range: destRng}, nil } -// selectionContainsType returns the Cursor, GenDecl and TypeSpec of the type -// declaration that encloses cursor if one exists. Otherwise it returns false. -func selectionContainsType(cursor inspector.Cursor) (inspector.Cursor, *ast.GenDecl, *ast.TypeSpec, string, bool) { - declCur, ok := moreiters.First(cursor.Enclosing((*ast.GenDecl)(nil))) - if !ok { - return inspector.Cursor{}, &ast.GenDecl{}, &ast.TypeSpec{}, "", false +// selectionContainsTypeSpec returns the [inspector.Cursor] of the type declaration that +// encloses cur if one exists. Otherwise it returns false. +func selectionContainsTypeSpec(cur inspector.Cursor) (inspector.Cursor, bool) { + spec, curSpec := cursorutil.FirstEnclosing[*ast.TypeSpec](cur) + if spec == nil { + return inspector.Cursor{}, false } - + declNode := curSpec.Parent().Node().(*ast.GenDecl) // Verify that we have a type declaration (e.g. not an import declaration). - declNode := declCur.Node().(*ast.GenDecl) if declNode.Tok != token.TYPE { - return inspector.Cursor{}, &ast.GenDecl{}, &ast.TypeSpec{}, "", false + return inspector.Cursor{}, false + } + return curSpec, true +} + +// addTypeToFile returns the necessary document changes to add the type spec to +// the end of the given existing file. +// It also returns the [protocol.Range] where the type is added. +func addTypeToFile(ctx context.Context, snapshot *cache.Snapshot, curPkg, destPkg *cache.Package, curPGF, destPGF *parsego.File, spec *ast.TypeSpec, comments []*ast.CommentGroup) ([]protocol.DocumentChange, protocol.Range, error) { + // Ensure comments are formatted along with the type spec. + var typSpecBuf bytes.Buffer + { + commentedNode := &printer.CommentedNode{ + Node: &ast.GenDecl{ + Tok: token.TYPE, + Specs: []ast.Spec{spec}, + }, + Comments: comments, + } + err := format.Node(&typSpecBuf, curPkg.FileSet(), commentedNode) + if err != nil { + return nil, protocol.Range{}, fmt.Errorf("error formatting type decl: %v", err) + } + typSpecBuf.WriteString("\n") + } + // Calculate imports to add to the destination file. + adds, deletes, err := findImportEdits(curPGF.File, curPkg.TypesInfo(), spec.Pos(), spec.End()) + if err != nil { + return nil, protocol.Range{}, err + } + var addImportEdits []protocol.TextEdit + { + + for _, importSpec := range adds { + path, err := strconv.Unquote(importSpec.Path.Value) + if err != nil { + return nil, protocol.Range{}, err + } + name := "" + if importSpec.Name != nil { + name = importSpec.Name.Name + } + _, impEdits := refactor.AddImport(destPkg.TypesInfo(), destPGF.File, name, path, "", destPGF.File.FileEnd-1) + for _, edit := range impEdits { + editRng, err := destPGF.PosRange(edit.Pos, edit.End) + if err != nil { + return nil, protocol.Range{}, err + } + addImportEdits = append(addImportEdits, protocol.TextEdit{ + Range: editRng, + NewText: string(edit.NewText), + }) + } + } } - typSpec, ok := declNode.Specs[0].(*ast.TypeSpec) - if !ok { - return inspector.Cursor{}, &ast.GenDecl{}, &ast.TypeSpec{}, "", false - } + // Imports that are now unused and can be removed from the current file. + deleteImportEdits := importDeletesEdits(curPGF, deletes) - return declCur, declNode, declNode.Specs[0].(*ast.TypeSpec), typSpec.Name.Name, true + // Add the type spec to the end of the file. + destRng, err := destPGF.PosRange(destPGF.File.FileEnd, destPGF.File.FileEnd) + if err != nil { + return nil, protocol.Range{}, err + } + destFH, err := snapshot.ReadFile(ctx, destPGF.URI) + if err != nil { + return nil, protocol.Range{}, err + } + curFH, err := snapshot.ReadFile(ctx, curPGF.URI) + if err != nil { + return nil, protocol.Range{}, err + } + return []protocol.DocumentChange{ + protocol.DocumentChangeEdit(destFH, + append(addImportEdits, []protocol.TextEdit{ + {Range: destRng, NewText: typSpecBuf.String()}, + }...)), + protocol.DocumentChangeEdit(curFH, deleteImportEdits), + }, destRng, nil }
diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index d77a432..9e04f8d 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go
@@ -283,7 +283,7 @@ // renameFuncSignature computes and applies the effective change signature // operation resulting from a 'renamed' (=rewritten) signature. -func renameFuncSignature(ctx context.Context, pkg *cache.Package, pgf *parsego.File, start, end token.Pos, snapshot *cache.Snapshot, cursor inspector.Cursor, f file.Handle, rng protocol.Range, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, error) { +func renameFuncSignature(ctx context.Context, pkg *cache.Package, pgf *parsego.File, start, end token.Pos, snapshot *cache.Snapshot, cursor inspector.Cursor, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, error) { fdecl := funcKeywordDecl(start, end, cursor) if fdecl == nil { return nil, nil @@ -428,7 +428,7 @@ return nil, fmt.Errorf("can't find cursor for selection") } - if edits, err := renameFuncSignature(ctx, pkg, pgf, start, end, snapshot, cur, f, rng, newName); err != nil { + if edits, err := renameFuncSignature(ctx, pkg, pgf, start, end, snapshot, cur, newName); err != nil { return nil, err } else if edits != nil { return editsToDocChanges(ctx, snapshot, edits)
diff --git a/gopls/internal/golang/resolve.go b/gopls/internal/golang/resolve.go index e018599..c4ad48c 100644 --- a/gopls/internal/golang/resolve.go +++ b/gopls/internal/golang/resolve.go
@@ -48,11 +48,14 @@ var addTagsForm = []protocol.FormField{ { + ID: "tags", Description: `comma-separated list of tags to add; e.g.. "json,xml"`, Type: protocol.FormFieldTypeString{Kind: "string"}, + Required: true, Default: "json", }, { + ID: "transform", Description: `transform rule for added tags, e.g., "camelcase' or 'snakecase"`, Type: protocol.FormFieldTypeEnum{ Kind: "enum", @@ -79,14 +82,17 @@ }, }, }, - Default: "camelcase", + Required: true, + Default: "camelcase", }, } var removeTagsForm = []protocol.FormField{ { + ID: "tags", Description: `comma-separated list of tags to remove; e.g., "json,xml"`, Type: protocol.FormFieldTypeString{Kind: "string"}, + Required: true, Default: "json", // TODO(?): put the existing tags here? }, } @@ -130,8 +136,7 @@ return nil } - // User parameter 0. - v0, err := FormAnswer[string](¶m.InteractiveParams, 0) + v0, err := FormAnswer[string](¶m.InteractiveParams, "tags") if err != nil { return err } @@ -142,8 +147,7 @@ return nil } - // User parameter 1. - _, err = FormAnswer[string](¶m.InteractiveParams, 1) + _, err = FormAnswer[string](¶m.InteractiveParams, "transform") if err != nil { return err } @@ -163,7 +167,7 @@ return nil } - v, err := FormAnswer[string](¶m.InteractiveParams, 0) + v, err := FormAnswer[string](¶m.InteractiveParams, "tags") if err != nil { return err } @@ -191,6 +195,7 @@ var implementInterfaceFormLazyEnum = []protocol.FormField{ { + ID: "interface", Description: `fully qualified interface identifier path/to/pkg.interface; e.g., "net.Error"`, Type: protocol.FormFieldTypeLazyEnum{ Kind: "lazyEnum", @@ -199,17 +204,20 @@ Kinds: []protocol.SymbolKind{protocol.Interface}, }), }, - Default: "error", + Required: true, + Default: "error", }, } var implementInterfaceFormString = []protocol.FormField{ { + ID: "interface", Description: `fully qualified interface identifier path/to/pkg.interface; e.g., "net.Error"`, Type: protocol.FormFieldTypeString{ Kind: "string", }, - Default: "error", + Required: true, + Default: "error", }, } @@ -236,7 +244,7 @@ return nil } - v, err := FormAnswer[string](¶m.InteractiveParams, 0) + v, err := FormAnswer[string](¶m.InteractiveParams, "interface") if err != nil { return err } @@ -277,16 +285,26 @@ return nil } -func FormAnswer[T any](params *protocol.InteractiveParams, index int) (v T, err error) { - if len(params.FormAnswers) <= index { - return v, fmt.Errorf("truncated FormAnswers: got %d items, want at least %d", len(params.FormAnswers), index+1) +// FormAnswer finds, validates, and returns the unique answer for id. +// +// It uses a linear scan since the number of answers is small (usually < 5). +func FormAnswer[T any](params *protocol.InteractiveParams, id string) (v T, err error) { + matches := 0 + for _, ans := range params.FormAnswers { + if ans.ID == id { + matches++ + val, ok := ans.Value.(T) + if !ok { + return v, fmt.Errorf("form answer %q has unexpected type %T, want %T", id, ans.Value, v) + } + v = val + } } - - v, ok := params.FormAnswers[index].(T) - if !ok { - return v, fmt.Errorf("invalid type at index %d, want %T: got %T", index, *new(T), params.FormAnswers[index]) + if matches == 0 { + return v, fmt.Errorf("form lacks answer %q", id) + } else if matches > 1 { + return v, fmt.Errorf("form contains duplicate answer %q", id) } - return v, nil }
diff --git a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go index 4d0a835..e87a387 100644 --- a/gopls/internal/golang/semtok.go +++ b/gopls/internal/golang/semtok.go
@@ -982,7 +982,7 @@ func (tv *tokenVisitor) godirective(c *ast.Comment) { // First check if '//go:directive args...' is a valid directive. directive, args, _ := strings.Cut(c.Text, " ") - kind, _ := stringsCutPrefix(directive, "//go:") + kind, _ := strings.CutPrefix(directive, "//go:") if _, ok := godirectives[kind]; !ok { // Unknown 'go:' directive. tv.token(c.Pos(), len(c.Text), semtok.TokComment) @@ -1001,14 +1001,6 @@ } } -// Go 1.20 strings.CutPrefix. -func stringsCutPrefix(s, prefix string) (after string, found bool) { - if !strings.HasPrefix(s, prefix) { - return s, false - } - return s[len(prefix):], true -} - func is[T any](x any) bool { _, ok := x.(T) return ok
diff --git a/gopls/internal/golang/stubmethods/stubmethods.go b/gopls/internal/golang/stubmethods/stubmethods.go index 3a2db8f..305660f 100644 --- a/gopls/internal/golang/stubmethods/stubmethods.go +++ b/gopls/internal/golang/stubmethods/stubmethods.go
@@ -17,28 +17,19 @@ "golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/util/typesutil" ) -// TODO(adonovan): eliminate the confusing Fset parameter; only the -// file name and byte offset of Concrete are needed. - // IfaceStubInfo represents a concrete type // that wants to stub out an interface type type IfaceStubInfo struct { - // Interface is the interface that the client wants to implement. - // When the interface is defined, the underlying object will be a TypeName. - // Note that we keep track of types.Object instead of types.Type in order - // to keep a reference to the declaring object's package and the ast file - // in the case where the concrete type file requires a new import that happens to be renamed - // in the interface file. - // TODO(marwan-at-work): implement interface literals. - Fset *token.FileSet // the FileSet used to type-check the types below - Interface *types.TypeName - Concrete typesinternal.NamedOrAlias + Fset *token.FileSet + Interface types.Type // interface type to implement (may be unnamed, named, or even instantiated) + Concrete typesinternal.NamedOrAlias // concrete type on which to declare methods Pointer bool } @@ -92,13 +83,9 @@ } } - var ( - missing []*types.Func - // Find subset of interface methods that the concrete type lacks. - ifaceType = si.Interface.Type().Underlying().(*types.Interface) - ) - - for imethod := range ifaceType.Methods() { + // Find subset of interface methods that the concrete type lacks. + var missing []*types.Func + for imethod := range si.Interface.Underlying().(*types.Interface).Methods() { cmethod, index, _ := types.LookupFieldOrMethod(si.Concrete, si.Pointer, imethod.Pkg(), imethod.Name()) if cmethod == nil { missing = append(missing, imethod) @@ -137,10 +124,15 @@ return fmt.Errorf("no missing methods found") } - // Format interface name (used only in a comment). - iface := si.Interface.Name() - if ipkg := si.Interface.Pkg(); ipkg != nil && ipkg != conc.Pkg() { - iface = ipkg.Name() + "." + iface + // Format doc link to interface type for "implements" doc comment. + ifaceLink := "an anonymous interface" + // (I don't know why we unalias here, other than issue65024.txt wants it.) + if named, ok := types.Unalias(si.Interface).(*types.Named); ok { + ifaceLink = named.Obj().Name() + if ipkg := named.Obj().Pkg(); ipkg != nil && ipkg != conc.Pkg() { + ifaceLink = ipkg.Name() + "." + ifaceLink + } + ifaceLink = "[" + ifaceLink + "]" } // Pointer receiver? @@ -178,13 +170,13 @@ mrn = "" } - fmt.Fprintf(out, `// %s implements [%s]. + fmt.Fprintf(out, `// %s implements %s. func (%s%s%s%s) %s%s { panic("unimplemented") } `, missing[index].Name(), - iface, + ifaceLink, mrn, star, si.Concrete.Obj().Name(), @@ -207,11 +199,7 @@ if concType == nil || concType.Obj().Pkg() == nil { return nil } - tv, ok := info.Types[call.Fun] - if !ok { - return nil - } - sig, ok := types.Unalias(tv.Type).(*types.Signature) + sig, ok := types.Unalias(info.TypeOf(call.Fun)).(*types.Signature) if !ok { return nil } @@ -226,18 +214,14 @@ } else if argIdx < sig.Params().Len() { paramType = sig.Params().At(argIdx).Type() } - if paramType == nil { - return nil // A type error prevents us from determining the param type. - } - iface := ifaceObjFromType(paramType) - if iface == nil { + if !validInterface(paramType) { return nil } return &IfaceStubInfo{ Fset: fset, Concrete: concType, Pointer: pointer, - Interface: iface, + Interface: paramType, } } @@ -260,12 +244,11 @@ sig := typesutil.EnclosingSignature(curResult, info) if sig == nil { - // Either curResult is not within a function (incontheivable?), - // or the function's type information is missing (in which case - // EnclosingSignature will have called bug.Report). - // Don't report a second bug here. - // See https://go.dev/issue/70666. - return nil, fmt.Errorf("internal error: return statement lacks type information or enclosing function (issue 70666)") + // Since curResult must be within a function, this + // indicates that the function's type information is + // missing, for example because there are two + // functions with the same name. + return nil, fmt.Errorf("function enclosing return statement has incomplete type information") } rets := sig.Results() // The return operands and function results must match. @@ -276,8 +259,8 @@ len(ret.Results), rets.Len()) } - iface := ifaceObjFromType(rets.At(curResult.ParentEdgeIndex()).Type()) - if iface == nil { + iface := rets.At(curResult.ParentEdgeIndex()).Type() + if !validInterface(iface) { return nil, nil } return &IfaceStubInfo{ @@ -311,14 +294,14 @@ return nil } - ifaceObj := ifaceType(ifaceNode, info) - if ifaceObj == nil { + iface := info.TypeOf(ifaceNode) + if !validInterface(iface) { return nil } return &IfaceStubInfo{ Fset: fset, Concrete: concType, - Interface: ifaceObj, + Interface: iface, Pointer: pointer, } } @@ -334,10 +317,15 @@ // ^^^^ assign := curRhs.Parent().Node().(*ast.AssignStmt) - lhs, rhs := assign.Lhs[curRhs.ParentEdgeIndex()], curRhs.Node().(ast.Expr) + idx := curRhs.ParentEdgeIndex() + // ill-type code may have fewer LHS than RHS so guard it. + if idx >= len(assign.Lhs) { + return nil + } + lhs, rhs := assign.Lhs[idx], curRhs.Node().(ast.Expr) - ifaceObj := ifaceType(lhs, info) - if ifaceObj == nil { + iface := info.TypeOf(lhs) + if !validInterface(iface) { return nil } concType, pointer := concreteType(rhs, info) @@ -351,37 +339,11 @@ return &IfaceStubInfo{ Fset: fset, Concrete: concType, - Interface: ifaceObj, + Interface: iface, Pointer: pointer, } } -// ifaceType returns the named interface type to which e refers, if any. -func ifaceType(e ast.Expr, info *types.Info) *types.TypeName { - tv, ok := info.Types[e] - if !ok { - return nil - } - return ifaceObjFromType(tv.Type) -} - -func ifaceObjFromType(t types.Type) *types.TypeName { - named, ok := types.Unalias(t).(*types.Named) - if !ok { - return nil - } - if !types.IsInterface(named) { - return nil - } - // Interfaces defined in the "builtin" package return nil a Pkg(). - // But they are still real interfaces that we need to make a special case for. - // Therefore, protect gopls from panicking if a new interface type was added in the future. - if named.Obj().Pkg() == nil && named.Obj().Name() != "error" { - return nil - } - return named.Obj() -} - // concreteType tries to extract the *types.Named that defines // the concrete type given the ast.Expr where the "missing method" // or "conversion" errors happened. If the concrete type is something @@ -390,18 +352,22 @@ // is a boolean that indicates whether the concreteType was defined as a // pointer or value. func concreteType(e ast.Expr, info *types.Info) (*types.Named, bool) { - tv, ok := info.Types[e] - if !ok { - return nil, false - } - typ := tv.Type - ptr, isPtr := types.Unalias(typ).(*types.Pointer) + t := info.TypeOf(e) + ptr, isPtr := types.Unalias(t).(*types.Pointer) if isPtr { - typ = ptr.Elem() + t = ptr.Elem() } - named, ok := types.Unalias(typ).(*types.Named) + named, ok := types.Unalias(t).(*types.Named) if !ok { return nil, false } return named, isPtr } + +// validInterface reports whether iface is a non-nil interface +// type with no free type parameters. +func validInterface(iface types.Type) bool { + return iface != nil && + types.IsInterface(iface) && + !new(typeparams.Free).Has(iface) +}
diff --git a/gopls/internal/golang/types_format.go b/gopls/internal/golang/types_format.go index 1e50ae3..b029058 100644 --- a/gopls/internal/golang/types_format.go +++ b/gopls/internal/golang/types_format.go
@@ -305,7 +305,7 @@ return "", bug.Errorf("failed to find file %q in deps of %q", targetpgf.URI, srcpkg.Metadata().ID) } - decl, spec, field, _ := findDeclInfo([]*ast.File{targetpgf.File}, pos) + decl, spec, field, _ := findDeclInfo(targetpgf, pos) // We can't handle type parameters correctly, so we fall back on TypeString // for parameterized decls.
diff --git a/gopls/internal/licenses/licenses.go b/gopls/internal/licenses/licenses.go index 1b68bbf..f57856d 100644 --- a/gopls/internal/licenses/licenses.go +++ b/gopls/internal/licenses/licenses.go
@@ -174,36 +174,6 @@ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --- github.com/google/go-cmp LICENSE -- - -Copyright (c) 2017 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -- github.com/google/jsonschema-go LICENSE -- MIT License
diff --git a/gopls/internal/lsprpc/export_test.go b/gopls/internal/lsprpc/export_test.go index 8447d4f..eda9c56 100644 --- a/gopls/internal/lsprpc/export_test.go +++ b/gopls/internal/lsprpc/export_test.go
@@ -14,7 +14,6 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/event" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/xcontext" ) const HandshakeMethod = handshakeMethod @@ -73,7 +72,7 @@ preempter := &Canceler{ Conn: conn, } - detached := xcontext.Detach(ctx) + detached := context.WithoutCancel(ctx) go func() { conn.Wait() // ignore error if err := serverConn.Close(); err != nil {
diff --git a/gopls/internal/mcp/workspace.go b/gopls/internal/mcp/workspace.go index 59f7ab3..636ee76 100644 --- a/gopls/internal/mcp/workspace.go +++ b/gopls/internal/mcp/workspace.go
@@ -103,7 +103,7 @@ func packageSummaries(snapshot *cache.Snapshot, pkgs immutable.Map[cache.PackageID, cache.PackagePath]) []string { var summaries []string - for id := range pkgs.All() { + for id := range pkgs.Keys() { mp := snapshot.Metadata(id) if len(mp.CompiledGoFiles) == 0 { continue // For convenience, just skip uncompiled packages; we could do more if it matters.
diff --git a/gopls/internal/mcp/workspace_diagnostics.go b/gopls/internal/mcp/workspace_diagnostics.go index 49a4685..2315fd8 100644 --- a/gopls/internal/mcp/workspace_diagnostics.go +++ b/gopls/internal/mcp/workspace_diagnostics.go
@@ -13,8 +13,6 @@ "github.com/modelcontextprotocol/go-sdk/mcp" "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" ) @@ -25,13 +23,13 @@ func (h *handler) workspaceDiagnosticsHandler(ctx context.Context, req *mcp.CallToolRequest, params workspaceDiagnosticsParams) (*mcp.CallToolResult, any, error) { countGoDiagnosticsMCP.Inc() var ( - fh file.Handle snapshot *cache.Snapshot release func() err error ) if len(params.Files) > 0 { - fh, snapshot, release, err = h.fileOf(ctx, params.Files[0]) + // This assumes that all files belong to the same view. + _, snapshot, release, err = h.fileOf(ctx, params.Files[0]) if err != nil { return nil, nil, err } @@ -47,14 +45,7 @@ } defer release() - pkgMap := snapshot.WorkspacePackages() - var ids []metadata.PackageID - for id := range pkgMap.All() { - ids = append(ids, id) - } - slices.Sort(ids) - - diagnostics, err := snapshot.PackageDiagnostics(ctx, ids...) + diagnostics, err := snapshot.PackageDiagnostics(ctx, slices.Collect(snapshot.WorkspacePackages().Keys())...) if err != nil { return nil, nil, fmt.Errorf("diagnostics failed: %v", err) } @@ -67,13 +58,12 @@ if err != nil { return nil, nil, fmt.Errorf("diagnostics failed: %v", err) } - diagnostics[fh.URI()] = fileDiagnostics - maps.Insert(fixes, maps.All(fileFixes)) + diagnostics[uri] = fileDiagnostics + maps.Copy(fixes, fileFixes) } - keys := slices.Sorted(maps.Keys(diagnostics)) var b strings.Builder - for _, uri := range keys { + for _, uri := range slices.Sorted(maps.Keys(diagnostics)) { diags := diagnostics[uri] if len(diags) > 0 { fmt.Fprintf(&b, "File `%s` has the following diagnostics:\n", uri.Path()) @@ -83,7 +73,6 @@ fmt.Fprintln(&b) } } - if b.Len() == 0 { return textResult("No diagnostics."), nil, nil }
diff --git a/gopls/internal/progress/progress.go b/gopls/internal/progress/progress.go index d782065..e8c8c80 100644 --- a/gopls/internal/progress/progress.go +++ b/gopls/internal/progress/progress.go
@@ -19,7 +19,6 @@ "golang.org/x/tools/gopls/internal/label" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/xcontext" ) // NewTracker returns a new Tracker that reports progress to the @@ -83,7 +82,7 @@ // // Do the work... // } func (t *Tracker) Start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *WorkDone { - ctx = xcontext.Detach(ctx) // progress messages should not be cancelled + ctx = context.WithoutCancel(ctx) // progress messages should not be cancelled wd := &WorkDone{ client: t.client, token: token, @@ -188,7 +187,7 @@ // Report reports an update on WorkDone report back to the client. func (wd *WorkDone) Report(ctx context.Context, message string, fraction float64) { - ctx = xcontext.Detach(ctx) // progress messages should not be cancelled + ctx = context.WithoutCancel(ctx) // progress messages should not be cancelled if wd == nil { return } @@ -224,7 +223,7 @@ // End reports a workdone completion back to the client. func (wd *WorkDone) End(ctx context.Context, message string) { - ctx = xcontext.Detach(ctx) // progress messages should not be cancelled + ctx = context.WithoutCancel(ctx) // progress messages should not be cancelled if wd == nil { return }
diff --git a/gopls/internal/protocol/context.go b/gopls/internal/protocol/context.go index 5f3151c..fae02de 100644 --- a/gopls/internal/protocol/context.go +++ b/gopls/internal/protocol/context.go
@@ -13,7 +13,6 @@ "golang.org/x/tools/internal/event/core" "golang.org/x/tools/internal/event/export" "golang.org/x/tools/internal/event/label" - "golang.org/x/tools/internal/xcontext" ) type contextKey int @@ -53,7 +52,7 @@ // Add the log item to a queue, rather than sending a // window/logMessage request to the client synchronously, // which would slow down this thread. - ctx2 := xcontext.Detach(ctx) + ctx2 := context.WithoutCancel(ctx) logQueue <- func() { client.LogMessage(ctx2, msg) } return ctx
diff --git a/gopls/internal/protocol/form.go b/gopls/internal/protocol/form.go index 031f5aa..d4871f2 100644 --- a/gopls/internal/protocol/form.go +++ b/gopls/internal/protocol/form.go
@@ -10,6 +10,24 @@ import "encoding/json" +// InteractiveResolveOptions represents the server capabilities for interactive +// resolve. +type InteractiveResolveOptions struct { + // The kinds of interactive resolutions that the server supports. + // + // For example, "command" indicates that the server supports resolving + // `ExecuteCommandParams` interactively through "command/resolve". + Kinds []string `json:"kinds"` +} + +// 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"` +} + // FormFieldTypeString defines a text input. // // It is defined as a struct to allow for future extensibility, such as @@ -64,6 +82,12 @@ // // Only applicable against existing file. Type FileType `json:"type"` + + // Filters specifies the allowed file extensions without the leading dot. A file + // is valid if it matches any of the extensions (OR logic). e.g. ["png", "jpg"]. + // + // If omitted or empty, no extension filter is applied. + Filters []string `json:"filters"` } // FormFieldTypeBool defines a boolean input. @@ -146,6 +170,10 @@ // FormField describes a single question in a form and its validation state. type FormField struct { + // ID is a unique identifier for this field. This key is used as the property + // name in FormAnswers to map the user's input back to this specific field. + ID string `json:"id"` + // Description is the text content of the question (the prompt) presented // to the user. Description string `json:"description"` @@ -160,6 +188,9 @@ // fall back to a string input. Type any `json:"type"` + // Required specifies whether an answer is required for this field. + Required bool `json:"required"` + // Default specifies an optional initial value for the answer. // // If Type is FormFieldTypeEnum, this value must be present in the enum's @@ -171,6 +202,16 @@ Error string `json:"error,omitempty"` } +// FormAnswer describes a single answer to a FormField, identified by its unique +// ID. +type FormAnswer struct { + // The ID of the FormField being answered. + ID string `json:"id"` + + // The user's answer value. + Value any `json:"value"` +} + // InteractiveParams facilitates a multi-step, interactive dialogue between the // client and server during a Language Server Protocol (LSP) request. // @@ -227,18 +268,22 @@ // Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#1164. FormFields []FormField `json:"formFields,omitempty"` - // FormAnswers contains the values for the form questions. + // FormAnswers contains the answers for the form questions. // - // When sent by the language server, this field is optional but recommended - // to support editing previous values. + // When sent by the language server, this field is optional and contains the + // current or default answers to the questions to support editing previous values. // - // When sent by the language client as part of the ResolveXXX request, this - // field is required. The slice must have the same length as FormFields (one - // answer per question), where the answer at index i corresponds to the - // field at index i. + // When sent by the language client, this field contains the user's answers. + // Answers are linked to their respective questions using the field's unique + // `id` rather than their array index. The list must not contain duplicate IDs, + // and each answer's ID must correspond to a field ID defined in `formFields`. + // + // The client must include answers for all required fields (where `required` + // is true). Answers for optional fields (where `required` is false) + // may be omitted if no answer was provided, or included if an answer is available. // // Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#1164. - FormAnswers []any `json:"formAnswers,omitempty"` + FormAnswers []FormAnswer `json:"formAnswers,omitempty"` } // InteractiveListEnumParams defines the parameters for the
diff --git a/gopls/internal/protocol/generate/tables.go b/gopls/internal/protocol/generate/tables.go index bf3f8ce..5ca5cc8 100644 --- a/gopls/internal/protocol/generate/tables.go +++ b/gopls/internal/protocol/generate/tables.go
@@ -78,6 +78,10 @@ {"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]any", {"ExecuteCommandParams", "arguments"}: "[]json.RawMessage", + {"FileCreate", "uri"}: "DocumentURI", // see go.dev/issue/74652 + {"FileDelete", "uri"}: "DocumentURI", + {"FileRename", "oldUri"}: "DocumentURI", + {"FileRename", "newUri"}: "DocumentURI", {"FoldingRange", "kind"}: "string", {"Hover", "contents"}: "MarkupContent", {"InlayHint", "label"}: "[]InlayHintLabelPart",
diff --git a/gopls/internal/protocol/protocol.go b/gopls/internal/protocol/protocol.go index d92e4c5..91bab44 100644 --- a/gopls/internal/protocol/protocol.go +++ b/gopls/internal/protocol/protocol.go
@@ -14,7 +14,6 @@ "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - "golang.org/x/tools/internal/xcontext" ) var ( @@ -89,7 +88,7 @@ call := c.conn.Call(ctx, method, params) err := call.Await(ctx, result) if ctx.Err() != nil { - detached := xcontext.Detach(ctx) + detached := context.WithoutCancel(ctx) c.conn.Notify(detached, "$/cancelRequest", &CancelParams{ID: call.ID().Raw()}) } return err @@ -112,7 +111,7 @@ func ClientHandler(client Client, handler jsonrpc2.Handler) jsonrpc2.Handler { return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { if ctx.Err() != nil { - ctx := xcontext.Detach(ctx) + ctx := context.WithoutCancel(ctx) return reply(ctx, nil, RequestCancelledError) } handled, err := clientDispatch(ctx, client, reply, req) @@ -152,7 +151,7 @@ func ServerHandler(server Server, handler jsonrpc2.Handler) jsonrpc2.Handler { return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { if ctx.Err() != nil { - ctx := xcontext.Detach(ctx) + ctx := context.WithoutCancel(ctx) return reply(ctx, nil, RequestCancelledError) } handled, err := serverDispatch(ctx, server, reply, req) @@ -235,7 +234,7 @@ if ctx.Err() != nil && err == nil { err = RequestCancelledError } - ctx = xcontext.Detach(ctx) + ctx = context.WithoutCancel(ctx) return reply(ctx, resp, err) } return handler(ctx, replyWithDetachedContext, req) @@ -264,7 +263,7 @@ } func cancelCall(ctx context.Context, sender connSender, id jsonrpc2.ID) { - ctx = xcontext.Detach(ctx) + ctx = context.WithoutCancel(ctx) ctx, done := event.Start(ctx, "protocol.canceller") defer done() // Note that only *jsonrpc2.ID implements json.Marshaler.
diff --git a/gopls/internal/protocol/tsprotocol.go b/gopls/internal/protocol/tsprotocol.go index 5601bce..023883e 100644 --- a/gopls/internal/protocol/tsprotocol.go +++ b/gopls/internal/protocol/tsprotocol.go
@@ -2342,7 +2342,7 @@ // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#fileCreate type FileCreate struct { // A file:// URI for the location of the file/folder being created. - URI string `json:"uri"` + URI DocumentURI `json:"uri"` } // Represents information on a file/folder delete. @@ -2352,7 +2352,7 @@ // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#fileDelete type FileDelete struct { // A file:// URI for the location of the file/folder being deleted. - URI string `json:"uri"` + URI DocumentURI `json:"uri"` } // An event describing a file change. @@ -2480,9 +2480,9 @@ // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#fileRename type FileRename struct { // A file:// URI for the original location of the file/folder being renamed. - OldURI string `json:"oldUri"` + OldURI DocumentURI `json:"oldUri"` // A file:// URI for the new location of the file/folder being renamed. - NewURI string `json:"newUri"` + NewURI DocumentURI `json:"newUri"` } // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#fileSystemWatcher
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 781abd9..d858f17 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go
@@ -45,7 +45,6 @@ "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/xcontext" ) func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (any, error) { @@ -78,6 +77,12 @@ } } + if len(params.FormAnswers) != 0 { + commandName := strings.TrimPrefix(params.Command, "gopls.") + counterName := fmt.Sprintf("gopls/interactive/command:%s", commandName) + counter.New(counterName).Inc() + } + handler := &commandHandler{ s: s, params: params, @@ -404,7 +409,7 @@ // Inv: release() must be called exactly once after this point. // In the async case, runcmd may outlive run(). - ctx, cancel := context.WithCancel(xcontext.Detach(ctx)) + ctx, cancel := context.WithCancel(context.WithoutCancel(ctx)) if cfg.progress != "" { header := "" if _, ok := c.s.options.SupportedWorkDoneProgressFormats[cfg.progressStyle]; ok && cfg.progressStyle != "" { @@ -1814,7 +1819,7 @@ progress: "Implement interface X", forURI: args.Location.URI, }, func(ctx context.Context, deps commandDeps) error { - iface, err := golang.FormAnswer[string](params, 0) + iface, err := golang.FormAnswer[string](params, "interface") if err != nil { return err } @@ -1835,7 +1840,7 @@ if len(params.FormAnswers) > 0 { switch args.Modification { case "add": - tags, err := golang.FormAnswer[string](params, 0) + tags, err := golang.FormAnswer[string](params, "tags") if err != nil { return err } @@ -1843,12 +1848,12 @@ if err != nil { return err } - args.Transform, err = golang.FormAnswer[string](params, 1) + args.Transform, err = golang.FormAnswer[string](params, "transform") if err != nil { return err } case "remove": - tags, err := golang.FormAnswer[string](params, 0) + tags, err := golang.FormAnswer[string](params, "tags") if err != nil { return err } @@ -1936,10 +1941,15 @@ err := c.run(ctx, commandConfig{ forURI: args.Location.URI, }, func(ctx context.Context, deps commandDeps) error { - changes, err := golang.MoveType(ctx, deps.fh, deps.snapshot, args.Location, "newpkg/new.go") + // TODO(mkalil): implement with interactive params + destFile := filepath.Join(args.Location.URI.DirPath(), "otherpkg", "destfile.go") + destURI := protocol.URIFromPath(destFile) + changes, rng, err := golang.MoveType(ctx, deps.fh, deps.snapshot, args.Location, destURI) if err != nil { return err } + // Open the file where the type was moved to. + showDocumentImpl(ctx, c.s.client, protocol.URI(destURI), &rng.Range, c.s.options) return applyChanges(ctx, c.s.client, changes) }) return err
diff --git a/gopls/internal/server/general.go b/gopls/internal/server/general.go index 4d0131d..420a702 100644 --- a/gopls/internal/server/general.go +++ b/gopls/internal/server/general.go
@@ -38,7 +38,6 @@ "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/xcontext" ) func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { @@ -128,7 +127,53 @@ } var semanticTokenProvider any - if options.SemanticTokens { + if options.SemanticTokens || options.ConfigurationSupported { + // Also provide the semantic token provider if the client supports + // Configuration calls. Reasoning: + // + // There are two ways to tell the client that this LSP server supports + // semantic token calls: + // 1. Return the semanticTokenProvider here in the `InitializeResult` + // 2. Anytime after Initialize() finishes, call + // client.register("textDocument/semanticTokens") with the + // semanticTokenProvider. Doing it this way would have many + // specific requirements: + // * The client must have set + // `SemanticTokensClientCapabilities.dynamicRegistration = true`. + // * The server must maintain the state of what it has actively + // registered on the client, as the LSP doesn't allow the same + // capability to be registered multiple times. + // * As most clients don't support dynamic registration, we wouldn't + // be able to just support that route, we would have to maintain + // both static and dynamic registration paths. + // + // For all these reasons, we choose not to support the dynamic + // registration and fully rely on option 1. + // + // The only way the server would ever change to start/stop supporting + // semantic tokens is on a user's change of setting: `semanticTokens`. + // gopls only *retrieves* updated user configuration by sending + // `workspace/configuration` requests to the client. + // `options.ConfigurationSupported` indicates whether the client + // supports those calls and gopls doesn't send the `configuration` + // requests if not. + // + // gopls also will only send `workspace/configuration` requests after + // it receives a `workspace/didChangeConfiguration` request or if a new + // directory is added to the current session. We can't determine + // through the `ClientCapabilities` whether the either of these things + // can happen, so we have to always assume that they will. + // + // To conclude, the only signal to guarantee that gopls will never see + // updated user configs is if the client has + // `options.ConfigurationSupported = false`. So if the user currently + // has semanticTokens disabled AND their client doesn't support + // configuration calls, we know we never need to support semantic + // tokens and can inform the client by *not* returning a + // semanticTokenProvider. In any other case (the current scope) we need + // to return a semanticTokenProvider here so that the client will try + // to send `semanticToken` requests in the possibility that the user's + // gopls settings at that point allow us to return them. semanticTokenProvider = protocol.SemanticTokensOptions{ Range: &protocol.Or_SemanticTokensOptions_range{Value: true}, Full: &protocol.Or_SemanticTokensOptions_full{Value: true}, @@ -137,6 +182,12 @@ TokenModifiers: moreslices.ConvertStrings[string](semtok.Modifiers), }, } + // Note: If we ever get to a point that the performance of + // semanticTokens isn't significantly different from other file level + // LSP methods, we should remove this option alltogether and always + // return the semanticTokenProvider. At that point users can configure + // whether their client will send calls for the semantic tokens. This + // is a setting that should ideally live on the front-end. } versionInfo := debug.VersionInfo() @@ -214,7 +265,9 @@ // // TODO(hxjiang): experiment with interactively resolving // "RenameParams". See golang/go#69107. - "interactiveResolveProvider": []string{"command"}, + "interactiveResolveProvider": protocol.InteractiveResolveOptions{ + Kinds: []string{"command"}, + }, }, }, ServerInfo: &protocol.ServerInfo{ @@ -249,6 +302,9 @@ var registrations []protocol.Registration options := s.Options() if options.ConfigurationSupported && options.DynamicConfigurationSupported { + // Even though we are registering `didChangeConfiguration` based on the + // client capabilities, clients can and do still send requests to it + // even if it's not registered. registrations = append(registrations, protocol.Registration{ ID: "workspace/didChangeConfiguration", Method: "workspace/didChangeConfiguration", @@ -479,7 +535,7 @@ // Create new file watcher based on the desired mode. if s.fileWatcher == nil { // TODO(hxjiang): ensure gopls don't process events after shutdown. - watcherCtx := xcontext.Detach(ctx) + watcherCtx := context.WithoutCancel(ctx) onChange := func(events []protocol.FileEvent) { modifications := make([]file.Modification, len(events)) for i, e := range events { @@ -489,7 +545,7 @@ OnDisk: true, } } - if err := s.didModifyFiles(watcherCtx, modifications, FromDidChangeWatchedFiles); err != nil { + if err := s.didModifyFiles(watcherCtx, FromDidChangeWatchedFiles, modifications...); err != nil { event.Error(watcherCtx, "failed to process file changes", err) } } @@ -757,50 +813,62 @@ // recordClientInfo records gopls client info. func recordClientInfo(clientName string) { - key := "gopls/client:other" - switch clientName { - case "Visual Studio Code": - key = "gopls/client:vscode" - case "Visual Studio Code - Insiders": - key = "gopls/client:vscode-insiders" - case "VSCodium": - key = "gopls/client:vscodium" - case "code-server": + // This table maps LSP (not MCP) clientInfo.Name prefixes to Go telemetry counters. + // Where authoritative source is available, we link to it. + for _, cli := range [...]struct { + clientNamePrefix, telemetryKey string + }{ + {"Visual Studio Code - Insiders", "gopls/client:vscode-insiders"}, + {"Visual Studio Code", "gopls/client:vscode"}, + + {"VSCodium", "gopls/client:vscodium"}, + // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19 - key = "gopls/client:code-server" - case "Eglot": + {"code-server", "gopls/client:code-server"}, + // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html - key = "gopls/client:eglot" - case "govim": + {"Eglot", "gopls/client:eglot"}, + // https://github.com/govim/govim/pull/1189 - key = "gopls/client:govim" - case "helix": + {"govim", "gopls/client:govim"}, + // https://github.com/helix-editor/helix/blob/d0218f7e78bc0c3af4b0995ab8bda66b9c542cf3/helix-lsp/src/client.rs#L714 - key = "gopls/client:helix" - case "Neovim": + {"helix", "gopls/client:helix"}, + // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361 // https://github.com/neovim/neovim/blob/fe6026825883b44b09a8d3a03f2d49bfc8ed4725/runtime/lua/vim/lsp/client.lua#564 - key = "gopls/client:neovim" - case "coc.nvim": + {"Neovim", "gopls/client:neovim"}, + // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994 - key = "gopls/client:coc.nvim" - case "Sublime Text LSP": + {"coc.nvim", "gopls/client:coc.nvim"}, + // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493 - key = "gopls/client:sublimetext" - case "Windsurf": - key = "gopls/client:windsurf" - case "Cursor": - key = "gopls/client:cursor" - case "Zed", "Zed Dev", "Zed Nightly", "Zed Preview": + {"Sublime Text LSP", "gopls/client:sublimetext"}, + + {"Cursor", "gopls/client:cursor"}, + // https: //github.com/zed-industries/zed/blob/0ac17526687bf11007f0fbb5c3b2ff463ce47293/crates/release_channel/src/lib.rs#L147 - key = "gopls/client:zed" - default: - // Accumulate at least a local counter for an unknown - // client name, but also fall through to count it as - // ":other" for collection. - if clientName != "" { - counter.New(fmt.Sprintf("gopls/client-other:%s", clientName)).Inc() + {"Zed", "gopls/client:zed"}, // incl. "Zed Dev", "Zed Nightly", "Zed Preview" + + // (Observed empirically.) + {"Claude Code", "gopls/client:claude"}, + + // (Observed empirically.) + {"Antigravity", "gopls/client:antigravity"}, + {"Jetski", "gopls/client:antigravity"}, + {"Windsurf", "gopls/client:windsurf"}, + } { + if strings.HasPrefix(clientName, cli.clientNamePrefix) { + counter.Inc(cli.telemetryKey) + return } } - counter.Inc(key) + + // Accumulate at least a local counter for an unknown + // client name, but also fall through to count it as + // ":other" for collection. + if clientName != "" { + counter.New(fmt.Sprintf("gopls/client-other:%s", clientName)).Inc() + } + counter.Inc("gopls/client:other") }
diff --git a/gopls/internal/server/text_synchronization.go b/gopls/internal/server/text_synchronization.go index 710c401..ccbee9f 100644 --- a/gopls/internal/server/text_synchronization.go +++ b/gopls/internal/server/text_synchronization.go
@@ -20,7 +20,6 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/xcontext" ) // ModificationSource identifies the origin of a change. @@ -111,13 +110,13 @@ Name: filepath.Base(dir), }}) } - return s.didModifyFiles(ctx, []file.Modification{{ + return s.didModifyFiles(ctx, FromDidOpen, file.Modification{ URI: uri, Action: file.Open, Version: params.TextDocument.Version, Text: []byte(params.TextDocument.Text), LanguageID: params.TextDocument.LanguageID, - }}, FromDidOpen) + }) } func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { @@ -129,16 +128,12 @@ if err != nil { return err } - c := file.Modification{ + return s.didModifyFiles(ctx, FromDidChange, file.Modification{ URI: uri, Action: file.Change, Version: params.TextDocument.Version, Text: text, - } - if err := s.didModifyFiles(ctx, []file.Modification{c}, FromDidChange); err != nil { - return err - } - return s.warnAboutModifyingGeneratedFiles(ctx, uri) + }) } // warnAboutModifyingGeneratedFiles shows a warning if a user tries to edit a @@ -178,47 +173,46 @@ ctx, done := event.Start(ctx, "server.DidChangeWatchedFiles") defer done() - var modifications []file.Modification - for _, change := range params.Changes { + modifications := make([]file.Modification, len(params.Changes)) + for i, change := range params.Changes { action := changeTypeToFileAction(change.Type) - modifications = append(modifications, file.Modification{ + modifications[i] = file.Modification{ URI: change.URI, Action: action, OnDisk: true, - }) + } } - return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles) + return s.didModifyFiles(ctx, FromDidChangeWatchedFiles, modifications...) } func (s *server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { ctx, done := event.Start(ctx, "server.DidSave", label.URI.Of(params.TextDocument.URI)) defer done() - c := file.Modification{ + var text []byte + if params.Text != nil { + text = []byte(*params.Text) + } + return s.didModifyFiles(ctx, FromDidSave, file.Modification{ URI: params.TextDocument.URI, Action: file.Save, - } - if params.Text != nil { - c.Text = []byte(*params.Text) - } - return s.didModifyFiles(ctx, []file.Modification{c}, FromDidSave) + Text: text, + }) } func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { ctx, done := event.Start(ctx, "server.DidClose", label.URI.Of(params.TextDocument.URI)) defer done() - return s.didModifyFiles(ctx, []file.Modification{ - { - URI: params.TextDocument.URI, - Action: file.Close, - Version: -1, - Text: nil, - }, - }, FromDidClose) + return s.didModifyFiles(ctx, FromDidClose, file.Modification{ + URI: params.TextDocument.URI, + Action: file.Close, + Version: -1, + Text: nil, + }) } -func (s *server) didModifyFiles(ctx context.Context, modifications []file.Modification, cause ModificationSource) error { +func (s *server) didModifyFiles(ctx context.Context, cause ModificationSource, modifications ...file.Modification) error { // Something happened. Wake up a quiescent file watcher. s.fileWatcherMu.Lock() if s.fileWatcher != nil { @@ -271,6 +265,12 @@ // golang/go#50267: diagnostics should be re-sent after each change. for _, mod := range modifications { s.mustPublishDiagnostics(mod.URI) + + if cause == FromDidChange && mod.Action == file.Change { + if err := s.warnAboutModifyingGeneratedFiles(ctx, mod.URI); err != nil { + return err // e.g. failed to get snapshot + } + } } modCtx, modID := s.needsDiagnosis(ctx, viewsToDiagnose) @@ -328,7 +328,7 @@ if s.cancelPrevDiagnostics != nil { s.cancelPrevDiagnostics() } - modCtx := xcontext.Detach(ctx) + modCtx := context.WithoutCancel(ctx) modCtx, s.cancelPrevDiagnostics = context.WithCancel(modCtx) s.lastModificationID++ modID := s.lastModificationID
diff --git a/gopls/internal/server/vulncheck_prompt.go b/gopls/internal/server/vulncheck_prompt.go index 52197b3..cb69f1a 100644 --- a/gopls/internal/server/vulncheck_prompt.go +++ b/gopls/internal/server/vulncheck_prompt.go
@@ -27,7 +27,6 @@ "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/xcontext" ) const ( @@ -221,7 +220,7 @@ return } defer release() - ctx = xcontext.Detach(ctx) + ctx = context.WithoutCancel(ctx) work := s.progress.Start(ctx, GoVulncheckCommandTitle, "Running govulncheck...", nil, nil) defer work.End(ctx, "Done.") @@ -458,9 +457,13 @@ } func vulncheckFilename() (string, error) { - configDir, err := os.UserConfigDir() + configDir := os.Getenv(GoplsConfigDirEnvvar) // set for testing + if configDir != "" { + return filepath.Join(configDir, "vulncheck", "settings.json"), nil + } + userDir, err := os.UserConfigDir() if err != nil { return "", err } - return filepath.Join(configDir, "gopls", "vulncheck", "settings.json"), nil + return filepath.Join(userDir, "gopls", "vulncheck", "settings.json"), nil }
diff --git a/gopls/internal/server/vulncheck_prompt_test.go b/gopls/internal/server/vulncheck_prompt_test.go index d5ee9ea..b32ea8a 100644 --- a/gopls/internal/server/vulncheck_prompt_test.go +++ b/gopls/internal/server/vulncheck_prompt_test.go
@@ -265,16 +265,7 @@ for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Cleanup(func() { - configDir, err := os.UserConfigDir() - if err != nil { - t.Fatalf("os.UserConfigDir() failed: %v", err) - } - if err := os.RemoveAll(filepath.Join(configDir, "gopls")); err != nil && !os.IsNotExist(err) { - t.Fatalf("failed to clear user config: %v", err) - } - }) - t.Setenv("HOME", t.TempDir()) + t.Setenv(GoplsConfigDirEnvvar, t.TempDir()) ctx := context.Background() var promptShown bool client := &mockClient{ @@ -357,16 +348,7 @@ if runtime.GOARCH == "wasm" { t.Skip("test not supported in wasm") } - t.Cleanup(func() { - configDir, err := os.UserConfigDir() - if err != nil { - t.Fatalf("os.UserConfigDir() failed: %v", err) - } - if err := os.RemoveAll(filepath.Join(configDir, "gopls")); err != nil && !os.IsNotExist(err) { - t.Fatalf("failed to clear user config: %v", err) - } - }) - t.Setenv("HOME", t.TempDir()) + t.Setenv(GoplsConfigDirEnvvar, t.TempDir()) pref, err := getVulncheckPreference() if err != nil {
diff --git a/gopls/internal/server/workspace.go b/gopls/internal/server/workspace.go index 86f01e2..66d682a 100644 --- a/gopls/internal/server/workspace.go +++ b/gopls/internal/server/workspace.go
@@ -157,8 +157,7 @@ var allChanges []protocol.DocumentChange for _, createdFile := range params.Files { - uri := protocol.DocumentURI(createdFile.URI) - fh, snapshot, release, err := s.session.FileOf(ctx, uri) + fh, snapshot, release, err := s.session.FileOf(ctx, createdFile.URI) if err != nil { event.Error(ctx, "fail to call fileOf", err) continue @@ -175,7 +174,6 @@ if change != nil { allChanges = append(allChanges, *change) } - default: } }
diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go index a08a7d9..247d7b7 100644 --- a/gopls/internal/settings/analysis.go +++ b/gopls/internal/settings/analysis.go
@@ -6,57 +6,24 @@ import ( "log" - "slices" "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/appends" - "golang.org/x/tools/go/analysis/passes/asmdecl" - "golang.org/x/tools/go/analysis/passes/assign" - "golang.org/x/tools/go/analysis/passes/atomic" "golang.org/x/tools/go/analysis/passes/atomicalign" - "golang.org/x/tools/go/analysis/passes/bools" - "golang.org/x/tools/go/analysis/passes/buildtag" - "golang.org/x/tools/go/analysis/passes/cgocall" - "golang.org/x/tools/go/analysis/passes/composite" - "golang.org/x/tools/go/analysis/passes/copylock" "golang.org/x/tools/go/analysis/passes/deepequalerrors" - "golang.org/x/tools/go/analysis/passes/defers" - "golang.org/x/tools/go/analysis/passes/directive" - "golang.org/x/tools/go/analysis/passes/errorsas" "golang.org/x/tools/go/analysis/passes/fieldalignment" - "golang.org/x/tools/go/analysis/passes/framepointer" - "golang.org/x/tools/go/analysis/passes/hostport" - "golang.org/x/tools/go/analysis/passes/httpresponse" - "golang.org/x/tools/go/analysis/passes/ifaceassert" "golang.org/x/tools/go/analysis/passes/inline" - "golang.org/x/tools/go/analysis/passes/loopclosure" - "golang.org/x/tools/go/analysis/passes/lostcancel" "golang.org/x/tools/go/analysis/passes/modernize" - "golang.org/x/tools/go/analysis/passes/nilfunc" "golang.org/x/tools/go/analysis/passes/nilness" - "golang.org/x/tools/go/analysis/passes/printf" "golang.org/x/tools/go/analysis/passes/scannererr" "golang.org/x/tools/go/analysis/passes/shadow" - "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/sortslice" - "golang.org/x/tools/go/analysis/passes/stdmethods" - "golang.org/x/tools/go/analysis/passes/stdversion" - "golang.org/x/tools/go/analysis/passes/stringintconv" - "golang.org/x/tools/go/analysis/passes/structtag" - "golang.org/x/tools/go/analysis/passes/testinggoroutine" - "golang.org/x/tools/go/analysis/passes/tests" - "golang.org/x/tools/go/analysis/passes/timeformat" - "golang.org/x/tools/go/analysis/passes/unmarshal" - "golang.org/x/tools/go/analysis/passes/unreachable" - "golang.org/x/tools/go/analysis/passes/unsafeptr" - "golang.org/x/tools/go/analysis/passes/unusedresult" + "golang.org/x/tools/go/analysis/passes/sqlrowserr" "golang.org/x/tools/go/analysis/passes/unusedwrite" - "golang.org/x/tools/go/analysis/passes/waitgroup" + "golang.org/x/tools/go/analysis/suite/fix" + "golang.org/x/tools/go/analysis/suite/vet" "golang.org/x/tools/gopls/internal/analysis/deprecated" "golang.org/x/tools/gopls/internal/analysis/embeddirective" - "golang.org/x/tools/gopls/internal/analysis/errorsastype" + "golang.org/x/tools/gopls/internal/analysis/errorsastypeshadow" "golang.org/x/tools/gopls/internal/analysis/fillreturns" "golang.org/x/tools/gopls/internal/analysis/infertypeargs" "golang.org/x/tools/gopls/internal/analysis/maprange" @@ -72,11 +39,13 @@ "golang.org/x/tools/gopls/internal/analysis/writestring" "golang.org/x/tools/gopls/internal/analysis/yield" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/goplsexport" "honnef.co/go/tools/analysis/lint" ) -var AllAnalyzers = slices.Concat(DefaultAnalyzers, StaticcheckAnalyzers) +// AllAnalyzers holds the list of Analyzers available to all gopls +// sessions, independent of build version. It is the source from which +// gopls/doc/analyzers.md is generated. +var AllAnalyzers = initAnalyzers() // Analyzer augments an [analysis.Analyzer] with additional LSP configuration. // @@ -159,143 +128,107 @@ // String returns the name of this analyzer. func (a *Analyzer) String() string { return a.analyzer.String() } -// DefaultAnalyzers holds the list of Analyzers available to all gopls -// sessions, independent of build version. It is the source from which -// gopls/doc/analyzers.md is generated. -var DefaultAnalyzers = []*Analyzer{ - // See [Analyzer.Severity] for guidance on setting analyzer severity below. +func initAnalyzers() (res []*Analyzer) { + seen := make(map[*analysis.Analyzer]bool) - // The traditional vet suite: - {analyzer: appends.Analyzer}, - {analyzer: asmdecl.Analyzer}, - {analyzer: assign.Analyzer}, - {analyzer: atomic.Analyzer}, - {analyzer: bools.Analyzer}, - {analyzer: buildtag.Analyzer}, - {analyzer: cgocall.Analyzer}, - {analyzer: composite.Analyzer}, - {analyzer: copylock.Analyzer}, - {analyzer: defers.Analyzer}, - { - analyzer: deprecated.Analyzer, - severity: protocol.SeverityHint, - tags: []protocol.DiagnosticTag{protocol.Deprecated}, - }, - {analyzer: directive.Analyzer}, - {analyzer: errorsas.Analyzer}, - {analyzer: framepointer.Analyzer}, - {analyzer: httpresponse.Analyzer}, - {analyzer: ifaceassert.Analyzer}, - {analyzer: loopclosure.Analyzer}, - {analyzer: lostcancel.Analyzer}, - {analyzer: nilfunc.Analyzer}, - {analyzer: printf.Analyzer}, - {analyzer: shift.Analyzer}, - {analyzer: sigchanyzer.Analyzer}, - {analyzer: slog.Analyzer}, - {analyzer: stdmethods.Analyzer}, - {analyzer: stdversion.Analyzer}, - {analyzer: stringintconv.Analyzer}, - {analyzer: structtag.Analyzer}, - {analyzer: testinggoroutine.Analyzer}, - {analyzer: tests.Analyzer}, - {analyzer: timeformat.Analyzer}, - {analyzer: unmarshal.Analyzer}, - {analyzer: unreachable.Analyzer}, - {analyzer: unsafeptr.Analyzer}, - {analyzer: unusedresult.Analyzer}, + // Start with the traditional vet and fix suites. + for _, suite := range []struct { + name string + analyzers []*analysis.Analyzer + severity protocol.DiagnosticSeverity + }{ + {"fix.Suite", fix.Suite, protocol.SeverityHint}, + {"vet.Suite", vet.Suite, protocol.SeverityWarning}, + } { + for _, a := range suite.analyzers { + // De-duplicate, since the suites overlap. + if !seen[a] { + seen[a] = true + res = append(res, &Analyzer{analyzer: a, severity: suite.severity}) + } + } + } - // not suitable for vet: - // - some (nilness, yield) use go/ssa; see #59714. - // - others don't meet the "frequency" criterion; - // see GOROOT/src/cmd/vet/README. - {analyzer: atomicalign.Analyzer}, - {analyzer: deepequalerrors.Analyzer}, - {analyzer: nilness.Analyzer}, // uses go/ssa - {analyzer: yield.Analyzer}, // uses go/ssa - {analyzer: sortslice.Analyzer}, - {analyzer: embeddirective.Analyzer}, - {analyzer: scannererr.Analyzer}, // to appear in cmd/vet@go1.27 - {analyzer: waitgroup.Analyzer}, // to appear in cmd/vet@go1.25 - {analyzer: hostport.Analyzer}, // to appear in cmd/vet@go1.25 - {analyzer: recursiveiter.Analyzer}, // under evaluation - {analyzer: writestring.Analyzer}, - - // disabled due to high false positives - {analyzer: shadow.Analyzer, severity: protocol.SeverityHint, nonDefault: true}, // very noisy - {analyzer: fieldalignment.Analyzer, severity: protocol.SeverityHint, nonDefault: true}, // #67762, #76237 - - // simplifiers and modernizers - // - // These analyzers offer mere style fixes on correct code, - // thus they will never appear in cmd/vet and - // their severity level is "information". - // - // gofmt -s suite - { - analyzer: simplifycompositelit.Analyzer, - actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, - severity: protocol.SeverityInformation, - }, - { - analyzer: simplifyrange.Analyzer, - actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, - severity: protocol.SeverityInformation, - }, - { - analyzer: simplifyslice.Analyzer, - actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, - severity: protocol.SeverityInformation, - }, - // other simplifiers - {analyzer: inline.Analyzer, severity: protocol.SeverityHint}, // (in -lazy_edit mode) - {analyzer: infertypeargs.Analyzer, severity: protocol.SeverityInformation}, - {analyzer: maprange.Analyzer, severity: protocol.SeverityHint}, - {analyzer: unusedparams.Analyzer, severity: protocol.SeverityInformation}, - {analyzer: unusedfunc.Analyzer, severity: protocol.SeverityInformation}, - {analyzer: unusedwrite.Analyzer, severity: protocol.SeverityInformation}, // uses go/ssa - // the modernize suite - {analyzer: modernize.AnyAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.AppendClippedAnalyzer, severity: protocol.SeverityHint, nonDefault: true}, // not nil-preserving - {analyzer: modernize.AtomicTypesAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.BLoopAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.EmbedLitAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.ErrorsAsTypeAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.FmtAppendfAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.ForVarAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.MapsLoopAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.MinMaxAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.NewExprAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.OmitZeroAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.PlusBuildAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.RangeIntAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.ReflectTypeForAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.SlicesContainsAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.SlicesDeleteAnalyzer, severity: protocol.SeverityHint, nonDefault: true}, // not nil-preserving - {analyzer: modernize.SlicesSortAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.StdIteratorsAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.StringsBuilderAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.StringsCutAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.StringsCutPrefixAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.StringsSeqAnalyzer, severity: protocol.SeverityHint}, - {analyzer: modernize.TestingContextAnalyzer, severity: protocol.SeverityHint}, - {analyzer: goplsexport.UnsafeFuncsModernizer, severity: protocol.SeverityHint}, - {analyzer: modernize.WaitGroupGoAnalyzer, severity: protocol.SeverityHint}, - - // type-error analyzers - // These analyzers enrich go/types errors with suggested fixes. - // Since they exist only to attach their fixes to type errors, their - // severity is irrelevant. - {analyzer: fillreturns.Analyzer}, - {analyzer: nonewvars.Analyzer}, - {analyzer: noresultvalues.Analyzer}, - {analyzer: unusedvariable.Analyzer}, - - {analyzer: errorsastype.Analyzer}, -} - -func init() { + // set inline -lazy_edit mode if err := inline.Analyzer.Flags.Set("lazy_edits", "true"); err != nil { log.Fatalf("setting inline -lazy_edits flag: %v", err) } + + // See [Analyzer.Severity] for guidance on setting analyzer severity below. + for _, a := range []*Analyzer{ + // not suitable for vet.Suite: + // - some (nilness, yield) use go/ssa; see #59714. + // - others don't meet the "frequency" criterion; + // see GOROOT/src/cmd/vet/README. + {analyzer: atomicalign.Analyzer}, + {analyzer: deprecated.Analyzer, + severity: protocol.SeverityHint, + tags: []protocol.DiagnosticTag{protocol.Deprecated}, + }, + {analyzer: deepequalerrors.Analyzer}, + {analyzer: nilness.Analyzer}, // uses go/ssa + {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 + + // disabled due to high false positives + {analyzer: shadow.Analyzer, severity: protocol.SeverityHint, nonDefault: true}, // very noisy + {analyzer: fieldalignment.Analyzer, severity: protocol.SeverityHint, nonDefault: true}, // #67762, #76237 + + // simplifiers and modernizers (beyond fix.Suite) + // + // These analyzers offer mere style fixes on correct code, + // thus they will never appear in cmd/vet or cmd/fix and + // their severity level is "information". + // + // gofmt -s suite + { + analyzer: simplifycompositelit.Analyzer, + actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, + severity: protocol.SeverityInformation, + }, + { + analyzer: simplifyrange.Analyzer, + actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, + severity: protocol.SeverityInformation, + }, + { + analyzer: simplifyslice.Analyzer, + actionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, + severity: protocol.SeverityInformation, + }, + // other simplifiers + {analyzer: infertypeargs.Analyzer, severity: protocol.SeverityInformation}, + {analyzer: maprange.Analyzer, severity: protocol.SeverityHint}, + {analyzer: unusedparams.Analyzer, severity: protocol.SeverityInformation}, + {analyzer: unusedfunc.Analyzer, severity: protocol.SeverityInformation}, + {analyzer: unusedwrite.Analyzer, severity: protocol.SeverityInformation}, // uses go/ssa + // modernizers not included in modernize.Suite (nor fix.Suite) + {analyzer: modernize.AppendClippedAnalyzer, nonDefault: true}, // not nil-preserving + {analyzer: modernize.BLoopAnalyzer}, // may skew benchmark results, see golang/go#74967 + {analyzer: modernize.FmtAppendfAnalyzer}, // makes code less clear, see golang/go#77581 + {analyzer: modernize.SlicesDeleteAnalyzer, nonDefault: true}, // not nil-preserving + + // type-error analyzers + // These analyzers enrich go/types errors with suggested fixes. + // Since they exist only to attach their fixes to type errors, their + // severity is irrelevant. + {analyzer: fillreturns.Analyzer}, + {analyzer: nonewvars.Analyzer}, + {analyzer: noresultvalues.Analyzer}, + {analyzer: unusedvariable.Analyzer}, + } { + if seen[a.analyzer] { + log.Fatalf("duplicate analyzer: %q", a.analyzer.Name) + } + seen[a.analyzer] = true + res = append(res, a) + } + + return append(res, staticcheckAnalyzers()...) }
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index b4c0ec1..fa9e44f 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go
@@ -72,7 +72,7 @@ RefactorExtractVariable: true, RefactorExtractVariableAll: true, RefactorExtractToNewFile: true, - RefactorMoveType: true, // off while implementation unfinished + RefactorMoveType: true, // gated by MoveType setting, which is off by default // Not GoTest: it must be explicit in CodeActionParams.Context.Only }, file.Mod: { @@ -137,12 +137,12 @@ NewGoFileHeader: true, RenameMovesSubpackages: false, }, + FileWatcher: FileWatcherOff, }, InternalOptions: InternalOptions{ CompleteUnimported: true, CompletionDocumentation: true, DeepCompletion: true, - FileWatcher: FileWatcherOff, SubdirWatchPatterns: SubdirWatchPatternsAuto, ReportAnalysisProgressAfter: 5 * time.Second, TelemetryPrompt: false,
diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index 32a2439..b0a6477 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go
@@ -227,7 +227,18 @@ // SemanticTokens determines whether gopls will return a // SemanticTokensProvider at initialization, or respond - // to request for semantic tokens. + // to requests for semantic tokens. + // + // This setting being `false` won't necessary disable the client's calls + // for semantic tokens. If you want that, it would need to be configured in + // the client. For example, in VSCode, this would disable all Go semantic + // token calls to the LSP server: + // + // ```json5 + // "[go]": { + // "editor.semanticHighlighting.enabled": false, + // } + // ``` SemanticTokens bool `status:"experimental"` // NoSemanticString turns off the sending of the semantic token 'string' @@ -259,6 +270,10 @@ // RenameMovesSubpackages enables Rename operations on packages to // move subdirectories of the target package. RenameMovesSubpackages bool `status:"experimental"` + + // MoveType enables producing Move Type codeactions. The implementation + // is unfinished so we use this setting to gate its use. + MoveType bool `status:"experimental"` } // A CodeLensSource identifies an (algorithmic) source of code lenses. @@ -559,30 +574,30 @@ const ( // ParameterNames controls inlay hints for parameter names: // ```go - // parseInt(/* str: */ "123", /* radix: */ 8) + // parseInt(« str: » "123", « radix: » 8) // ``` ParameterNames InlayHint = "parameterNames" // AssignVariableTypes controls inlay hints for variable types in assign statements: // ```go - // i/* int*/, j/* int*/ := 0, len(r)-1 + // i« int», j« int» := 0, len(r)-1 // ``` AssignVariableTypes InlayHint = "assignVariableTypes" // ConstantValues controls inlay hints for constant values: // ```go // const ( - // KindNone Kind = iota/* = 0*/ - // KindPrint/* = 1*/ - // KindPrintf/* = 2*/ - // KindErrorf/* = 3*/ + // KindNone Kind = iota« = 0» + // KindPrint« = 1» + // KindPrintf« = 2» + // KindErrorf« = 3» // ) // ``` ConstantValues InlayHint = "constantValues" // RangeVariableTypes controls inlay hints for variable types in range statements: // ```go - // for k/* int*/, v/* string*/ := range []string{} { + // for k« int», v« string» := range []string{} { // fmt.Println(k, v) // } // ``` @@ -593,26 +608,28 @@ // for _, c := range []struct { // in, want string // }{ - // /*struct{ in string; want string }*/{"Hello, world", "dlrow ,olleH"}, + // «struct{ in string; want string }»{"Hello, world", "dlrow ,olleH"}, // } // ``` CompositeLiteralTypes InlayHint = "compositeLiteralTypes" // CompositeLiteralFieldNames inlay hints for composite literal field names: // ```go - // {/*in: */"Hello, world", /*want: */"dlrow ,olleH"} + // Point2D{«X: »1, «Y: »2} + // + // Outer{«Embedded.»Field: 0} // ``` CompositeLiteralFieldNames InlayHint = "compositeLiteralFields" // FunctionTypeParameters inlay hints for implicit type parameters on generic functions: // ```go - // myFoo/*[int, string]*/(1, "hello") + // myFoo«[int, string]»(1, "hello") // ``` FunctionTypeParameters InlayHint = "functionTypeParameters" // IgnoredError inlay hints for implicitly discarded errors: // ```go - // f.Close() // ignore error + // f.Close()« // ignore error» // ``` // This check inserts an `// ignore error` hint following any // statement that is a function call whose error result is @@ -663,6 +680,17 @@ UIOptions FormattingOptions + // FileWatcher specifies the server-side file watching strategy used by gopls. + // + // By default, this is set to "off", meaning gopls relies exclusively on the + // language client (e.g., the editor) to send file change notifications. + // + // Available options: + // - "off" : Client-driven watching (default) + // - "fsnotify" : OS-level event notifications + // - "poll" : Periodic directory scanning + FileWatcher FileWatcherMode `status:"experimental"` + // MaxFileCacheBytes sets a soft limit on the file cache size in bytes. // If zero, the default budget is used. // @@ -793,17 +821,6 @@ // issue. SubdirWatchPatterns SubdirWatchPatterns - // FileWatcher specifies the server-side file watching strategy used by gopls. - // - // By default, this is set to "off", meaning gopls relies exclusively on the - // language client (e.g., the editor) to send file change notifications. - // - // Available options: - // - "off" : Client-driven watching (default) - // - "fsnotify" : OS-level event notifications - // - "poll" : Periodic directory scanning - FileWatcher FileWatcherMode - // ReportAnalysisProgressAfter sets the duration for gopls to wait before starting // progress reporting for ongoing go/analysis passes. // @@ -1088,10 +1105,14 @@ } } - if inputTypes, ok := experimental["interactiveInputTypes"].([]any); ok { - o.SupportedInteractiveInputTypes = make(map[InteractiveInputType]bool, len(inputTypes)) - for _, t := range inputTypes { - o.SupportedInteractiveInputTypes[InteractiveInputType(t.(string))] = true + if interactiveCap, ok := experimental["interactiveResolve"].(map[string]any); ok { + if inputTypes, ok := interactiveCap["inputTypes"].([]any); ok { + o.SupportedInteractiveInputTypes = make(map[InteractiveInputType]bool, len(inputTypes)) + for _, t := range inputTypes { + if s, ok := t.(string); ok { + o.SupportedInteractiveInputTypes[InteractiveInputType(s)] = true + } + } } } } @@ -1195,10 +1216,16 @@ case "completionBudget": return nil, setDuration(&o.CompletionBudget, value) case "importsSource": - return setEnum(&o.ImportsSource, value, + res, err := setEnum(&o.ImportsSource, value, ImportsSourceOff, ImportsSourceGopls, ImportsSourceGoimports) + if err != nil { + return nil, err + } + return res, &SoftError{ + msg: "importsSource is deprecated as it is no longer needed", + } case "matcher": return setEnum(&o.Matcher, value, Fuzzy, @@ -1418,6 +1445,9 @@ case "fileWatcher": return setEnum(&o.FileWatcher, value, FileWatcherOff, FileWatcherFSNotify, FileWatcherPoll) + case "moveType": + return setBool(&o.MoveType, value) + // deprecated and renamed settings // // These should never be deleted: there is essentially no cost
diff --git a/gopls/internal/settings/staticcheck.go b/gopls/internal/settings/staticcheck.go index 04b89d5..cf9c9a3 100644 --- a/gopls/internal/settings/staticcheck.go +++ b/gopls/internal/settings/staticcheck.go
@@ -177,10 +177,7 @@ "honnef.co/go/tools/stylecheck/st1023" ) -// StaticcheckAnalyzers lists available Staticcheck analyzers. -var StaticcheckAnalyzers = initStaticcheckAnalyzers() - -func initStaticcheckAnalyzers() (res []*Analyzer) { +func staticcheckAnalyzers() (res []*Analyzer) { mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { switch severity {
diff --git a/gopls/internal/test/integration/bench/bench_test.go b/gopls/internal/test/integration/bench/bench_test.go index f86c8e2..f42cf47 100644 --- a/gopls/internal/test/integration/bench/bench_test.go +++ b/gopls/internal/test/integration/bench/bench_test.go
@@ -24,13 +24,13 @@ "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" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/pprof" - "golang.org/x/tools/internal/tool" ) var (
diff --git a/gopls/internal/test/integration/codelens/codelens_test.go b/gopls/internal/test/integration/codelens/codelens_test.go index c1f2c52..af45c2a 100644 --- a/gopls/internal/test/integration/codelens/codelens_test.go +++ b/gopls/internal/test/integration/codelens/codelens_test.go
@@ -385,7 +385,7 @@ } ` Run(t, workspace, func(t *testing.T, env *Env) { - // Open the file. We have a nonexistant symbol that will break cgo processing. + // Open the file. We have a nonexistent symbol that will break cgo processing. env.OpenFile("cgo.go") env.AfterChange( Diagnostics(env.AtRegexp("cgo.go", ``), WithMessage("go list failed to return CompiledGoFiles")),
diff --git a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go index 58d6b37..60a6310 100644 --- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go +++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go
@@ -703,7 +703,12 @@ opts := []RunOption{ EnvVars{"GOMODCACHE": modcache}, ProxyFiles(ardanLabsProxy), - //WriteGoSum("."), // TODO(golang/go#74594): uncommenting this causes mysterious failure; investigate and make the error clearer (go list?) + // WriteGoSum() cannot be uncommented because it has a side effect + // that breaks the test. The test relies on go.mod not having a + // require statement for the package imported by main.go. However + // WriteGoSum() causes the execution of go list -mod=mod ./... + // which both writes go.sum and adds the require statement to go.mod. + //WriteGoSum("."), } t.Run("setup", func(t *testing.T) {
diff --git a/gopls/internal/test/integration/diagnostics/repro64235_test.go b/gopls/internal/test/integration/diagnostics/repro64235_test.go new file mode 100644 index 0000000..e6468bd --- /dev/null +++ b/gopls/internal/test/integration/diagnostics/repro64235_test.go
@@ -0,0 +1,116 @@ +// 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 diagnostics + +import ( + "os" + "testing" + "time" + + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/server" + . "golang.org/x/tools/gopls/internal/test/integration" +) + +// TestIssue64235 deterministically reproduces the importPackage +// "package name is %q, want %q" bug.Errorf reported by telemetry in +// golang/go#64235 (the dominant pkg.Name()=="" bucket). +// +// Mechanism: the snapshot caches file content, but go list reads disk +// directly. If a closed file's on-disk content diverges from the +// cached content (a missed or delayed file-watcher event), two +// consecutive loads can produce inconsistent metadata: the first +// installs mp_a.Name="" (and type-checks a from cached source, +// caching export data with manifest name "a"); the second, after a is +// restored on disk, gives a fresh importer b an edge to a while a's +// own update is discarded by the existing-metadata filter in +// Snapshot.load. importPackage(a) then reads the cached export data +// and observes the name mismatch. +// +// Within a single load, the guards in load.go (the existing-metadata +// filter and the imported.Name=="" edge drop) prevent this state; it +// requires two loads observing different disk states. +// +// See golang/go#NNNNN for the proposed fix direction (recover from +// snapshot/disk incoherence rather than patching this symptom). +func TestIssue64235(t *testing.T) { + t.Skip("golang/go#64235: deterministic repro of a known coherency bug; unskip when fixed") + + const aOriginal = "package a\n\ntype T int\n" + const files = ` +-- go.mod -- +module mod.com + +go 1.21 +-- a/a.go -- +` + aOriginal + ` +-- b/b.go -- +package b + +var V int +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("b/b.go") + env.AfterChange(NoDiagnostics()) + + aPath := env.Sandbox.Workdir.AbsPath("a/a.go") + gomodPath := env.Sandbox.Workdir.AbsPath("go.mod") + + // --- Phase 1: install mp_a.Name="" with cached a.go intact --- + // Truncate a/a.go on disk WITHOUT notifying gopls (simulating + // a missed file-watcher event), then trigger reinit via go.mod. + // go list sees an empty a.go and returns Name=""; go/packages + // falls back CompiledGoFiles=GoFiles, so a is still + // type-checked, but from the cached "package a" source. + // storePackageResults caches export data with manifest name + // "a" under ph_a.key (which incorporates Name=""). + if err := os.WriteFile(aPath, nil, 0644); err != nil { + t.Fatal(err) + } + gomod, _ := os.ReadFile(gomodPath) + if err := os.WriteFile(gomodPath, append(gomod, []byte("\n// touched\n")...), 0644); err != nil { + t.Fatal(err) + } + if err := env.Editor.Server.DidChangeWatchedFiles(env.Ctx, &protocol.DidChangeWatchedFilesParams{ + Changes: []protocol.FileEvent{{URI: env.Sandbox.Workdir.URI("go.mod"), Type: protocol.Changed}}, + }); err != nil { + t.Fatal(err) + } + env.Await(CompletedWork(server.DiagnosticWorkTitle(server.FromDidChangeWatchedFiles), 1, true)) + + // storePackageResults runs asynchronously. In practice phase + // 2's own go-list latency provides the necessary wait (0ms + // passed 10/10 in testing), but a small margin avoids flakes + // on slow filesystems. + // TODO(rfindley): replace this sleep with a hook. + time.Sleep(100 * time.Millisecond) + + // --- Phase 2: restore a on disk; b adds import "a" --- + // b is invalidated; a is not (b had no a-edge in the prior + // graph, so addRevDeps(b) doesn't reach a). go list for b sees + // the restored a (Name="a") so the b→a edge is kept, but a's + // fresh metadata is discarded by load.go's existing-metadata + // filter. Type-checking b calls importPackage(mp_a{Name=""}, + // data{item.Name="a"}) and the bug.Errorf fires. + if err := os.WriteFile(aPath, []byte(aOriginal), 0644); err != nil { + t.Fatal(err) + } + env.SetBufferContent("b/b.go", "package b\n\nimport \"mod.com/a\"\n\nvar V a.T\n") + + // The bug.Errorf panic (under PanicOnBugs) is recovered by + // iimportCommon's defer/recover and the resulting error is + // swallowed by getPackage's errgroup, so the only observable + // effect is the spurious "could not import" diagnostic on b. + // In this test that diagnostic can only arise from the + // importPackage failure: a is a valid package and b's import + // is well-formed. + // + // Once golang/go#64235 is fixed there should be no diagnostic + // at all here, so flip this assertion to NoDiagnostics(). + env.AfterChange( + Diagnostics(env.AtRegexp("b/b.go", `"mod.com/a"`), WithMessage("could not import mod.com/a")), + ) + }) +}
diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index 33fa308..de0f557 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go
@@ -29,7 +29,6 @@ "golang.org/x/tools/gopls/internal/util/pathutil" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" - "golang.org/x/tools/internal/xcontext" ) // Editor is a fake client editor. It keeps track of client state and can be @@ -171,7 +170,7 @@ // // editor, err := NewEditor(s).Connect(ctx, conn, hooks) func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks) (*Editor, error) { - bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx)) + bgCtx, cancelConn := context.WithCancel(context.WithoutCancel(ctx)) conn := connector.Connect(bgCtx) e.cancelConn = cancelConn @@ -1296,7 +1295,7 @@ params := &protocol.CreateFilesParams{} for _, file := range files { params.Files = append(params.Files, protocol.FileCreate{ - URI: string(file), + URI: file, }) } return e.Server.DidCreateFiles(ctx, params)
diff --git a/gopls/internal/test/integration/misc/addtest_test.go b/gopls/internal/test/integration/misc/addtest_test.go index 7e15913..a5fe9bc 100644 --- a/gopls/internal/test/integration/misc/addtest_test.go +++ b/gopls/internal/test/integration/misc/addtest_test.go
@@ -19,6 +19,8 @@ -- go.mod -- module example.com +go 1.24 + -- a/a.go -- package a @@ -40,7 +42,6 @@ const want = `package a_test import ( - "context" "testing" "example.com/a" @@ -59,7 +60,7 @@ } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := a.Foo(context.Background(), tt.in) + got := a.Foo(t.Context(), tt.in) // TODO: update the condition below to compare got with tt.want. if true { t.Errorf("Foo() = %v, want %v", got, tt.want) @@ -111,10 +112,10 @@ // Pointing to the line of test function declaration. if want := (protocol.Range{ Start: protocol.Position{ - Line: 11, + Line: 10, }, End: protocol.Position{ - Line: 11, + Line: 10, }, }); *got[0].Selection != want { t.Errorf("gopls.add_test: got showDocument requests selection for %v, want %v", *got[0].Selection, want)
diff --git a/gopls/internal/test/integration/misc/generate_test.go b/gopls/internal/test/integration/misc/generate_test.go index cb325d8..36f9e16 100644 --- a/gopls/internal/test/integration/misc/generate_test.go +++ b/gopls/internal/test/integration/misc/generate_test.go
@@ -121,6 +121,8 @@ collectMessages := env.Awaiter.ListenToShownMessages() env.RegexpReplace("a/a.go", "var", "const") env.Await(env.DoneWithChange()) + // We don't need to await [ShownMessage]: the warning + // should "happen before" completion of DidChange. messages := collectMessages() const want = "Warning: editing a.go, a generated file."
diff --git a/gopls/internal/test/integration/misc/imports_test.go b/gopls/internal/test/integration/misc/imports_test.go index 6f6d45d..4269207 100644 --- a/gopls/internal/test/integration/misc/imports_test.go +++ b/gopls/internal/test/integration/misc/imports_test.go
@@ -10,7 +10,9 @@ "runtime" "strings" "testing" + "time" + "github.com/google/go-cmp/cmp" "golang.org/x/tools/gopls/internal/test/compare" . "golang.org/x/tools/gopls/internal/test/integration" "golang.org/x/tools/gopls/internal/test/integration/fake" @@ -417,7 +419,14 @@ // information. Logf is fine as the test will fail later. ix, err := modindex.Read(modcache) if err != nil { - t.Logf("could not read modcache index: %v", err) + // no index? maybe it's a rare race condition + time.Sleep(3 * time.Second) + ix, err = modindex.Read(modcache) + if err != nil { + t.Logf("could not read modcache index: %v", err) + } else { + t.Logf("re-read modcache index, found %d entries", len(ix.Entries)) + } } else if len(ix.Entries) != 2 { t.Logf("%d modcache entries", len(ix.Entries)) if len(ix.Entries) == 0 { @@ -838,3 +847,76 @@ } }) } + +// Test behavior if there is no module cache index. +// There are two cases, one where the index dir is +// unwritable, and one where it doesn't exist. +// In both cases, import "fmt" should be found, +// a missing import should run to completion and not crash. +func TestNoIndex(t *testing.T) { + const files = `-- go.mod -- +module foo +go 1.24 +-- ok.go -- +package main +var x = fmt.Println +-- fail.go -- +package main +var x = foo.Var +` + modcache := t.TempDir() + // make it unwritable + if err := os.Chmod(modcache, 0555); err != nil { + t.Fatal(err) + } + // a function testing that std lib imports are found + ok := func(t *testing.T, env *Env) { + env.OpenFile("ok.go") + env.AfterChange(env.DoneWithOpen()) + env.OrganizeImports("ok.go") + env.AfterChange(NoDiagnostics(ForFile("a.go"))) + buf := env.BufferText("ok.go") + if !strings.Contains(buf, `import "fmt"`) { + t.Errorf(`expected import "fmt" but got %q`, buf) + } + } + // a function testing missing imports don't crash + notok := func(t *testing.T, env *Env) { + env.OpenFile("fail.go") + env.AfterChange(env.DoneWithOpen()) + obuf := env.BufferText("fail.go") + env.OrganizeImports("fail.go") + buf := env.BufferText("fail.go") + if !cmp.Equal(buf, obuf) { + t.Errorf("unexpected change %q\n%s", obuf, cmp.Diff(obuf, buf)) + } + } + + dir := t.TempDir() + + // create an unwritable index dir + modindex.IndexDir = filepath.Join(dir, "nope") + if err := os.MkdirAll(modindex.IndexDir, 0555); err != nil { + t.Fatal(err) + } + + // This should pass + WithOptions(Modes(Default), + EnvVars{"GOMODCACHE": modcache}, + ).Run(t, files, ok) + // This should do nothing and not crash + WithOptions(Modes(Default), + EnvVars{"GOMODCACHE": modcache}, + ).Run(t, files, notok) + + // now with empty index dir + modindex.IndexDir = "" + + WithOptions(Modes(Default), + EnvVars{"GOMODCACHE": modcache}, + ).Run(t, files, ok) + // This should do nothing and not crash + WithOptions(Modes(Default), + EnvVars{"GOMODCACHE": modcache}, + ).Run(t, files, notok) +}
diff --git a/gopls/internal/test/integration/regtest.go b/gopls/internal/test/integration/regtest.go index 8518fe1..25ea415 100644 --- a/gopls/internal/test/integration/regtest.go +++ b/gopls/internal/test/integration/regtest.go
@@ -18,11 +18,11 @@ "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" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/tool" ) var (
diff --git a/gopls/internal/test/integration/runner.go b/gopls/internal/test/integration/runner.go index 38fe473..ae77ed1 100644 --- a/gopls/internal/test/integration/runner.go +++ b/gopls/internal/test/integration/runner.go
@@ -29,7 +29,6 @@ "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/jsonrpc2/servertest" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/internal/xcontext" ) // Mode is a bitmask that defines for which execution modes a test should run. @@ -240,7 +239,7 @@ // the editor: in general we want to clean up before proceeding to the // next test, and if there is a deadlock preventing closing it will // eventually be handled by the `go test` timeout. - if err := env.Editor.Close(xcontext.Detach(ctx)); err != nil { + if err := env.Editor.Close(context.WithoutCancel(ctx)); err != nil { t.Errorf("closing editor: %v", err) } }()
diff --git a/gopls/internal/test/integration/web/assembly_test.go b/gopls/internal/test/integration/web/assembly_test.go index b11c407..eeee431 100644 --- a/gopls/internal/test/integration/web/assembly_test.go +++ b/gopls/internal/test/integration/web/assembly_test.go
@@ -5,10 +5,12 @@ package web_test import ( + "bytes" "regexp" "runtime" "testing" + "golang.org/x/net/html" "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/settings" . "golang.org/x/tools/gopls/internal/test/integration" @@ -148,6 +150,38 @@ }) } +// TestAssemblyMisquote exercises that compiler output is properly quoted. +func TestAssemblyMisquote(t *testing.T) { + const files = ` +-- go.mod -- +module example.com + +-- a/a.go -- +package a + +func f() { + // The compiler rejects this addition. The error it prints may include + // the string literal but this should not cause the resulting HTML page + // to contain a <u> element, which would result in misformatting. + const _ = "</script><u>underline</u>" + 1 +} +` + Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("a/a.go") + loc := env.RegexpSearch("a/a.go", "const") + report := asmFor(t, env, loc) + doc, err := html.Parse(bytes.NewReader(report)) + if err != nil { + t.Fatalf("html.Parse: %v", err) + } + for n := range doc.Descendants() { + if n.Type == html.ElementNode && n.Data == "u" { + t.Fatalf("unwanted <u> element in HTML document:\n%s", report) + } + } + }) +} + // asmFor returns the HTML document served by gopls for a "Browse assembly" // command at the specified location in an open file. func asmFor(t *testing.T, env *Env, loc protocol.Location) []byte {
diff --git a/gopls/internal/test/integration/workspace/didcreatefiles_test.go b/gopls/internal/test/integration/workspace/didcreatefiles_test.go index cba0daf..50eb659 100644 --- a/gopls/internal/test/integration/workspace/didcreatefiles_test.go +++ b/gopls/internal/test/integration/workspace/didcreatefiles_test.go
@@ -144,3 +144,14 @@ }) } } + +// TestDidCreateFiles_BadURI is an integration test for go.dev/issue/74652, +// in which a bad URI passed to DidOpen would cause the server to panic. +// Now, the server uses DocumentURI (not raw string) when unmarshalling, +// causing it to sanitize properly. +func TestDidCreateFiles_BadURI(t *testing.T) { + Run(t, "", func(t *testing.T, env *Env) { + env.DidCreateFiles("badschema:badfilename") + env.AfterChange() + }) +}
diff --git a/gopls/internal/test/integration/wrappers.go b/gopls/internal/test/integration/wrappers.go index 850c8c8..b9027df 100644 --- a/gopls/internal/test/integration/wrappers.go +++ b/gopls/internal/test/integration/wrappers.go
@@ -5,6 +5,7 @@ package integration import ( + "context" "errors" "os" "path" @@ -12,7 +13,6 @@ "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/protocol/command" "golang.org/x/tools/gopls/internal/test/integration/fake" - "golang.org/x/tools/internal/xcontext" ) // RemoveWorkspaceFile deletes a file on disk but does nothing in the @@ -618,7 +618,7 @@ // Close shuts down resources associated with the environment, calling t.Error // on any error. func (e *Env) Close() { - ctx := xcontext.Detach(e.Ctx) + ctx := context.WithoutCancel(e.Ctx) if e.MCPSession != nil { if err := e.MCPSession.Close(); err != nil { e.TB.Errorf("closing MCP session: %v", err)
diff --git a/gopls/internal/test/marker/codeaction_test.go b/gopls/internal/test/marker/codeaction_test.go index ec97eac..ca16223 100644 --- a/gopls/internal/test/marker/codeaction_test.go +++ b/gopls/internal/test/marker/codeaction_test.go
@@ -310,15 +310,18 @@ // Check if the command interactivity is required. if len(cmd.FormFields) > 0 { - // Fill in "formAnswers" field from named arg "formX". - for _, name := range []string{"form0", "form1"} { // Test forms have at most two fields. - arg, ok := mark.note.NamedArgs[name] - if !ok { - break // No more answers provided by the test. + // Fill in "formAnswers" field from named arg "answers". + if rawArgs, ok := mark.note.NamedArgs["answers"]; ok { + args := make(map[string]any) + if err := json.Unmarshal([]byte(rawArgs.(string)), &args); err != nil { + mark.errorf("fail to unmarshal arguments to map[string]any: %v", err) } - - // TODO(hxjiang): support other kind of inputs. - cmd.FormAnswers = append(cmd.FormAnswers, arg.(string)) + for k, v := range args { + cmd.FormAnswers = append(cmd.FormAnswers, protocol.FormAnswer{ + ID: k, + Value: v, + }) + } } // Re-resolve command with the "formAnswers" field filled.
diff --git a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go index ee87234..378e42d 100644 --- a/gopls/internal/test/marker/doc.go +++ b/gopls/internal/test/marker/doc.go
@@ -144,15 +144,16 @@ completion candidate produced at the given location with provided label results in the given golden state. - - codeaction(start location, kind string, diag=regexp, end=location, action=golden, edit=golden, result=golden, err=stringMatcher, form0=string, form1=string) + - codeaction(start location, kind string, diag=regexp, end=location, action=golden, edit=golden, result=golden, err=stringMatcher, answers=string) Specifies a code action to request at the location, with given kind. If diag is set, the code action must be associated with the given diagnostic. If end is set, the location is defined to be between start.Start and end.End. - If form0 is set the codeaction is invoking a user dialog and the form - arguments are the user response. + If answers is set, it is a JSON string mapping form field IDs to their + respective answer values (e.g. answers=`{"tags":"xml"}`), representing the + user's response to interactive refactoring dialogs. Exactly one of action, edit, result, or err must be set: If action is set, it is a golden reference to a JSON blob representing the
diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index 309ad74..109ee90 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go
@@ -605,7 +605,7 @@ // See doc.go for marker documentation. var actionMarkerFuncs = map[string]func(marker){ "acceptcompletion": actionMarkerFunc(acceptCompletionMarker), - "codeaction": actionMarkerFunc(codeActionMarker, "end", "diag", "action", "result", "edit", "err", "form0", "form1"), + "codeaction": actionMarkerFunc(codeActionMarker, "end", "diag", "action", "result", "edit", "err", "answers"), "codelenses": actionMarkerFunc(codeLensesMarker), "complete": actionMarkerFunc(completeMarker), "def": actionMarkerFunc(defMarker), @@ -1493,10 +1493,11 @@ } } -// checkDiffs checks that the diff content stored in the given golden directory -// converts the original contents into the changed contents. -// (This logic is strange because hundreds of existing marker tests were created -// containing a modified version of unified diffs.) +// checkDiffs checks that the diff content stored in the given golden +// directory converts the original contents into the changed contents. +// (It does _not_ check that the diff has a specific notation, as +// there are many valid ways to notate the same patch and we do not +// want tests to break whenever our diff algorithm evolves.) func checkDiffs(mark marker, changed map[string][]byte, golden *Golden) { for name, after := range changed { before := mark.run.env.FileContent(name) @@ -1517,23 +1518,19 @@ } // the call to Get is so that the -update flag will update the test. // normally it just returns 'got'. - if tdiffs, ok := golden.Get(mark.T(), name, []byte(got)); !ok { + tdiffs, ok := golden.Get(mark.T(), name, []byte(got)) + if !ok { mark.errorf("%s: unexpected change to file %s; got diff:\n%s", mark.note.Name, name, got) return - } else { - // restore the ToUnified header lines deleted above - // before calling ApplyUnified - diffsFromTest := "--- \n+++ \n" + string(tdiffs) - want, err := diff.ApplyUnified(diffsFromTest, before) - if err != nil { - mark.errorf("%s: ApplyUnified(%q,%q) failed: %v", - mark.note.Name, before, after, err) - } - if want != string(after) { - mark.errorf("%s: got\n%q expected\n%q", - mark.note.Name, want, string(after)) - } + } + want, err := diff.ApplyUnified(string(tdiffs), before) + if err != nil { + mark.errorf("in @%s golden section %s: %v; actual diff:\n%s", + mark.note.Name, name, err, d) + } + if string(after) != want { + mark.errorf("%s: got:\n%s\nwant:\n%s", mark.note.Name, after, want) } }
diff --git a/gopls/internal/test/marker/testdata/codeaction/addtest.txt b/gopls/internal/test/marker/testdata/codeaction/addtest.txt index 7e75cd1..a37ee11 100644 --- a/gopls/internal/test/marker/testdata/codeaction/addtest.txt +++ b/gopls/internal/test/marker/testdata/codeaction/addtest.txt
@@ -6,7 +6,7 @@ -- go.mod -- module golang.org/lsptests/addtest -go 1.18 +go 1.24 -- copyrightandbuildconstraint/copyrightandbuildconstraint.go -- // Copyright 2020 The Go Authors. All rights reserved. @@ -1448,21 +1448,14 @@ -- contextinput/contextinput_test.go -- package main_test -import renamedctx "context" - -var local renamedctx.Context - -- @function_context/contextinput/contextinput_test.go -- -@@ -3 +3,3 @@ --import renamedctx "context" +@@ -3 +3,32 @@ +import ( -+ renamedctx "context" + "testing" -@@ -5 +7,3 @@ ++ + "golang.org/lsptests/addtest/contextinput" +) + -@@ -7 +12,26 @@ + +func TestFunction(t *testing.T) { + tests := []struct { @@ -1475,7 +1468,7 @@ + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { -+ got, got2, got3 := main.Function(renamedctx.Background(), "", "") ++ got, got2, got3 := main.Function(t.Context(), "", "") + // TODO: update the condition below to compare got with tt.want. + if true { + t.Errorf("Function() = %v, want %v", got, tt.want) @@ -1490,16 +1483,13 @@ + } +} -- @method_context/contextinput/contextinput_test.go -- -@@ -3 +3,3 @@ --import renamedctx "context" +@@ -3 +3,36 @@ +import ( -+ renamedctx "context" + "testing" -@@ -5 +7,3 @@ ++ + "golang.org/lsptests/addtest/contextinput" +) + -@@ -7 +12,30 @@ + +func TestFoo_Method(t *testing.T) { + tests := []struct { @@ -1512,11 +1502,11 @@ + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { -+ f, err := main.NewFoo(renamedctx.Background()) ++ f, err := main.NewFoo(t.Context()) + if err != nil { + t.Fatalf("could not construct receiver type: %v", err) + } -+ got, got2, got3 := f.Method(renamedctx.Background(), "", "") ++ got, got2, got3 := f.Method(t.Context(), "", "") + // TODO: update the condition below to compare got with tt.want. + if true { + t.Errorf("Method() = %v, want %v", got, tt.want)
diff --git a/gopls/internal/test/marker/testdata/codeaction/addtest_pre124.txt b/gopls/internal/test/marker/testdata/codeaction/addtest_pre124.txt new file mode 100644 index 0000000..dab169f --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/addtest_pre124.txt
@@ -0,0 +1,48 @@ +This test checks the fallback behavior of the 'add test for FUNC' code action +when the module's Go version is older than 1.24 (before testing.T.Context was +available): the generated test must continue to use context.Background() and +reuse the existing rename of the "context" import in the test file. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module example.com + +go 1.23 + +-- a/a.go -- +package a + +import "context" + +func Foo(ctx context.Context) {} //@codeaction("Foo", "source.addTest", edit=foo) + +-- a/a_test.go -- +package a_test + +import renamedctx "context" + +-- @foo/a/a_test.go -- +@@ -3 +3,3 @@ +-import renamedctx "context" ++import ( ++ renamedctx "context" ++ "testing" +@@ -5 +7,16 @@ ++ "example.com/a" ++) ++ ++ ++func TestFoo(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ a.Foo(renamedctx.Background()) ++ }) ++ } ++}
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_add_tags.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_add_tags.txt index 2a7e25d..890412c 100644 --- a/gopls/internal/test/marker/testdata/codeaction/dialog_add_tags.txt +++ b/gopls/internal/test/marker/testdata/codeaction/dialog_add_tags.txt
@@ -2,7 +2,7 @@ -- capabilities.json -- { - "experimental":{"interactiveInputTypes":["string","bool","number","enum"]} + "experimental":{"interactiveResolve":{"inputTypes":["string","bool","number","enum"]}} } -- flags -- @@ -13,25 +13,25 @@ package addtags type A struct { - x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase") - y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase") - z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase") + x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, answers=`{"tags":"xml","transform":"camelcase"}`) + y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, answers=`{"tags":"xml","transform":"camelcase"}`) + z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, answers=`{"tags":"xml","transform":"camelcase"}`) } -- @entirestruct/addtags.go -- @@ -4,3 +4,3 @@ -- x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase") -- y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase") -- z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase") -+ x int `xml:"x"` //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase") -+ y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase") -+ z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase") +- x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, answers=`{"tags":"xml","transform":"camelcase"}`) +- y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, answers=`{"tags":"xml","transform":"camelcase"}`) +- z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, answers=`{"tags":"xml","transform":"camelcase"}`) ++ x int `xml:"x"` //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, answers=`{"tags":"xml","transform":"camelcase"}`) ++ y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, answers=`{"tags":"xml","transform":"camelcase"}`) ++ z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, answers=`{"tags":"xml","transform":"camelcase"}`) -- @singleline/addtags.go -- @@ -4 +4 @@ -- x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase") -+ x int `xml:"x"` //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, form0="xml", form1="camelcase") +- x int //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, answers=`{"tags":"xml","transform":"camelcase"}`) ++ x int `xml:"x"` //@codeaction("x", "refactor.rewrite.addTags", edit=singleline, answers=`{"tags":"xml","transform":"camelcase"}`) -- @twolines/addtags.go -- @@ -5,2 +5,2 @@ -- y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase") -- z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase") -+ y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, form0="xml", form1="camelcase") -+ z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, form0="xml", form1="camelcase") +- y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, answers=`{"tags":"xml","transform":"camelcase"}`) +- z int //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, answers=`{"tags":"xml","transform":"camelcase"}`) ++ y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.addTags", edit=twolines, answers=`{"tags":"xml","transform":"camelcase"}`) ++ z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.addTags", edit=entirestruct, answers=`{"tags":"xml","transform":"camelcase"}`)
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_implement_interface.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_implement_interface.txt index 143b749..12751c6 100644 --- a/gopls/internal/test/marker/testdata/codeaction/dialog_implement_interface.txt +++ b/gopls/internal/test/marker/testdata/codeaction/dialog_implement_interface.txt
@@ -3,7 +3,7 @@ -- capabilities.json -- { - "experimental":{"interactiveInputTypes":["string"]} + "experimental":{"interactiveResolve":{"inputTypes":["string"]}} } -- flags -- @@ -21,29 +21,29 @@ type foo struct{} // Named -type namedOfPointer *foo //@codeaction("Pointer", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") +type namedOfPointer *foo //@codeaction("Pointer", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) -type namedOfInterface error //@codeaction("Interface", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") +type namedOfInterface error //@codeaction("Interface", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) // Alias -type aliasOfBasic = int //@codeaction("Basic", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") +type aliasOfBasic = int //@codeaction("Basic", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) -type aliasOfInterface = error //@codeaction("Interface", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") +type aliasOfInterface = error //@codeaction("Interface", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) -type aliasOfPointer = *foo //@codeaction("Pointer", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") +type aliasOfPointer = *foo //@codeaction("Pointer", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) -type aliasOfNamedPointer namedOfPointer //@codeaction("Pointer", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") +type aliasOfNamedPointer namedOfPointer //@codeaction("Pointer", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) -type aliasOfNameInterface namedOfInterface //@codeaction("Interface", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") +type aliasOfNameInterface namedOfInterface //@codeaction("Interface", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) // Interface -type iface interface {} //@codeaction("iface", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") +type iface interface {} //@codeaction("iface", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) // Not package level func _ () { - type NamedOfBasic int //@codeaction("Basic", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") + type NamedOfBasic int //@codeaction("Basic", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) - type NamedOfStruct foo //@codeaction("Struct", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", form0="error") + type NamedOfStruct foo //@codeaction("Struct", "refactor.rewrite.implementInterface", err=re"found 0 CodeActions", answers=`{"interface":"error"}`) } -- good/good.go -- @@ -52,27 +52,27 @@ type foo struct{} // Named -type namedOfBasic int //@codeaction("Basic", "refactor.rewrite.implementInterface", edit=namedBasic, form0="error") +type namedOfBasic int //@codeaction("Basic", "refactor.rewrite.implementInterface", edit=namedBasic, answers=`{"interface":"error"}`) -type namedOfStruct foo //@codeaction("Struct", "refactor.rewrite.implementInterface", edit=namedStruct, form0="error") +type namedOfStruct foo //@codeaction("Struct", "refactor.rewrite.implementInterface", edit=namedStruct, answers=`{"interface":"error"}`) -type namedOfFunc func(string) bool //@codeaction("Func", "refactor.rewrite.implementInterface", edit=namedFunc, form0="error") +type namedOfFunc func(string) bool //@codeaction("Func", "refactor.rewrite.implementInterface", edit=namedFunc, answers=`{"interface":"error"}`) -type namedOfChannel chan struct{} //@codeaction("Channel", "refactor.rewrite.implementInterface", edit=namedChannel, form0="error") +type namedOfChannel chan struct{} //@codeaction("Channel", "refactor.rewrite.implementInterface", edit=namedChannel, answers=`{"interface":"error"}`) // Struct -type structType struct{} //@codeaction("Type", "refactor.rewrite.implementInterface", edit=struct, form0="error") +type structType struct{} //@codeaction("Type", "refactor.rewrite.implementInterface", edit=struct, answers=`{"interface":"error"}`) -type genericStructType[T any] struct{} //@codeaction("Type", "refactor.rewrite.implementInterface", edit=genericStruct, form0="error") +type genericStructType[T any] struct{} //@codeaction("Type", "refactor.rewrite.implementInterface", edit=genericStruct, answers=`{"interface":"error"}`) // Alias -type aliasOfNamedBasic namedOfBasic //@codeaction("Basic", "refactor.rewrite.implementInterface", edit=aliasNamedBasic, form0="error") +type aliasOfNamedBasic namedOfBasic //@codeaction("Basic", "refactor.rewrite.implementInterface", edit=aliasNamedBasic, answers=`{"interface":"error"}`) -type aliasOfNamedStruct namedOfStruct //@codeaction("Struct", "refactor.rewrite.implementInterface", edit=aliasNamedStruct, form0="error") +type aliasOfNamedStruct namedOfStruct //@codeaction("Struct", "refactor.rewrite.implementInterface", edit=aliasNamedStruct, answers=`{"interface":"error"}`) -- @aliasNamedBasic/good/good.go -- @@ -20 +20,8 @@ --type aliasOfNamedBasic namedOfBasic //@codeaction("Basic", "refactor.rewrite.implementInterface", edit=aliasNamedBasic, form0="error") +-type aliasOfNamedBasic namedOfBasic //@codeaction("Basic", "refactor.rewrite.implementInterface", edit=aliasNamedBasic, answers=`{"interface":"error"}`) +type aliasOfNamedBasic namedOfBasic + +// Error implements [error]. @@ -80,12 +80,12 @@ + panic("unimplemented") +} + -+//@codeaction("Basic", "refactor.rewrite.implementInterface", edit=aliasNamedBasic, form0="error") ++//@codeaction("Basic", "refactor.rewrite.implementInterface", edit=aliasNamedBasic, answers=`{"interface":"error"}`) @@ -23 +30 @@ - -- @aliasNamedStruct/good/good.go -- @@ -22 +22,6 @@ --type aliasOfNamedStruct namedOfStruct //@codeaction("Struct", "refactor.rewrite.implementInterface", edit=aliasNamedStruct, form0="error") +-type aliasOfNamedStruct namedOfStruct //@codeaction("Struct", "refactor.rewrite.implementInterface", edit=aliasNamedStruct, answers=`{"interface":"error"}`) +type aliasOfNamedStruct namedOfStruct + +// Error implements [error]. @@ -93,10 +93,10 @@ + panic("unimplemented") +} @@ -24 +29 @@ -+//@codeaction("Struct", "refactor.rewrite.implementInterface", edit=aliasNamedStruct, form0="error") ++//@codeaction("Struct", "refactor.rewrite.implementInterface", edit=aliasNamedStruct, answers=`{"interface":"error"}`) -- @genericStruct/good/good.go -- @@ -17 +17,8 @@ --type genericStructType[T any] struct{} //@codeaction("Type", "refactor.rewrite.implementInterface", edit=genericStruct, form0="error") +-type genericStructType[T any] struct{} //@codeaction("Type", "refactor.rewrite.implementInterface", edit=genericStruct, answers=`{"interface":"error"}`) +type genericStructType[T any] struct{} + +// Error implements [error]. @@ -104,12 +104,12 @@ + panic("unimplemented") +} + -+//@codeaction("Type", "refactor.rewrite.implementInterface", edit=genericStruct, form0="error") ++//@codeaction("Type", "refactor.rewrite.implementInterface", edit=genericStruct, answers=`{"interface":"error"}`) @@ -23 +30 @@ - -- @namedBasic/good/good.go -- @@ -6 +6,8 @@ --type namedOfBasic int //@codeaction("Basic", "refactor.rewrite.implementInterface", edit=namedBasic, form0="error") +-type namedOfBasic int //@codeaction("Basic", "refactor.rewrite.implementInterface", edit=namedBasic, answers=`{"interface":"error"}`) +type namedOfBasic int + +// Error implements [error]. @@ -117,12 +117,12 @@ + panic("unimplemented") +} + -+//@codeaction("Basic", "refactor.rewrite.implementInterface", edit=namedBasic, form0="error") ++//@codeaction("Basic", "refactor.rewrite.implementInterface", edit=namedBasic, answers=`{"interface":"error"}`) @@ -23 +30 @@ - -- @namedChannel/good/good.go -- @@ -12 +12,8 @@ --type namedOfChannel chan struct{} //@codeaction("Channel", "refactor.rewrite.implementInterface", edit=namedChannel, form0="error") +-type namedOfChannel chan struct{} //@codeaction("Channel", "refactor.rewrite.implementInterface", edit=namedChannel, answers=`{"interface":"error"}`) +type namedOfChannel chan struct{} + +// Error implements [error]. @@ -130,12 +130,12 @@ + panic("unimplemented") +} + -+//@codeaction("Channel", "refactor.rewrite.implementInterface", edit=namedChannel, form0="error") ++//@codeaction("Channel", "refactor.rewrite.implementInterface", edit=namedChannel, answers=`{"interface":"error"}`) @@ -23 +30 @@ - -- @namedFunc/good/good.go -- @@ -10 +10,8 @@ --type namedOfFunc func(string) bool //@codeaction("Func", "refactor.rewrite.implementInterface", edit=namedFunc, form0="error") +-type namedOfFunc func(string) bool //@codeaction("Func", "refactor.rewrite.implementInterface", edit=namedFunc, answers=`{"interface":"error"}`) +type namedOfFunc func(string) bool + +// Error implements [error]. @@ -143,12 +143,12 @@ + panic("unimplemented") +} + -+//@codeaction("Func", "refactor.rewrite.implementInterface", edit=namedFunc, form0="error") ++//@codeaction("Func", "refactor.rewrite.implementInterface", edit=namedFunc, answers=`{"interface":"error"}`) @@ -23 +30 @@ - -- @namedStruct/good/good.go -- @@ -8 +8,8 @@ --type namedOfStruct foo //@codeaction("Struct", "refactor.rewrite.implementInterface", edit=namedStruct, form0="error") +-type namedOfStruct foo //@codeaction("Struct", "refactor.rewrite.implementInterface", edit=namedStruct, answers=`{"interface":"error"}`) +type namedOfStruct foo + +// Error implements [error]. @@ -156,12 +156,12 @@ + panic("unimplemented") +} + -+//@codeaction("Struct", "refactor.rewrite.implementInterface", edit=namedStruct, form0="error") ++//@codeaction("Struct", "refactor.rewrite.implementInterface", edit=namedStruct, answers=`{"interface":"error"}`) @@ -23 +30 @@ - -- @struct/good/good.go -- @@ -15 +15,8 @@ --type structType struct{} //@codeaction("Type", "refactor.rewrite.implementInterface", edit=struct, form0="error") +-type structType struct{} //@codeaction("Type", "refactor.rewrite.implementInterface", edit=struct, answers=`{"interface":"error"}`) +type structType struct{} + +// Error implements [error]. @@ -169,7 +169,7 @@ + panic("unimplemented") +} + -+//@codeaction("Type", "refactor.rewrite.implementInterface", edit=struct, form0="error") ++//@codeaction("Type", "refactor.rewrite.implementInterface", edit=struct, answers=`{"interface":"error"}`) @@ -23 +30 @@ - -- cycle/a/a.go -- @@ -200,7 +200,7 @@ const C = 1 // Package a -> b -> named -x-> a. -type Named struct {} //@codeaction("Named", "refactor.rewrite.implementInterface", err=re"import cycle", form0="golang.org/lsptests/implementinterface/cycle/a.A") +type Named struct {} //@codeaction("Named", "refactor.rewrite.implementInterface", err=re"import cycle", answers=`{"interface":"golang.org/lsptests/implementinterface/cycle/a.A"}`) -- cycle/alias/alias.go -- package alias @@ -211,21 +211,21 @@ // Method should be added to the named type [named.Named]. // Package a -> b -> named -x-> a. -type Alias = named.Named //@codeaction("Alias", "refactor.rewrite.implementInterface", err=re"import cycle", form0="golang.org/lsptests/implementinterface/cycle/a.A") +type Alias = named.Named //@codeaction("Alias", "refactor.rewrite.implementInterface", err=re"import cycle", answers=`{"interface":"golang.org/lsptests/implementinterface/cycle/a.A"}`) -- visibility/foo/ok.go -- package foo // We can implement an internal interface without importing its package, as long // as its methods only use built-in or accessible types. -type Ok struct{} //@codeaction("Ok", "refactor.rewrite.implementInterface", edit=visible, form0="golang.org/lsptests/implementinterface/visibility/bar/internal/bar.VisibleTypes") +type Ok struct{} //@codeaction("Ok", "refactor.rewrite.implementInterface", edit=visible, answers=`{"interface":"golang.org/lsptests/implementinterface/visibility/bar/internal/bar.VisibleTypes"}`) -- visibility/foo/notok.go -- package foo // We cannot implement this interface because its method signature requires // importing the internal, inaccessible type bar.Bar. -type NotOk struct{} //@codeaction("NotOk", "refactor.rewrite.implementInterface", err=re"inaccessible package", form0="golang.org/lsptests/implementinterface/visibility/bar/internal/bar.InvisibleTypes") +type NotOk struct{} //@codeaction("NotOk", "refactor.rewrite.implementInterface", err=re"inaccessible package", answers=`{"interface":"golang.org/lsptests/implementinterface/visibility/bar/internal/bar.InvisibleTypes"}`) -- visibility/bar/internal/bar/bar.go -- package bar @@ -242,7 +242,7 @@ -- @visible/visibility/foo/ok.go -- @@ -5 +5,6 @@ --type Ok struct{} //@codeaction("Ok", "refactor.rewrite.implementInterface", edit=visible, form0="golang.org/lsptests/implementinterface/visibility/bar/internal/bar.VisibleTypes") +-type Ok struct{} //@codeaction("Ok", "refactor.rewrite.implementInterface", edit=visible, answers=`{"interface":"golang.org/lsptests/implementinterface/visibility/bar/internal/bar.VisibleTypes"}`) +type Ok struct{} + +// Bar implements [bar.VisibleTypes]. @@ -250,4 +250,4 @@ + panic("unimplemented") +} @@ -7 +12 @@ -+//@codeaction("Ok", "refactor.rewrite.implementInterface", edit=visible, form0="golang.org/lsptests/implementinterface/visibility/bar/internal/bar.VisibleTypes") ++//@codeaction("Ok", "refactor.rewrite.implementInterface", edit=visible, answers=`{"interface":"golang.org/lsptests/implementinterface/visibility/bar/internal/bar.VisibleTypes"}`)
diff --git a/gopls/internal/test/marker/testdata/codeaction/dialog_remove_tags.txt b/gopls/internal/test/marker/testdata/codeaction/dialog_remove_tags.txt index bb57428..68811b4 100644 --- a/gopls/internal/test/marker/testdata/codeaction/dialog_remove_tags.txt +++ b/gopls/internal/test/marker/testdata/codeaction/dialog_remove_tags.txt
@@ -12,25 +12,25 @@ package removetags type A struct { - x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml") - y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml") - z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml") + x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, answers=`{"tags":"xml"}`) + y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, answers=`{"tags":"xml"}`) + z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, answers=`{"tags":"xml"}`) } -- @entirestruct/removetags.go -- @@ -4,3 +4,3 @@ -- x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml") -- y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml") -- z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml") -+ x int //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml") -+ y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml") -+ z int //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml") +- x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, answers=`{"tags":"xml"}`) +- y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, answers=`{"tags":"xml"}`) +- z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, answers=`{"tags":"xml"}`) ++ x int //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, answers=`{"tags":"xml"}`) ++ y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, answers=`{"tags":"xml"}`) ++ z int //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, answers=`{"tags":"xml"}`) -- @singleline/removetags.go -- @@ -4 +4 @@ -- x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml") -+ x int //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, form0="xml") +- x int `xml:"x"` //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, answers=`{"tags":"xml"}`) ++ x int //@codeaction("x", "refactor.rewrite.removeTags", edit=singleline, answers=`{"tags":"xml"}`) -- @twolines/removetags.go -- @@ -5,2 +5,2 @@ -- y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml") -- z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml") -+ y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, form0="xml") -+ z int //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, form0="xml") +- y int `xml:"y"` //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, answers=`{"tags":"xml"}`) +- z int `xml:"z"` //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, answers=`{"tags":"xml"}`) ++ y int //@codeaction(re`(?s)y.*.z int`, "refactor.rewrite.removeTags", edit=twolines, answers=`{"tags":"xml"}`) ++ z int //@codeaction(re`()n`, "refactor.rewrite.removeTags", edit=entirestruct, answers=`{"tags":"xml"}`)
diff --git a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt index 0e363f8..9280323 100644 --- a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt +++ b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt
@@ -17,11 +17,10 @@ } -- @out/a/a.go -- -@@ -7 +7 @@ +@@ -7 +7,5 @@ - { println(logf) } //@loc(block, re`{[^}]*}`) + { newFunction(logf) } //@loc(block, re`{[^}]*}`) -@@ -10 +10,4 @@ -+func newFunction(logf func( string, ...any)) { -+ println(logf) +} + ++func newFunction(logf func(string, ...any)) { ++ println(logf)
diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt index 8b5bab4..2588ba9 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt
@@ -619,10 +619,16 @@ } func _() { - // Note: the golden content for issue63921 is empty: fillstruct produces no - // edits, but does not panic. + // Note: fillstruct now skips invalid/unresolvable fields (like Undefined) + // rather than aborting completely, it successfully fills the valid field F. invalidStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue63921) } +-- @issue63921/issue63921.go -- +@@ -12 +12,3 @@ +- invalidStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue63921) ++ invalidStruct{ ++ F: 0, ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue63921) -- named/named.go -- package named @@ -761,3 +767,36 @@ @@ -24 +24,2 @@ + Edges: map[*Node]*Node{}, + Other: "", +-- issue80033.go -- +package fillstruct +// Test behavior for a struct inside an anonymous struct or anonymous interface. +type OtherStruct struct{} + +type MapAnonStruct struct { + bar map[string]struct { + fizz OtherStruct + } +} + +type InterfaceAnonStruct struct { + bar []interface { + fizz(OtherStruct) + } +} + +func _() { + _ = &MapAnonStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue80033) + _ = &InterfaceAnonStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue80033_2) +} +-- @issue80033/issue80033.go -- +@@ -18 +18,3 @@ +- _ = &MapAnonStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue80033) ++ _ = &MapAnonStruct{ ++ bar: map[string]struct{ fizz OtherStruct }{}, ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue80033) +-- @issue80033_2/issue80033.go -- +@@ -19 +19,3 @@ +- _ = &InterfaceAnonStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue80033_2) ++ _ = &InterfaceAnonStruct{ ++ bar: []interface{ fizz(OtherStruct) }{}, ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue80033_2)
diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct_go126.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct_go126.txt new file mode 100644 index 0000000..06b8750 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_go126.txt
@@ -0,0 +1,78 @@ +This file tests the behavior of the 'fill struct' code action in a Go 1.26 module, +focusing on how gopls handles direct references to embedded fields in struct literals +(a feature introduced in Go 1.27, see golang/go#9859). + +-- flags -- +-min_go_command=go1.26 +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/fillstruct + +go 1.26 + +-- malformed/methodasfield.go -- +package malformed + +type foo struct { + A, B, C string +} + +func (f *foo) Bar() {} + +// Test that we don't crash when a method is used as a struct literal key. +var _ = foo{ + Bar: 0, +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=methodasfield) +-- @methodasfield/malformed/methodasfield.go -- +@@ -12 +12,3 @@ ++ A: "", ++ B: "", ++ C: "", +-- malformed/fieldconflict.go -- +package malformed + +type a struct { + b + a1 string + a2 string +} + +type b struct { + b1 string +} + +// In Go 1.26, direct reference to embedded fields (like b1: "") is invalid. +// gopls treats "b1" as unknown, does not detect a conflict with b, and +// suggests filling direct fields a1 and a2. +var _ = a{ + b: b{}, + b1: "", +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=fieldconflict) +-- @fieldconflict/malformed/fieldconflict.go -- +@@ -19 +19,2 @@ ++ a1: "", ++ a2: "", +-- malformed/embeddedfields.go -- +package malformed + +type E struct { + E1, E2 string +} + +type T struct { + E + T1, T2 string +} + +// In Go 1.26, direct reference to embedded fields (like E1: "") is invalid. +// gopls treat "E1" as unknown, does not flatten E, and suggests filling direct +// fields E, T1, and T2. +var _ = T{ + E1: "", +} //@codeaction("}", "refactor.rewrite.fillStruct", edit=embeddedfieldsgo) +-- @embeddedfieldsgo/malformed/embeddedfields.go -- +@@ -17 +17,3 @@ ++ E: E{}, ++ T1: "", ++ T2: "",
diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct_go127.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct_go127.txt new file mode 100644 index 0000000..3f3aea1 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_go127.txt
@@ -0,0 +1,85 @@ +Test for fill struct on promoted fields in struct literals. +Ref: go.dev/issues/78553 + +-- flags -- +-min_go_command=go1.27 +-ignore_extra_diags + +-- go.mod -- +module mod.com + +go 1.27 + +-- main.go -- +package main + +type F struct { + F1 int + F2 int +} + +type E struct { + E1 int + E2 int + F +} + +type T struct { + E + F int +} + +// Fill in struct at T level. +var _ = T{E: E{}} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_T_F) + +var _ = T{F: 0} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_T_E) + +// Fill in struct at T.E level: T.E.F shadowed by T.F. +var _ = T{E1: 0} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", err=re"shadowed") + +var _ = T{E1: 0, E2: 0, F: 0} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", err=re"shadowed") + +// Fill in struct at T.E.F level: T.E.F does not need to be promoted. +var _ = T{F1: 0} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_all) + +-- @fill_T_E/main.go -- +@@ -22 +22,4 @@ +-var _ = T{F: 0} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_T_E) ++var _ = T{ ++ F: 0, ++ E: E{}, ++} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_T_E) +-- @fill_T_F/main.go -- +@@ -20 +20,4 @@ +-var _ = T{E: E{}} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_T_F) ++var _ = T{ ++ E: E{}, ++ F: 0, ++} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_T_F) +-- @fill_all/main.go -- +@@ -30 +30,7 @@ +-var _ = T{F1: 0} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_all) ++var _ = T{ ++ F1: 0, ++ E1: 0, ++ E2: 0, ++ F2: 0, ++ F: 0, ++} //@codeaction(re"T{()", "refactor.rewrite.fillStruct", edit=fill_all) +-- malformed/fieldconflict.go -- +package malformed + +type a struct { + b + a1 string + a2 string +} + +type b struct { + b1 string +} + +var _ = a{ + b: b{}, + b1: "", +} //@codeaction("}", "refactor.rewrite.fillStruct", err=re"cannot fill both .* and its subfields")
diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt index f2dc7a1..7381e09 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt
@@ -607,10 +607,16 @@ } func _() { - // Note: the golden content for issue63921 is empty: fillstruct produces no - // edits, but does not panic. + // Note: fillstruct now skips invalid/unresolvable fields (like Undefined) + // rather than aborting completely, it successfully fills the valid field F. invalidStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue63921) } +-- @issue63921/issue63921.go -- +@@ -12 +12,3 @@ +- invalidStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue63921) ++ invalidStruct{ ++ F: 0, ++ } //@codeaction("}", "refactor.rewrite.fillStruct", edit=issue63921) -- named/named.go -- package named
diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_issue74653.txt b/gopls/internal/test/marker/testdata/codeaction/inline_issue74653.txt new file mode 100644 index 0000000..1e3aa44 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/inline_issue74653.txt
@@ -0,0 +1,36 @@ +Test case for golang/go#74653. Inlining a generic function where a parameter type's +selector name matches the type parameter name (e.g. p.Value) should not cause +a type mismatch panic during type parameter substitution. + +-- go.mod -- +module example.com + +go 1.18 + +-- p/p.go -- +package p + +type Value struct{} + +-- a.go -- +package foo + +import "example.com/p" + +func F[Value any](v p.Value) {} + +func _() { + F[*int](p.Value{}) //@ codeaction("F", "refactor.inline.call", result=inline) +} + +-- @inline/a.go -- +package foo + +import "example.com/p" + +func F[Value any](v p.Value) {} + +func _() { + //@ codeaction("F", "refactor.inline.call", result=inline) +} +
diff --git a/gopls/internal/test/marker/testdata/codeaction/movetype.txt b/gopls/internal/test/marker/testdata/codeaction/movetype.txt new file mode 100644 index 0000000..68e840e --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/movetype.txt
@@ -0,0 +1,94 @@ +This test checks the behavior of the 'move type' code action. + +-- settings.json -- +{ + "moveType": true +} + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module example.com + +-- movetype/struct.go -- +package movetype + +import ( + "time" + "html/template" +) + +// This comment gets deleted. +type Struct struct { //@ codeaction("Struct", "refactor.move.moveType", result=basic) + Seconds time.Duration +} + +type ( + S1 struct { //@ codeaction("S1", "refactor.move.moveType", result=single) + Template template.HTML + } + + S2 struct { + Template template.HTML + } +) + +-- @basic/movetype/struct.go -- +package movetype + +import ( + + "html/template" +) + + +type ( + S1 struct { //@ codeaction("S1", "refactor.move.moveType", result=single) + Template template.HTML + } + + S2 struct { + Template template.HTML + } +) + +-- @single/movetype/struct.go -- +package movetype + +import ( + "time" + "html/template" +) + +// This comment gets deleted. +type Struct struct { //@ codeaction("Struct", "refactor.move.moveType", result=basic) + Seconds time.Duration +} + +type ( + + S2 struct { + Template template.HTML + } +) + +-- movetype/otherpkg/destfile.go -- +package otherpkg + +-- @basic/movetype/otherpkg/destfile.go -- +package otherpkg + +import "time" + +type Struct struct { //@ codeaction("Struct", "refactor.move.moveType", result=basic) + Seconds time.Duration +} +-- @single/movetype/otherpkg/destfile.go -- +package otherpkg + +import "html/template" + +type S1 struct { //@ codeaction("S1", "refactor.move.moveType", result=single) + Template template.HTML +}
diff --git a/gopls/internal/test/marker/testdata/codeaction/movetype_comments.txt b/gopls/internal/test/marker/testdata/codeaction/movetype_comments.txt new file mode 100644 index 0000000..6310ac7 --- /dev/null +++ b/gopls/internal/test/marker/testdata/codeaction/movetype_comments.txt
@@ -0,0 +1,74 @@ +This test checks that comments are preserved during the 'move type' code action. + +-- settings.json -- +{ + "moveType": true +} + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module example.com + +-- movetype/comments.go -- +package movetype + +type A struct { //@codeaction("A", "refactor.move.moveType", result=comments) + B int // comment B + C int // comment C +} // not copied + +type ( + D struct { + E int // comment E + F int // comment F + } + G struct { //@codeaction("G", "refactor.move.moveType", result=comments_single) + H int // comment H + } +) + +-- @comments/movetype/comments.go -- +package movetype + +// not copied + +type ( + D struct { + E int // comment E + F int // comment F + } + G struct { //@codeaction("G", "refactor.move.moveType", result=comments_single) + H int // comment H + } +) + +-- @comments/movetype/otherpkg/destfile.go -- +package otherpkg +type A struct { //@codeaction("A", "refactor.move.moveType", result=comments) + B int // comment B + C int // comment C +} +-- @comments_single/movetype/comments.go -- +package movetype + +type A struct { //@codeaction("A", "refactor.move.moveType", result=comments) + B int // comment B + C int // comment C +} // not copied + +type ( + D struct { + E int // comment E + F int // comment F + } + ) + +-- @comments_single/movetype/otherpkg/destfile.go -- +package otherpkg +type G struct { //@codeaction("G", "refactor.move.moveType", result=comments_single) + H int // comment H +} +-- movetype/otherpkg/destfile.go -- +package otherpkg
diff --git a/gopls/internal/test/marker/testdata/completion/issue59096.txt b/gopls/internal/test/marker/testdata/completion/issue59096.txt index 1573004..23d82c4 100644 --- a/gopls/internal/test/marker/testdata/completion/issue59096.txt +++ b/gopls/internal/test/marker/testdata/completion/issue59096.txt
@@ -2,11 +2,6 @@ type-assert expression was panicking because gopls was translating it into a (malformed) selector expr. --- settings.json -- -{ - "importsSource": "gopls" -} - -- go.mod -- module example.com
diff --git a/gopls/internal/test/marker/testdata/completion/issue60545.txt b/gopls/internal/test/marker/testdata/completion/issue60545.txt index 0f0bb6a..4d20497 100644 --- a/gopls/internal/test/marker/testdata/completion/issue60545.txt +++ b/gopls/internal/test/marker/testdata/completion/issue60545.txt
@@ -5,11 +5,6 @@ go 1.18 --- settings.json -- -{ - "importsSource": "gopls" -} - -- main.go -- package main
diff --git a/gopls/internal/test/marker/testdata/completion/issue78553.txt b/gopls/internal/test/marker/testdata/completion/issue78553.txt index 23e813a..34fb489 100644 --- a/gopls/internal/test/marker/testdata/completion/issue78553.txt +++ b/gopls/internal/test/marker/testdata/completion/issue78553.txt
@@ -4,9 +4,6 @@ -- flags -- -min_go_command=go1.27 --- skip -- -Skipping as this feature is not yet implemented. Ref: go.dev/issues/78553 - -- go.mod -- module mod.com @@ -16,21 +13,32 @@ package main type E1 struct { - A int //@item(fieldA, "A", "int", "field") + A int //@item(fieldA, "A", "int", "field"),item(structE1, "E1", "struct{...}", "struct") } type E2 struct { - E1 + E1 //@item(fieldE1, "E1", "E1", "field"),item(structE2, "E2", "struct{...}", "struct") B int //@item(fieldB, "B", "int", "field") } type T struct { - E2 //@item(fieldE2, "E2", "E2", "field") + E2 //@item(fieldE2, "E2", "E2", "field"),item(structT, "T", "struct{...}", "struct") C int //@item(fieldC, "C", "int", "field") } +//@item(literalE2, "E2{}", "", "var") +//@item(funcMain, "main", "func()", "func") + func main() { _ = T{ - //@complete("", fieldA, fieldB, fieldC, fieldE2) + // Fields are suggested in breadth-first order: + // T fields (E2, C), then E2 fields (E1, B), then E1 fields (A). + + //@complete("", fieldE2, fieldC, fieldE1, fieldB, fieldA, literalE2, funcMain, structE1, structE2, structT) + } + + _ = T{ + A: 1, + //@complete("", fieldC, fieldB) } }
diff --git a/gopls/internal/test/marker/testdata/completion/issue78553_before127.txt b/gopls/internal/test/marker/testdata/completion/issue78553_before127.txt new file mode 100644 index 0000000..0e4ec31 --- /dev/null +++ b/gopls/internal/test/marker/testdata/completion/issue78553_before127.txt
@@ -0,0 +1,28 @@ +Test for completion on promoted fields in struct literals before Go 1.27. +Ref: go.dev/issues/78553 + +-- go.mod -- +module mod.com + +go 1.26 + +-- main.go -- +package main + +type E1 struct { + A int //@item(fieldA, "A", "int", "field") +} + +type T struct { + E1 //@item(fieldE1, "E1", "E1", "field"),item(structE1, "E1", "struct{...}", "struct") + C int //@item(fieldC, "C", "int", "field"),item(structT, "T", "struct{...}", "struct") +} + +//@item(literalE1, "E1{}", "", "var") +//@item(funcMain, "main", "func()", "func") + +func main() { + _ = T{ + //@complete("", fieldE1, fieldC, literalE1, funcMain, structE1, structT) + } +}
diff --git a/gopls/internal/test/marker/testdata/completion/issue79325.txt b/gopls/internal/test/marker/testdata/completion/issue79325.txt new file mode 100644 index 0000000..34e0d66 --- /dev/null +++ b/gopls/internal/test/marker/testdata/completion/issue79325.txt
@@ -0,0 +1,15 @@ +This test checks that the blank identifier `_` is not suggested as a completion candidate for struct fields. + +-- flags -- +-ignore_extra_diags +-- issue79325.go -- +package p + +type foo struct { + bar uint8 + _ [2]byte +} + +func (f foo) _() bool { + return f. //@rank(re"() \\/", "bar", "!_") +}
diff --git a/gopls/internal/test/marker/testdata/completion/randv2.txt b/gopls/internal/test/marker/testdata/completion/randv2.txt index b3f2ced..1d22647 100644 --- a/gopls/internal/test/marker/testdata/completion/randv2.txt +++ b/gopls/internal/test/marker/testdata/completion/randv2.txt
@@ -2,11 +2,6 @@ -- flags -- -min_go_command=go1.22 --- settings.json -- -{ - "importsSource": "gopls" -} - -- go.mod -- module unimported.test
diff --git a/gopls/internal/test/marker/testdata/completion/unimported.txt b/gopls/internal/test/marker/testdata/completion/unimported.txt index a8ca720..be10595 100644 --- a/gopls/internal/test/marker/testdata/completion/unimported.txt +++ b/gopls/internal/test/marker/testdata/completion/unimported.txt
@@ -2,11 +2,6 @@ -- flags -- -ignore_extra_diags --- settings.json -- -{ - "importsSource": "gopls" -} - -- go.mod -- module unimported.test
diff --git a/gopls/internal/test/marker/testdata/definition/embed.txt b/gopls/internal/test/marker/testdata/definition/embed.txt index 7169ba5..17f0d11 100644 --- a/gopls/internal/test/marker/testdata/definition/embed.txt +++ b/gopls/internal/test/marker/testdata/definition/embed.txt
@@ -301,6 +301,15 @@ ```go field A a.A // size=16 (0x10), offset=40 (0x28) ``` + +--- + +@def("A", AString),hover("A", "A", aA) + + +--- + +[`(b.S1).A` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1.A) -- @aAlias -- ```go field aAlias aAlias // size=16 (0x10), offset=56 (0x38)
diff --git a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt index 1535e22..a5024f1 100644 --- a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt +++ b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt
@@ -139,3 +139,19 @@ x = append(x, e) // no "replace loop with append" diagnostic } } + +-- sqlrowserr/sqlrowserr.go -- +package sqlrowserr + +import "database/sql" + +func _(db *sql.DB) { + rows, err := db.Query("") //@ diag("db.Query", re`sql.Rows "rows" is used in Next loop at line .. without final check of rows.Err\(\)`) + if err != nil { + return + } + defer rows.Close() // ignore error + for rows.Next() { + rows.Scan() // ignore error + } +}
diff --git a/gopls/internal/test/marker/testdata/highlight/issue79335.txt b/gopls/internal/test/marker/testdata/highlight/issue79335.txt new file mode 100644 index 0000000..0a79409 --- /dev/null +++ b/gopls/internal/test/marker/testdata/highlight/issue79335.txt
@@ -0,0 +1,14 @@ +Regression test for go.dev/issue/79335: crash when highlighting a +continue'statement not inside a loop. + +-- go.mod -- +module example.com +go 1.18 + +-- p.go -- +package p + +func _() { + continue //@hiloc(cont, "continue", text), diag("continue", re"continue") + //@highlight(cont) +}
diff --git a/gopls/internal/test/marker/testdata/hover/aliaspointer.txt b/gopls/internal/test/marker/testdata/hover/aliaspointer.txt new file mode 100644 index 0000000..f85b733 --- /dev/null +++ b/gopls/internal/test/marker/testdata/hover/aliaspointer.txt
@@ -0,0 +1,85 @@ +This test verifies the behavior of hovering over a promoted field through an +alias to a pointer to a struct type. + +-- go.mod -- +module example.com + +-- main.go -- +package main + +type U struct { + X int +} + +type T struct { + U +} + +type A = *T + +type S struct { + A +} + +type A1 = U +type S1 struct { + A1 +} + +type A2 = U +type S2 struct { + *A2 +} + +type A3_1 = *T +type A3_2 = A3_1 +type S3 struct { + A3_2 +} + +func main() { + var s S + _ = s.X //@hover("X", "X", X) + + var s1 S1 + _ = s1.X //@hover("X", "X", X1) + + var s2 S2 + _ = s2.X //@hover("X", "X", X2) + + var s3 S3 + _ = s3.X //@hover("X", "X", X3) +} + +-- @X -- +```go +field X int // through A, U +``` + +--- + +[`(main.U).X` on pkg.go.dev](https://pkg.go.dev/example.com#U.X) +-- @X1 -- +```go +field X int // through A1 +``` + +--- + +[`(main.U).X` on pkg.go.dev](https://pkg.go.dev/example.com#U.X) +-- @X2 -- +```go +field X int // through *A2 +``` + +--- + +[`(main.U).X` on pkg.go.dev](https://pkg.go.dev/example.com#U.X) +-- @X3 -- +```go +field X int // through A3_2, U +``` + +--- + +[`(main.U).X` on pkg.go.dev](https://pkg.go.dev/example.com#U.X)
diff --git a/gopls/internal/test/marker/testdata/hover/genericmethods.txt b/gopls/internal/test/marker/testdata/hover/genericmethods.txt new file mode 100644 index 0000000..24fe3cb --- /dev/null +++ b/gopls/internal/test/marker/testdata/hover/genericmethods.txt
@@ -0,0 +1,80 @@ +This file tests for hovering over generic and instantiated methods and funcs. + +-- flags -- +-min_go=go1.27 + +-- settings.json -- +{"analyses": {"unusedfunc": false}} + +-- go.mod -- +module example.com + +go 1.27 + +-- a/a.go -- +package a + +type C[T any] struct{} + +// This is method C.Method. +func (C[T]) Method[U any]() {} //@hover("Method", "Method", Methodgen) + +var _ = C[string].Method[int] //@hover("Method", "Method", Methodint) + +// This is func Func. +func Func[T any]() {} //@hover("Func", "Func", Funcgen) + +var _ = Func[int] //@hover("Func", "Func", Funcint) + +-- @Methodgen -- +```go +func (C[T]) Method[U any]() +``` + +--- + +This is method C.Method. + + +--- + +[`(a.C).Method` on pkg.go.dev](https://pkg.go.dev/example.com/a#C.Method) +-- @Methodint -- +```go +func (C[string]).Method() // func[U any]() +``` + +--- + +This is method C.Method. + + +--- + +[`(a.C).Method` on pkg.go.dev](https://pkg.go.dev/example.com/a#C.Method) +-- @Funcgen -- +```go +func Func[T any]() +``` + +--- + +This is func Func. + + +--- + +[`a.Func` on pkg.go.dev](https://pkg.go.dev/example.com/a#Func) +-- @Funcint -- +```go +func Func() // func[T any]() +``` + +--- + +This is func Func. + + +--- + +[`a.Func` on pkg.go.dev](https://pkg.go.dev/example.com/a#Func)
diff --git a/gopls/internal/test/marker/testdata/hover/issue78553.txt b/gopls/internal/test/marker/testdata/hover/issue78553.txt index 073aa19..2a7520a 100644 --- a/gopls/internal/test/marker/testdata/hover/issue78553.txt +++ b/gopls/internal/test/marker/testdata/hover/issue78553.txt
@@ -2,7 +2,7 @@ // in struct composite literals (a Go 1.27 feature). -- flags -- --min_go_command=go1.27 +-min_go=go1.27 -- go.mod -- module mod.com
diff --git a/gopls/internal/test/marker/testdata/implementation/genericmethods-mixed.txt b/gopls/internal/test/marker/testdata/implementation/genericmethods-mixed.txt new file mode 100644 index 0000000..e1639d6 --- /dev/null +++ b/gopls/internal/test/marker/testdata/implementation/genericmethods-mixed.txt
@@ -0,0 +1,42 @@ +Regression test for a crash in the method-set index when a type has +both an ordinary method and a generic method (Go 1.27). + +Generic methods are skipped while building the method-set index (they +do not participate in interface satisfaction), but the surrounding type +is still indexed because its ordinary method gives it a non-zero mask. +A bug once pre-sized the method slice to the full method-set length and +filled it by index, so skipping the generic method left a nil hole; +iterating those methods during a search panicked with a nil-pointer +dereference. + +The query and its implementer are deliberately in different packages so +that the search goes through the serialized methodsets index (the path +that held the nil hole), not the go/types-based local path. + +See the fix in gopls/internal/cache/methodsets, follow-up to golang/go#77549. + +-- flags -- +-min_go=go1.27 + +-- go.mod -- +module example.com +go 1.27 + +-- a/a.go -- +package a + +// C has an ordinary method G, so it is recorded in the method-set +// index, and a generic method F, which is skipped during indexing and +// must not leave a nil hole. C implements b.I via G alone. +type C struct{} //@loc(C, "C"), implementation("C", I) + +func (C) G(int) {} + +func (C) F[T any](T) {} + +-- b/b.go -- +package b + +type I interface { //@loc(I, "I"), implementation("I", C) + G(int) +}
diff --git a/gopls/internal/test/marker/testdata/implementation/genericmethods.txt b/gopls/internal/test/marker/testdata/implementation/genericmethods.txt new file mode 100644 index 0000000..ef67b69 --- /dev/null +++ b/gopls/internal/test/marker/testdata/implementation/genericmethods.txt
@@ -0,0 +1,34 @@ +Test of 'implementation' query on generic methods, which don't +participate in interface satisfaction. + +Test same-package and cross-package cases, +since they use different logic. + +-- flags -- +-min_go=go1.27 + +-- go.mod -- +module example.com +go 1.27 + +-- a/a.go -- +package a + +type C struct{} + +func (C) F[T any](T) {} //@ implementation("F") + +type I interface { + F(int) +} + +func _[T C]() { + var _ T //@ implementation("T") +} + +-- b/a.go -- +package a + +type I interface { + F(int) +}
diff --git a/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt b/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt index a2021cc..d89a8ce 100644 --- a/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt +++ b/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt
@@ -15,6 +15,11 @@ } } +-- go.mod -- +module example.com + +go 1.24 + -- composite_literals.go -- package inlayHint //@inlayhints(complit) @@ -159,12 +164,12 @@ ) var ( - varInt = 3 - varFloat = 3.14 - varBool = true - varRune = '3' + '4' - varComplex = 2.7i - varString = "Hello, world!" + varInt< int> = 3 + varFloat< float64> = 3.14 + varBool< bool> = true + varRune< rune> = '3' + '4' + varComplex< complex128> = 2.7i + varString< string> = "Hello, world!" ) -- parameter_names.go -- @@ -274,6 +279,8 @@ -- type_params.go -- package inlayHint //@inlayhints(typeparams) +import "example.com/other" + func main() { ints := map[string]int64{ "first": 34, @@ -293,6 +300,9 @@ SumNumbers(ints) SumNumbers(floats) + + other.Bar(ints) + other.Bar(other.Foo{}) } type Number interface { @@ -318,6 +328,8 @@ -- @typeparams -- package inlayHint //@inlayhints(typeparams) +import "example.com/other" + func main() { ints< map[string]int64> := map[string]int64{ "first": 34, @@ -337,6 +349,9 @@ SumNumbers<[string, int64]>(<m: >ints) SumNumbers<[string, float64]>(<m: >floats) + + other.Bar<[map[string]int64]>(<x: >ints) + other.Bar<[other.Foo]>(<x: >other.Foo{}) } type Number interface { @@ -388,7 +403,7 @@ func assignTypes() { var x string - var y = "" + var y< string> = "" i< int>, j< int> := 0, len([]string{})-1 println(i, j) } @@ -407,3 +422,9 @@ foo< map[string]any> := map[string]any{"": ""} } +-- other/other.go -- +package other + +type Foo struct{} + +func Bar[T any](x T) {}
diff --git a/gopls/internal/test/marker/testdata/inlayhints/issue67142.txt b/gopls/internal/test/marker/testdata/inlayhints/issue67142.txt index 456da25..af08c93 100644 --- a/gopls/internal/test/marker/testdata/inlayhints/issue67142.txt +++ b/gopls/internal/test/marker/testdata/inlayhints/issue67142.txt
@@ -31,5 +31,5 @@ //@inlayhints(out) package p -var _ = rand.Float64() +var _< invalid type> = rand.Float64()
diff --git a/gopls/internal/test/marker/testdata/inlayhints/nested-struct-lit.txt b/gopls/internal/test/marker/testdata/inlayhints/nested-struct-lit.txt new file mode 100644 index 0000000..699b0e3 --- /dev/null +++ b/gopls/internal/test/marker/testdata/inlayhints/nested-struct-lit.txt
@@ -0,0 +1,51 @@ +Test that inlay hints reveal the sequence of names implied by a field +name F in a struct literal T{F: v}, which in go1.27 may denote an +embedded field; see go.dev/issue/78553. + +-- flags -- +-ignore_extra_diags +-min_go=go1.27 + +-- settings.json -- +{"hints": {"compositeLiteralFields": true}} + +-- structlit.go -- +package p //@ inlayhints(structlit) + +type A struct { *B } +type B struct { C } +type C struct { F int } + +var _ = A{B: &B{C: C{F: 0}}} +var _ = A{B: &B{C: C{0}}} +var _ = A{&B{C: C{0}}} +var _ = A{&B{C{0}}} +var _ = A{C: C{F: 0}} +var _ = A{C: C{0}} +var _ = A{C{F: 0}} +var _ = A{C{F: 0}} +var _ = A{C{0}} +var _ = A{F: 0} +var _ = A{0} +var _ = A{} + +-- @structlit -- +package p //@ inlayhints(structlit) + +type A struct { *B } +type B struct { C } +type C struct { F int } + +var _ = A{B: &B{C: C{F: 0}}} +var _ = A{B: &B{C: C{<F: >0}}} +var _ = A{<B: >&B{C: C{<F: >0}}} +var _ = A{<B: >&B{<C: >C{<F: >0}}} +var _ = A{<B.>C: C{F: 0}} +var _ = A{<B.>C: C{<F: >0}} +var _ = A{<B: >C{F: 0}} +var _ = A{<B: >C{F: 0}} +var _ = A{<B: >C{<F: >0}} +var _ = A{<B.C.>F: 0} +var _ = A{<B: >0} +var _ = A{} +
diff --git a/gopls/internal/test/marker/testdata/mcptools/workspace_diagnostics.txt b/gopls/internal/test/marker/testdata/mcptools/workspace_diagnostics.txt index ede3947..3bbd0a8 100644 --- a/gopls/internal/test/marker/testdata/mcptools/workspace_diagnostics.txt +++ b/gopls/internal/test/marker/testdata/mcptools/workspace_diagnostics.txt
@@ -12,6 +12,9 @@ //@mcptool("go_diagnostics", `{"files":["$WORKDIR/a/a.go"]}`, output=diagnostics) //@mcptool("go_diagnostics", `{"files":["$WORKDIR/b/b.go"]}`, output=diagnostics) //@mcptool("go_diagnostics", `{"files":["$WORKDIR/main.go"]}`, output=diagnostics) +//@mcptool("go_diagnostics", `{"files":["$WORKDIR/b/b2.go", "$WORKDIR/main.go"]}`, output=diagnostics) +//@mcptool("go_diagnostics", `{"files":["$WORKDIR/main.go", "$WORKDIR/a/a.go"]}`, output=diagnostics) +//@mcptool("go_diagnostics", `{"files":["$WORKDIR/main.go", "$WORKDIR/a/a.go", "$WORKDIR/c/c.go"]}`, output=diagnostics_c) -- main.go -- package main @@ -42,6 +45,14 @@ const B = 2 +-- c/c.go -- +package c + +func _() { + var x interface{} + _ = x +} + -- @diagnostics -- File `$WORKDIR/b/b2.go` has the following diagnostics: 1:0-1:0: [Error] expected 'package', found 'const' @@ -50,3 +61,28 @@ 7:5-7:9: [Error] func main must have no arguments and no return values 8:9-8:12: [Error] cannot use b.B (untyped int constant 1) as string value in argument to a.Print +-- @diagnostics_c -- +File `$WORKDIR/b/b2.go` has the following diagnostics: +1:0-1:0: [Error] expected 'package', found 'const' + +File `$WORKDIR/c/c.go` has the following diagnostics: +3:7-3:18: [Hint] interface{} can be replaced by any +Fix: +--- $WORKDIR/c/c.go ++++ $WORKDIR/c/c.go +@@ -1,7 +1,7 @@ + package c + + func _() { +- var x interface{} ++ var x any + _ = x + } + + + + +File `$WORKDIR/main.go` has the following diagnostics: +7:5-7:9: [Error] func main must have no arguments and no return values +8:9-8:12: [Error] cannot use b.B (untyped int constant 1) as string value in argument to a.Print +
diff --git a/gopls/internal/test/marker/testdata/quickfix/stubmethods/anoniface.txt b/gopls/internal/test/marker/testdata/quickfix/stubmethods/anoniface.txt new file mode 100644 index 0000000..416f7c9 --- /dev/null +++ b/gopls/internal/test/marker/testdata/quickfix/stubmethods/anoniface.txt
@@ -0,0 +1,20 @@ +Test stubbing a method of an anonymous interface. + +-- go.mod -- +module example.com + +go 1.18 + +-- a/a.go -- +package a + +var _ interface{ f(int) } = C(0) //@quickfix(re"C", re"missing method", out) + +type C int + +-- @out/a/a.go -- +@@ -7 +7,4 @@ ++// f implements an anonymous interface. ++func (c C) f(int) { ++ panic("unimplemented") ++}
diff --git a/gopls/internal/test/marker/testdata/quickfix/stubmethods/genericmethods.txt b/gopls/internal/test/marker/testdata/quickfix/stubmethods/genericmethods.txt new file mode 100644 index 0000000..fc50678 --- /dev/null +++ b/gopls/internal/test/marker/testdata/quickfix/stubmethods/genericmethods.txt
@@ -0,0 +1,19 @@ +Test of stubmethods on generic methods, which do not participate +in interface satisfaction. + +-- flags -- +-min_go=go1.27 + +-- go.mod -- +module example.com +go 1.27 + +-- a/a.go -- +package a + +type C int +func (C) F[T any]() {} + +type I interface { F() } + +var _ I = C(0) //@ quickfixerr(re"C.0.", re"does not implement", "method C.F already exists but has the wrong type: got func[T any](), want func()")
diff --git a/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue70666.txt b/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue70666.txt new file mode 100644 index 0000000..fba1cec --- /dev/null +++ b/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue70666.txt
@@ -0,0 +1,27 @@ +This test verifies that the 'stub methods' quickfix is not offered in +duplicate function declarations because type information for the +duplicate declaration is missing from info.Defs. This scenario +previously triggered bug.Reports before the cause was understood; see +go.dev/issue/70666. + +-- go.mod -- +module example.com + +go 1.22 + +-- a/a.go -- +package a + +type I interface { + M() +} + +type T struct{} + +func foo() I { //@ diag(re"foo", re"redeclared") + return T{} //@ diag(re"T{}", re"cannot use T|does not implement") +} + +func foo() I { //@ diag(re"foo", re"redeclared") + return T{} //@ codeaction(re"T{}", "quickfix", diag=re"cannot use T|does not implement", err=re"found 0 CodeActions") +}
diff --git a/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue79399.txt b/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue79399.txt new file mode 100644 index 0000000..d0dc86a --- /dev/null +++ b/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue79399.txt
@@ -0,0 +1,45 @@ +Regression test for golang/go#79399: panic in fromAssignStmt when an +assignment has fewer LHS variables than RHS expressions (LHS < RHS). + +When an assignment like "x := Iface(&T{}), Iface2(&R{})" has 1 LHS variable +but 2 RHS expressions, iterating over enclosing nodes for the second RHS +expression (Iface2(&R{})) yields ParentEdgeIndex()=1, which is out of bounds +for Lhs (len=1), causing a panic in assign.Lhs[idx]. + +-- issue79399.go -- +package p + +// Regression test for golang/go#79399. + +type Writer interface { + Write([]byte) (int, error) +} + +type Reader interface { + Read([]byte) (int, error) +} + +type T struct{} +type R struct{} + +// Normal fromAssignStmt case: variable declared as interface, assigned a +// concrete type that doesn't implement it. +func _() { + var x Writer + x = &T{} //@quickfix(re"&T{}", re"missing method", stub) + _ = x +} + +// Regression test for golang/go#79399: panic when LHS < RHS. +func _() { + x := Writer(&T{}), Reader(&R{}) //@quickfixerr(re"&T{}", re"missing method", re"found 0"),quickfixerr(re"&R{}", re"missing method", re"found 0") + _ = x +} +-- @stub/issue79399.go -- +@@ -14 +14,6 @@ ++ ++// Write implements [Writer]. ++func (t *T) Write([]byte) (int, error) { ++ panic("unimplemented") ++} ++
diff --git a/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue79845.txt b/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue79845.txt new file mode 100644 index 0000000..7f8e7aa --- /dev/null +++ b/gopls/internal/test/marker/testdata/quickfix/stubmethods/issue79845.txt
@@ -0,0 +1,42 @@ +This test verifies that stubmethods quickfix generates correct +signatures for instantiated generic interfaces. See golang/go#79845. + +-- go.mod -- +module mod.com + +go 1.18 + +-- main.go -- +package main + +type Group[E any] interface { + Identity() E + Combine(E, E) E + Inverse(E) E +} + +type IntegersModuloPrime struct { + mod int +} + +var _ Group[int] = IntegersModuloPrime{} //@quickfix(re"IntegersModuloPrime", re"missing method", stub) + +-- @stub/main.go -- +@@ -13 +13,14 @@ +-var _ Group[int] = IntegersModuloPrime{} //@quickfix(re"IntegersModuloPrime", re"missing method", stub) ++// Combine implements [Group]. ++func (i IntegersModuloPrime) Combine(int, int) int { ++ panic("unimplemented") ++} ++ ++// Identity implements [Group]. ++func (i IntegersModuloPrime) Identity() int { ++ panic("unimplemented") ++} ++ ++// Inverse implements [Group]. ++func (i IntegersModuloPrime) Inverse(int) int { ++ panic("unimplemented") ++} +@@ -15 +28 @@ ++var _ Group[int] = IntegersModuloPrime{} //@quickfix(re"IntegersModuloPrime", re"missing method", stub)
diff --git a/gopls/internal/test/marker/testdata/quickfix/stubmethods/localiface.txt b/gopls/internal/test/marker/testdata/quickfix/stubmethods/localiface.txt new file mode 100644 index 0000000..4d12cbb --- /dev/null +++ b/gopls/internal/test/marker/testdata/quickfix/stubmethods/localiface.txt
@@ -0,0 +1,30 @@ +Test stubbing a method of a local interface. +It works so long as the methods have no free type parameters. + +-- go.mod -- +module example.com + +go 1.18 + +-- a/a.go -- +package a + +func f[T any]() { + // nope: method C.f(T) would have a free type param, T. + type I interface{ f(T) } + var _ I = C(0) //@quickfixerr(re"C", re"missing method", re"found 0 CodeActions") + + // ok: method C.f(int) is fine. + type J interface{ f(int) } + var _ J = C(0) //@quickfix(re"C", re"missing method", out) +} + +type C int + +-- @out/a/a.go -- +@@ -15 +15,4 @@ ++// f implements [J]. ++func (c C) f(int) { ++ panic("unimplemented") ++} +
diff --git a/gopls/internal/test/marker/testdata/references/genericmethods.txt b/gopls/internal/test/marker/testdata/references/genericmethods.txt new file mode 100644 index 0000000..e12999d --- /dev/null +++ b/gopls/internal/test/marker/testdata/references/genericmethods.txt
@@ -0,0 +1,30 @@ +Test of 'references' query via interfaces in presence of generic +methods, which don't participate in interface satisfaction. + +Test same-package and cross-package cases, +since they use different logic. + +-- flags -- +-min_go=go1.27 + +-- go.mod -- +module example.com +go 1.27 + +-- a/a.go -- +package a + +type C struct{} + +func (C) F[T any](T) {} //@ loc(CF, "F"), refs("F", CF) + +type I interface { + F(int) //@ loc(IF, "F"), refs("F", IF) +} + +-- b/a.go -- +package a + +type I interface { + F(int) //@ loc(bIF, "F"), refs("F", bIF) +}
diff --git a/internal/tool/tool.go b/gopls/internal/tool/tool.go similarity index 98% rename from internal/tool/tool.go rename to gopls/internal/tool/tool.go index 6420c96..70a6291 100644 --- a/internal/tool/tool.go +++ b/gopls/internal/tool/tool.go
@@ -20,7 +20,6 @@ ) // This file is a harness for writing your main function. -// The original version of the file is in golang.org/x/tools/internal/tool. // // It adds a method to the Application type // Main(name, usage string, args []string)
diff --git a/gopls/internal/util/fingerprint/fingerprint_test.go b/gopls/internal/util/fingerprint/fingerprint_test.go index ed94b29..23db935 100644 --- a/gopls/internal/util/fingerprint/fingerprint_test.go +++ b/gopls/internal/util/fingerprint/fingerprint_test.go
@@ -11,6 +11,7 @@ "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/util/fingerprint" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/testfiles" "golang.org/x/tools/txtar" ) @@ -196,3 +197,78 @@ check(test.b, test.b, true) } } + +// Generic methods were added in go1.27 but cannot satisfy +// interfaces, so their fingerprints aren't really important, +// but we test them nonetheless. +// +// TODO(adonovan): merge into test above once go1.27 is assured. +func TestMatches_genericMethods(t *testing.T) { + testenv.NeedsGo1Point(t, 27) + + const src = ` +-- go.mod -- +module example.com +go 1.27 + +-- a/a.go -- +package a + +type I interface{ F(int) } + +type C struct{} +func (C) F[T any](T) {} +` + pkg := testfiles.LoadPackages(t, txtar.Parse([]byte(src)), "./a")[0] + scope := pkg.Types.Scope() + for _, test := range []struct { + a, b string + method string // optional field or method + want bool + }{ + // C.F does match (unify with) I.F. + // However, C does not satisfy I since + // only non-generic methods are relevant. + {"I", "C", "F", true}, + } { + lookup := func(name string) types.Type { + obj := scope.Lookup(name) + if obj == nil { + t.Fatalf("Lookup %s failed", name) + } + if test.method != "" { + obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, pkg.Types, test.method) + if obj == nil { + t.Fatalf("Lookup %s.%s failed", name, test.method) + } + } + return obj.Type() + } + + check := func(sa, sb string, want bool) { + t.Helper() + + a := lookup(sa) + b := lookup(sb) + + afp, _ := fingerprint.Encode(a) + bfp, _ := fingerprint.Encode(b) + + atree := fingerprint.Parse(afp) + btree := fingerprint.Parse(bfp) + + got := fingerprint.Matches(atree, btree) + if got != want { + t.Errorf("a=%s b=%s method=%s: unify returned %t for these inputs:\n- %s\n- %s", + sa, sb, test.method, got, a, b) + } + } + + check(test.a, test.b, test.want) + // Matches is symmetric + check(test.b, test.a, test.want) + // Matches is reflexive + check(test.a, test.a, true) + check(test.b, test.b, true) + } +}
diff --git a/gopls/internal/util/immutable/immutable.go b/gopls/internal/util/immutable/immutable.go index 02d7b27..93aa987 100644 --- a/gopls/internal/util/immutable/immutable.go +++ b/gopls/internal/util/immutable/immutable.go
@@ -42,3 +42,8 @@ func (m Map[K, V]) All() iter.Seq2[K, V] { return maps.All(m.m) } + +// Keys returns the sequence of keys in the map. +func (m Map[K, V]) Keys() iter.Seq[K] { + return maps.Keys(m.m) +}
diff --git a/gopls/internal/util/memoize/memoize.go b/gopls/internal/util/memoize/memoize.go index e49942a..bacb1ce 100644 --- a/gopls/internal/util/memoize/memoize.go +++ b/gopls/internal/util/memoize/memoize.go
@@ -25,8 +25,6 @@ "runtime/trace" "sync" "sync/atomic" - - "golang.org/x/tools/internal/xcontext" ) // Function is the type of a function that can be memoized. @@ -164,7 +162,7 @@ // run starts p.function and returns the result. p.mu must be locked. func (p *Promise) run(ctx context.Context, arg any) (any, error) { - childCtx, cancel := context.WithCancel(xcontext.Detach(ctx)) + childCtx, cancel := context.WithCancel(context.WithoutCancel(ctx)) p.cancel = cancel p.state = stateRunning p.done = make(chan struct{})
diff --git a/gopls/internal/util/typesutil/typesutil.go b/gopls/internal/util/typesutil/typesutil.go index 6adf759..bd3374f 100644 --- a/gopls/internal/util/typesutil/typesutil.go +++ b/gopls/internal/util/typesutil/typesutil.go
@@ -13,7 +13,6 @@ "golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/gopls/internal/util/bug" ) // FormatTypeParams turns TypeParamList into its Go representation, such as: @@ -187,23 +186,35 @@ // EnclosingSignature returns the signature of the innermost // function enclosing the syntax node denoted by cur. // It returns nil if the node is not within a function, -// or the function's type information is missing. +// or the function's type information is missing +// (for example because there are duplicate func declarations). func EnclosingSignature(cur inspector.Cursor, info *types.Info) *types.Signature { -loop: for c := range cur.Enclosing((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { switch n := c.Node().(type) { case *ast.FuncDecl: if f, ok := info.Defs[n.Name]; ok { return f.Type().(*types.Signature) } - bug.Reportf("FuncDecl defines no types.Func (#70666)") - break loop + // FuncDecl defines no types.Func (#70666). + // Example: + // func f(); func f() + // The same name is defined twice, but only + // one results in a symbol being inserted into + // the package scope. + // + // (It is tempting to change the type checker + // to populate Defs with a second Func f that is + // not in the Package scope, but this would only + // lead to different inconsistencies.) + return nil + case *ast.FuncLit: if f, ok := info.Types[n]; ok { return f.Type.(*types.Signature) } - bug.Reportf("FuncLit has no type (#70666)") - break loop + // Presumably this is also reachable in case + // of missing type information. + return nil } } return nil
diff --git a/gopls/main.go b/gopls/main.go index 7aef8c1..0a79d27 100644 --- a/gopls/main.go +++ b/gopls/main.go
@@ -19,8 +19,8 @@ "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" - "golang.org/x/tools/internal/tool" ) var version = "" // if set by the linker, overrides the gopls version
diff --git a/internal/astutil/comment.go b/internal/astutil/comment.go index 40a3472..db9e68b 100644 --- a/internal/astutil/comment.go +++ b/internal/astutil/comment.go
@@ -13,11 +13,17 @@ ) // Deprecation returns the paragraph of the doc comment that starts with the -// conventional "Deprecation: " marker, as defined by -// https://go.dev/wiki/Deprecated, or "" if the documented symbol is not -// deprecated. +// conventional "Deprecation: " marker, or the end of a single-line comment +// with the deprecation marker, as defined by https://go.dev/wiki/Deprecated. +// Returns "" if the documented symbol is not deprecated. +// +// Deprecation(nil) returns the empty string. func Deprecation(doc *ast.CommentGroup) string { - for p := range strings.SplitSeq(doc.Text(), "\n\n") { + // doc.Text() is newline-terminated. For legacy reasons, this function will + // return as newline-terminated if is the last segment of the CommentGroup + // but not if it is a paragraph in the middle of the CommentGroup. + docText := doc.Text() + for p := range strings.SplitSeq(docText, "\n\n") { // There is still some ambiguity for deprecation message. This function // only returns the paragraph introduced by "Deprecated: ". More // information related to the deprecation may follow in additional @@ -27,6 +33,19 @@ return p } } + + // We also want to support deprecation markers in line comments. Not all + // call sites know whether they have a line comment or the type of AST node + // the comment is associated with; so to best match line deprecations, + // the CommentGroup must meet these criteria: + // * The doc.Text() is a single line. + // * The comment uses the "// ..." format. + if doc == nil || len(doc.List) != 1 || !strings.HasPrefix(doc.List[0].Text, "//") { + return "" + } + if i := strings.Index(docText, "Deprecated: "); i != -1 { + return docText[i:] + } return "" }
diff --git a/internal/astutil/comment_test.go b/internal/astutil/comment_test.go index bb4c432..3f73877 100644 --- a/internal/astutil/comment_test.go +++ b/internal/astutil/comment_test.go
@@ -5,6 +5,7 @@ package astutil_test import ( + "fmt" "go/ast" "go/parser" "go/token" @@ -57,3 +58,137 @@ }) } } + +func TestDeprecation(t *testing.T) { + testsCases := []struct { + name string + in string + want string + }{ + { + name: "doc_comment_only_paragraph", + in: `// Deprecated: Test + // a whole paragraph. + type A struct {}`, + want: "Deprecated: Test\na whole paragraph.\n", + }, + { + name: "doc_comment_any_paragraph", + in: `// First paragraph + // + // Deprecated: Middle + // paragraph. + // + // Last Paragraph + type A struct {}`, + want: "Deprecated: Middle\nparagraph.", + }, + { + name: "doc_comment_finds_the_first", + in: `// Deprecated: First + // + // Deprecated: second + type A struct {}`, + want: "Deprecated: First", + }, + { + name: "doc_comment_not_found_inside_paragraph", + in: `// First paragraph + // Deprecated: Middle paragraph. + type A struct {}`, + want: "", + }, + { + name: "multi_line_doc_comment_supported_if_no_whitespace", + in: ` +/*First paragraph + +Deprecated: Middle paragraph + +Last Paragraph +*/ +type A struct {}`, + want: "Deprecated: Middle paragraph", + }, + { + name: "multi_line_doc_comment_weird_format_not_supported", + // This is what the go formatter formats when the text starts on + // the second line of a /* */ comment. + in: ` + /* + First paragraph + + Deprecated: Middle paragraph + + Last Paragraph + */ + type A struct {}`, + // Not found, as the "Deprecated: ..." line has leading whitespace. + want: "", + }, + { + name: "line_comment_just_deprecated_tag", + in: `type A interface { + B(int) int // Deprecated: use 'C()' + }`, + want: "Deprecated: use 'C()'\n", + }, + { + name: "line_comment_comment_before_deprecated_tag", + in: `type A struct { + b int // This does x. Deprecated: use 'c'. Will cleanup. + }`, + want: "Deprecated: use 'c'. Will cleanup.\n", + }, + { + name: "line_comment_finds_first_deprecated_tag", + in: `type A struct { + int // Deprecated: use 'c'. Deprecated: use 'd'. + }`, + want: "Deprecated: use 'c'. Deprecated: use 'd'.\n", + }, + { + name: "line_comment_doesnt_support_multi_line_comment_type", + in: `type A struct { + b int /*Deprecated: use 'c'*/ + }`, + // We can't prevent this, as ast.CommentGroup doesn't specify + // where the comment happened. We won't advertise this. + want: "Deprecated: use 'c'\n", + }, + { + name: "multiline_comment_cant_have_comment_before_deprecated_tag", + in: `type A struct { + b int /* test Deprecated: use 'c' */ + }`, + want: "", + }, + } + + for _, test := range testsCases { + t.Run(test.name, func(t *testing.T) { + src := fmt.Sprintf("package a; \n\n%s", test.in) + f, err := parser.ParseFile(token.NewFileSet(), "a.go", src, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + switch len(f.Comments) { + case 0: + t.Error("No `ast.CommentGroup` found") + case 1: + default: + t.Errorf("%d `ast.CommentGroup`s found, only want one", len(f.Comments)) + } + if got := astutil.Deprecation(f.Comments[0]); got != test.want { + // align 'got' and 'want' for easier inspection + t.Errorf("\nfound comment: %q\ngot: %q\nwant: %q", f.Comments[0].Text(), got, test.want) + } + }) + } + + t.Run("Deprecation(nil)", func(t *testing.T) { + if got := astutil.Deprecation(nil); got != "" { + t.Errorf("Deprecation(nil) = %q, want: \"\"", got) + } + }) +}
diff --git a/internal/diff/unified.go b/internal/diff/unified.go index df8f2fc..6e8adad 100644 --- a/internal/diff/unified.go +++ b/internal/diff/unified.go
@@ -300,7 +300,7 @@ case ' ': return "", fmt.Errorf("unexpected line %q", l) default: - return "", fmt.Errorf("impossible unified %q", udiffs) + return "", fmt.Errorf("invalid unified diff: <<%s>>", udiffs) } } // copy any remaining lines
diff --git a/internal/gcimporter/bexport_test.go b/internal/gcimporter/bexport_test.go index 7967240..a87917c 100644 --- a/internal/gcimporter/bexport_test.go +++ b/internal/gcimporter/bexport_test.go
@@ -244,7 +244,7 @@ // Calling equalType here leads to infinite recursion, so just compare // strings. if xm.String() != ym.String() { - return fmt.Errorf("unequal methods: %s vs %s", x, y) + return fmt.Errorf("unequal methods: %s vs %s", xm, ym) } } return nil
diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go index 4c9450f..686f171 100644 --- a/internal/gcimporter/iexport.go +++ b/internal/gcimporter/iexport.go
@@ -823,6 +823,9 @@ w.pos(m.Pos()) w.string(m.Name()) sig, _ := m.Type().(*types.Signature) + if w.p.version >= iexportVersionGenericMethods && w.bool(sig.TypeParams().Len() > 0) { + w.tparamList(obj.Name()+"."+m.Name(), sig.TypeParams(), obj.Pkg()) + } // Receiver type parameters are type arguments of the receiver type, so // their name must be qualified before exporting recv.
diff --git a/internal/gcimporter/iexport_common_test.go b/internal/gcimporter/iexport_common_test.go index 00dc2ff..8a22afd 100644 --- a/internal/gcimporter/iexport_common_test.go +++ b/internal/gcimporter/iexport_common_test.go
@@ -9,4 +9,4 @@ var IExportCommon = iexportCommon -const IExportVersion = iexportVersionGenerics +const IExportVersion = iexportVersionGenericMethods
diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go index 1ee4e93..7b1723e 100644 --- a/internal/gcimporter/iimport.go +++ b/internal/gcimporter/iimport.go
@@ -48,13 +48,14 @@ // Keep this in sync with constants in iexport.go. const ( - iexportVersionGo1_11 = 0 - iexportVersionPosCol = 1 - iexportVersionGo1_18 = 2 - iexportVersionGenerics = 2 - iexportVersion = iexportVersionGenerics + iexportVersionGo1_11 = 0 + iexportVersionPosCol = 1 + iexportVersionGo1_18 = 2 + iexportVersionGenerics = 2 + iexportVersionGenericMethods = 3 + iexportVersion = iexportVersionGenericMethods - iexportVersionCurrent = 2 + iexportVersionCurrent = 3 ) type ident struct { @@ -179,9 +180,9 @@ version = int64(r.uint64()) switch version { - case iexportVersionGo1_18, iexportVersionPosCol, iexportVersionGo1_11: + case iexportVersionGenericMethods, iexportVersionGo1_18, iexportVersionPosCol, iexportVersionGo1_11: default: - if version > iexportVersionGo1_18 { + if version > iexportVersionGenericMethods { errorf("unstable iexport format version %d, just rebuild compiler and std library", version) } else { errorf("unknown iexport format version %d", version) @@ -614,6 +615,10 @@ for n := r.uint64(); n > 0; n-- { mpos := r.pos() mname := r.ident() + var tpars []*types.TypeParam + if r.p.version >= iexportVersionGenericMethods && r.bool() { + tpars = r.tparamList() + } recv := r.param(pkg) // If the receiver has any targs, set those as the @@ -628,8 +633,7 @@ rparams[i] = types.Unalias(targs.At(i)).(*types.TypeParam) } } - msig := r.signature(pkg, recv, rparams, nil) - + msig := r.signature(pkg, recv, rparams, tpars) named.AddMethod(types.NewFunc(mpos, pkg, mname, msig)) } }
diff --git a/internal/goplsexport/export.go b/internal/goplsexport/export.go deleted file mode 100644 index 414c9cb..0000000 --- a/internal/goplsexport/export.go +++ /dev/null
@@ -1,20 +0,0 @@ -// 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 goplsexport provides various backdoors to not-yet-published -// parts of x/tools that are needed by gopls. -package goplsexport - -import "golang.org/x/tools/go/analysis" - -var ( - ErrorsAsTypeModernizer *analysis.Analyzer // = modernize.errorsastypeAnalyzer - SlicesBackwardModernizer *analysis.Analyzer // = modernize.slicesbackwardAnalyzer - StdIteratorsModernizer *analysis.Analyzer // = modernize.stditeratorsAnalyzer - PlusBuildModernizer *analysis.Analyzer // = modernize.plusbuildAnalyzer - StringsCutModernizer *analysis.Analyzer // = modernize.stringscutAnalyzer - UnsafeFuncsModernizer *analysis.Analyzer // = modernize.unsafeFuncsAnalyzer - AtomicTypesModernizer *analysis.Analyzer // = modernize.atomicTypesAnalyzer - EmbedLitModernizer *analysis.Analyzer // = modernize.embedLitAnalyzer -)
diff --git a/internal/imports/fix.go b/internal/imports/fix.go index d29ecfb..b99ea6c 100644 --- a/internal/imports/fix.go +++ b/internal/imports/fix.go
@@ -273,7 +273,6 @@ } unknown = append(unknown, imp.ImportPath) } - names, err := p.source.LoadPackageNames(ctx, p.srcDir, unknown) if err != nil { return err
diff --git a/internal/imports/imports.go b/internal/imports/imports.go index b5f5218..0722938 100644 --- a/internal/imports/imports.go +++ b/internal/imports/imports.go
@@ -74,6 +74,10 @@ // Note that filename's directory influences which imports can be chosen, // so it is important that filename be accurate. func FixImports(ctx context.Context, filename string, src []byte, goroot string, logf func(string, ...any), source Source) (fixes []*ImportFix, err error) { + if source == nil { + // In case someone adds a defective call from a new place + panic("source is nil") + } ctx, done := event.Start(ctx, "imports.FixImports") defer done()
diff --git a/internal/jsonrpc2_v2/conn.go b/internal/jsonrpc2_v2/conn.go index 408bb14..7c26fb5 100644 --- a/internal/jsonrpc2_v2/conn.go +++ b/internal/jsonrpc2_v2/conn.go
@@ -12,7 +12,6 @@ "io" "sync" "sync/atomic" - "time" ) // Binder builds a connection configuration. @@ -209,7 +208,7 @@ // NewConnection creates a new [Connection] object and starts processing // incoming messages. func NewConnection(ctx context.Context, cfg ConnectionConfig) *Connection { - ctx = notDone{ctx} + ctx = context.WithoutCancel(ctx) c := &Connection{ state: inFlightState{closer: cfg.Closer}, @@ -234,7 +233,7 @@ func bindConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binder, onDone func()) *Connection { // TODO: Should we create a new event span here? // This will propagate cancellation from ctx; should it? - ctx := notDone{bindCtx} + ctx := context.WithoutCancel(bindCtx) c := &Connection{ state: inFlightState{closer: rwc}, @@ -695,7 +694,7 @@ delete(s.incomingByID, req.ID) }) if respErr == nil { - writeErr := c.write(notDone{req.ctx}, response) + writeErr := c.write(context.WithoutCancel(req.ctx), response) if err == nil { err = writeErr } @@ -766,14 +765,3 @@ return fmt.Errorf("%w: %v", ErrInternal, err) } - -// notDone is a context.Context wrapper that returns a nil Done channel. -type notDone struct{ ctx context.Context } - -func (ic notDone) Value(key any) any { - return ic.ctx.Value(key) -} - -func (notDone) Done() <-chan struct{} { return nil } -func (notDone) Err() error { return nil } -func (notDone) Deadline() (time.Time, bool) { return time.Time{}, false }
diff --git a/internal/modindex/index.go b/internal/modindex/index.go index c7ef97d..2938104 100644 --- a/internal/modindex/index.go +++ b/internal/modindex/index.go
@@ -188,6 +188,9 @@ ) for scan.Scan() { v := scan.Text() + if len(v) < 2 { + return nil, fmt.Errorf("malformed line: %q, %d entries", v, len(entries)) + } if v[0] == ':' { if curEntry != nil { entries = append(entries, *curEntry)
diff --git a/internal/packagestest/export.go b/internal/packagestest/export.go index 8fb68d4..a211f7e 100644 --- a/internal/packagestest/export.go +++ b/internal/packagestest/export.go
@@ -187,7 +187,7 @@ // debugging tests. // // If the Writer for any file within any module returns an error equivalent to -// ErrUnspported, Export skips the test. +// ErrUnsupported, Export skips the test. func Export(t testing.TB, exporter Exporter, modules []Module) *Exported { t.Helper() if exporter == Modules {
diff --git a/internal/refactor/delete.go b/internal/refactor/delete.go index 25547f0..b655b8a 100644 --- a/internal/refactor/delete.go +++ b/internal/refactor/delete.go
@@ -439,7 +439,7 @@ inStmtList = true case *ast.ForStmt: use(parent.For, parent.Body.Lbrace) - // special handling, as init;cond;post BlockStmt is not a statment list + // special handling, as init;cond;post BlockStmt is not a statement list if parent.Init != nil && parent.Cond != nil && stmt == parent.Init && lineOf(parent.Cond.Pos()) == lineOf(stmt.End()) { rightStmt = parent.Cond.Pos() } else if parent.Post != nil && parent.Cond != nil && stmt == parent.Post && lineOf(parent.Cond.End()) == lineOf(stmt.Pos()) {
diff --git a/internal/refactor/imports.go b/internal/refactor/imports.go index e1860ab..5ce70ae 100644 --- a/internal/refactor/imports.go +++ b/internal/refactor/imports.go
@@ -99,19 +99,28 @@ } // Create a new import declaration either before the first existing - // declaration (which must exist), including its comments; or - // inside the declaration, if it is an import group. - decl0 := file.Decls[0] - before := decl0.Pos() - switch decl0 := decl0.(type) { - case *ast.GenDecl: - if decl0.Doc != nil { - before = decl0.Doc.Pos() + // declaration (if it exists), including its comments; or at the end of the + // file (if there are no decls); or inside the declaration, if it is an + // import group. + var ( + before token.Pos + decl0 ast.Decl + ) + if len(file.Decls) > 0 { + decl0 = file.Decls[0] + before = decl0.Pos() + switch decl0 := decl0.(type) { + case *ast.GenDecl: + if decl0.Doc != nil { + before = decl0.Doc.Pos() + } + case *ast.FuncDecl: + if decl0.Doc != nil { + before = decl0.Doc.Pos() + } } - case *ast.FuncDecl: - if decl0.Doc != nil { - before = decl0.Doc.Pos() - } + } else { + before = file.FileEnd } var pos token.Pos if gd, ok := decl0.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() {
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index cf4c587..b329ab6 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go
@@ -768,12 +768,18 @@ } } - typeArgs := st.typeArguments(caller.Call) - if len(typeArgs) != len(callee.TypeParams) { - return nil, fmt.Errorf("cannot inline: type parameter inference is not yet supported") - } - if err := substituteTypeParams(logf, callee.TypeParams, typeArgs, params, replaceCalleeID); err != nil { - return nil, err + // Substitute type parameters in calleeDecl AST with type arguments from the + // call, and synchronize the parameter metadata. + { + typeArgs := st.typeArguments(caller.Call) + if len(typeArgs) != len(callee.TypeParams) { + return nil, fmt.Errorf("cannot inline: type parameter inference is not yet supported") + } + if err := substituteTypeParams(logf, callee.TypeParams, typeArgs, replaceCalleeID); err != nil { + return nil, err + } + // Synchronize the parameters' type pointers with the mutated calleeDecl. + syncParamFieldTypes(calleeDecl, params) } // Log effective arguments. @@ -1506,9 +1512,9 @@ // variadic elimination, and may be unpacked into variadic calls. type replacer = func(offset int, repl ast.Expr, unpackVariadic bool) -// substituteTypeParams replaces type parameters in the callee with the corresponding type arguments -// from the call. -func substituteTypeParams(logf logger, typeParams []*paramInfo, typeArgs []*argument, params []*parameter, replace replacer) error { +// substituteTypeParams replaces type parameters in the callee with the +// corresponding type arguments from the call. +func substituteTypeParams(logf logger, typeParams []*paramInfo, typeArgs []*argument, replace replacer) error { assert(len(typeParams) == len(typeArgs), "mismatched number of type params/args") for i, paramInfo := range typeParams { arg := typeArgs[i] @@ -1522,31 +1528,37 @@ for _, ref := range paramInfo.Refs { replace(ref.Offset, internalastutil.CloneNode(arg.expr), false) } - // Also replace parameter field types. - // TODO(jba): find a way to do this that is not so slow and clumsy. - // Ideally, we'd walk each p.fieldType once, replacing all type params together. - for _, p := range params { - if id, ok := p.fieldType.(*ast.Ident); ok && id.Name == paramInfo.Name { - p.fieldType = arg.expr - } else { - for _, id := range identsNamed(p.fieldType, paramInfo.Name) { - replaceNode(p.fieldType, id, arg.expr) - } - } - } } return nil } -func identsNamed(n ast.Node, name string) []*ast.Ident { - var ids []*ast.Ident - ast.Inspect(n, func(n ast.Node) bool { - if id, ok := n.(*ast.Ident); ok && id.Name == name { - ids = append(ids, id) +// syncParamFieldTypes synchronizes the fieldType of each parameter in params +// with the mutated calleeDecl AST. This is necessary because substituteTypeParams +// mutates the calleeDecl AST, replacing type nodes, but params still references +// the original (now outdated) type nodes. +func syncParamFieldTypes(calleeDecl *ast.FuncDecl, params []*parameter) { + var i int + setFieldType := func(t ast.Expr) { + assert(i < len(params), "mismatched parameter count") + params[i].fieldType = t + i++ + } + + if calleeDecl.Recv != nil && len(calleeDecl.Recv.List) > 0 { + setFieldType(calleeDecl.Recv.List[0].Type) + } + if calleeDecl.Type.Params != nil { + for _, field := range calleeDecl.Type.Params.List { + if field.Names == nil { + setFieldType(field.Type) + } else { + for range field.Names { + setFieldType(field.Type) + } + } } - return true - }) - return ids + } + assert(i == len(params), "mismatched parameter count") } // substitute implements parameter elimination by substitution.
diff --git a/internal/stdlib/deps.go b/internal/stdlib/deps.go index dacfc1d..0f73c71 100644 --- a/internal/stdlib/deps.go +++ b/internal/stdlib/deps.go
@@ -12,366 +12,386 @@ } var deps = [...]pkginfo{ - {"archive/tar", "\x03q\x03F=\x01\n\x01$\x01\x01\x02\x05\b\x02\x01\x02\x02\r"}, - {"archive/zip", "\x02\x04g\a\x03\x13\x021=\x01+\x05\x01\x0f\x03\x02\x0f\x04"}, - {"bufio", "\x03q\x86\x01D\x15"}, - {"bytes", "t+[\x03\fH\x02\x02"}, + {"archive/tar", "\x03{\x03F>\x01\n\x01&\x01\x01\x02\x05\b\x02\x01\x02\x02\r"}, + {"archive/zip", "\x02\x04j\x0e\x03\x12\x022>\x01-\x05\x01\x0f\x03\x02\x0f\x04"}, + {"bufio", "\x03{\x87\x01F\x15"}, + {"bytes", "~*]\x03\fJ\x02\x02"}, {"cmp", ""}, - {"compress/bzip2", "\x02\x02\xf6\x01A"}, - {"compress/flate", "\x02r\x03\x83\x01\f\x033\x01\x03"}, - {"compress/gzip", "\x02\x04g\a\x03\x15nU"}, - {"compress/lzw", "\x02r\x03\x83\x01"}, - {"compress/zlib", "\x02\x04g\a\x03\x13\x01o"}, - {"container/heap", "\xbc\x02"}, + {"compress/bzip2", "\x02\x02\x81\x02C"}, + {"compress/flate", "\x02|\x03\x84\x01\f\x034\x02\x03"}, + {"compress/gzip", "\x02\x04j\x0e\x03\x14pW"}, + {"compress/lzw", "\x02|\x03\x84\x01"}, + {"compress/zlib", "\x02\x04j\x0e\x03\x12\x01q"}, + {"container/heap", "\xc9\x02"}, {"container/list", ""}, {"container/ring", ""}, - {"context", "t\\p\x01\x0e"}, - {"crypto", "\x8a\x01pC"}, - {"crypto/aes", "\x10\v\t\x99\x02"}, - {"crypto/cipher", "\x03!\x01\x01 \x12\x1c,Z"}, - {"crypto/des", "\x10\x16 .,\x9d\x01\x03"}, - {"crypto/dsa", "F\x03+\x86\x01\r"}, - {"crypto/ecdh", "\x03\v\r\x10\x04\x17\x03\x0f\x1c\x86\x01"}, - {"crypto/ecdsa", "\x0e\x05\x03\x05\x01\x10\b\v\x06\x01\x03\x0e\x01\x1c\x86\x01\r\x05L\x01"}, - {"crypto/ed25519", "\x0e\x1f\x12\a\x03\b\a\x1cI=C"}, - {"crypto/elliptic", "4@\x86\x01\r9"}, - {"crypto/fips140", "#\x05\x95\x01\x98\x01"}, - {"crypto/hkdf", "0\x15\x01.\x16"}, - {"crypto/hmac", "\x1b\x16\x14\x01\x122"}, - {"crypto/hpke", "\x03\v\x02\x03\x04\x01\f\x01\x05\x1f\x05\a\x01\x01\x1d\x03\x13\x16\x9b\x01\x1c"}, - {"crypto/internal/boring", "\x0e\x02\x0el"}, - {"crypto/internal/boring/bbig", "\x1b\xec\x01N"}, - {"crypto/internal/boring/bcache", "\xc1\x02\x14"}, + {"context", "~]r\x01\x0e"}, + {"crypto", "\x93\x01rE"}, + {"crypto/aes", "\x10\v\n\xa5\x02"}, + {"crypto/cipher", "\x03\"\x01\x01 \x13$+\\"}, + {"crypto/des", "\x10\x17 7+\xa1\x01\x03"}, + {"crypto/dsa", "G\x034\x87\x01\r"}, + {"crypto/ecdh", "\x03\v\r\x11\x04\x17\x03\x10$\x87\x01"}, + {"crypto/ecdsa", "\x0e\x05\x03\x05\x01\x11\b\v\x06\x01\x03\x0f\x01$\x87\x01\r\x05O\x01"}, + {"crypto/ed25519", "\x0e \x12\a\x03\t\a$I>E"}, + {"crypto/elliptic", "5I\x87\x01\r;"}, + {"crypto/fips140", "$\x05\x9e\x01\x9b\x01"}, + {"crypto/hkdf", "1\x15\x017\x15"}, + {"crypto/hmac", "\x1b\x17\x14\x01\x139"}, + {"crypto/hpke", "\x03\v\x02\x03\x04\x01\r\x01\x05\x1f\x06\a\x01\x01%\x03\x12\x16\x9f\x01\x1d"}, + {"crypto/internal/boring", "\x0e\x02\x0eu"}, + {"crypto/internal/boring/bbig", "\x1b\xf7\x01P"}, + {"crypto/internal/boring/bcache", "\xce\x02\x14"}, {"crypto/internal/boring/sig", ""}, {"crypto/internal/constanttime", ""}, - {"crypto/internal/cryptotest", "\x03\r\v\b%\x10\x19\x06\x13\x12 \x04\x06\t\x19\x01\x11\x11\x1b\x01\a\x05\b\x03\x05\f"}, - {"crypto/internal/entropy", "K"}, - {"crypto/internal/entropy/v1.0.0", "D0\x95\x018\x14"}, - {"crypto/internal/fips140", "C1\xbf\x01\v\x17"}, - {"crypto/internal/fips140/aes", "\x03 \x03\x02\x14\x05\x01\x01\x05,\x95\x014"}, - {"crypto/internal/fips140/aes/gcm", "#\x01\x02\x02\x02\x12\x05\x01\x06,\x92\x01"}, - {"crypto/internal/fips140/alias", "\xd5\x02"}, - {"crypto/internal/fips140/bigmod", "(\x19\x01\x06,\x95\x01"}, - {"crypto/internal/fips140/check", "#\x0e\a\t\x02\xb7\x01["}, - {"crypto/internal/fips140/check/checktest", "(\x8b\x02\""}, - {"crypto/internal/fips140/drbg", "\x03\x1f\x01\x01\x04\x14\x05\n)\x86\x01\x0f7\x01"}, - {"crypto/internal/fips140/ecdh", "\x03 \x05\x02\n\r3\x86\x01\x0f7"}, - {"crypto/internal/fips140/ecdsa", "\x03 \x04\x01\x02\a\x03\x06:\x16pF"}, - {"crypto/internal/fips140/ed25519", "\x03 \x05\x02\x04\f:\xc9\x01\x03"}, - {"crypto/internal/fips140/edwards25519", "\x1f\t\a\x123\x95\x017"}, - {"crypto/internal/fips140/edwards25519/field", "(\x14\x053\x95\x01"}, - {"crypto/internal/fips140/hkdf", "\x03 \x05\t\a<\x16"}, - {"crypto/internal/fips140/hmac", "\x03 \x15\x01\x01:\x16"}, - {"crypto/internal/fips140/mldsa", "\x03\x1c\x04\x05\x02\x0e\x01\x03\x053\x95\x017"}, - {"crypto/internal/fips140/mlkem", "\x03 \x05\x02\x0f\x03\x053\xcc\x01"}, - {"crypto/internal/fips140/nistec", "\x1f\t\r\f3\x95\x01*\r\x15"}, - {"crypto/internal/fips140/nistec/fiat", "(\x148\x95\x01"}, - {"crypto/internal/fips140/pbkdf2", "\x03 \x05\t\a<\x16"}, - {"crypto/internal/fips140/rsa", "\x03\x1c\x04\x04\x01\x02\x0e\x01\x01\x028\x16pF"}, - {"crypto/internal/fips140/sha256", "\x03 \x1e\x01\x06,\x16\x7f"}, - {"crypto/internal/fips140/sha3", "\x03 \x19\x05\x012\x95\x01L"}, - {"crypto/internal/fips140/sha512", "\x03 \x1e\x01\x06,\x16\x7f"}, - {"crypto/internal/fips140/ssh", "(b"}, - {"crypto/internal/fips140/subtle", "\x1f\a\x1b\xc8\x01"}, - {"crypto/internal/fips140/tls12", "\x03 \x05\t\a\x02:\x16"}, - {"crypto/internal/fips140/tls13", "\x03 \x05\b\b\t3\x16"}, - {"crypto/internal/fips140cache", "\xb3\x02\r'"}, + {"crypto/internal/cryptotest", "\x03\r\v\t%\x11\x1a\r\x12\x12!\x04\x06\n\x19\x01\x11\x11\x1d\x01\a\x03\x02\b\x02\x01\x05\f"}, + {"crypto/internal/cryptotest/wycheproof", "\x0e\x12S\x01\f\x01r@\x05\x03\x15\x10"}, + {"crypto/internal/entropy", "L"}, + {"crypto/internal/entropy/v1.0.0", "E9\x96\x01:\x14"}, + {"crypto/internal/fips140", "D:\xc2\x01\v\x17"}, + {"crypto/internal/fips140/aes", "\x03!\x03\x02\x14\x05\x01\x01\x055\x96\x016"}, + {"crypto/internal/fips140/aes/gcm", "$\x01\x02\x02\x02\x12\x05\x01\x065\x93\x01"}, + {"crypto/internal/fips140/alias", "\xe2\x02"}, + {"crypto/internal/fips140/bigmod", ")\x19\x01\x065\x96\x01"}, + {"crypto/internal/fips140/check", "$\x0e\a\t\x02\xc1\x01]"}, + {"crypto/internal/fips140/check/checktest", ")\x97\x02\""}, + {"crypto/internal/fips140/drbg", "\x03 \x01\x01\x04\x14\x05\n2\x87\x01\x0f9\x01"}, + {"crypto/internal/fips140/ecdh", "\x03!\x05\x02\n\r<\x87\x01\x0f9"}, + {"crypto/internal/fips140/ecdsa", "\x03!\x04\x01\x02\a\x03\x06C\x15r\x0f9"}, + {"crypto/internal/fips140/ed25519", "\x03!\x05\x02\x04\fC\xcc\x01\x03"}, + {"crypto/internal/fips140/edwards25519", "\x1f\n\a\x12<\x96\x019"}, + {"crypto/internal/fips140/edwards25519/field", ")\x14\x05<\x96\x01"}, + {"crypto/internal/fips140/hkdf", "\x03!\x05\t\aE\x15"}, + {"crypto/internal/fips140/hmac", "\x03!\x15\x01\x01C\x15"}, + {"crypto/internal/fips140/mldsa", "\x03\x1c\x05\x05\x02\x0e\x01\x03\x05<\x96\x019"}, + {"crypto/internal/fips140/mlkem", "\x03!\x05\x02\x0f\x03\x05<\xcf\x01"}, + {"crypto/internal/fips140/nistec", "\x1f\n\r\f<\x96\x01,\r\x15"}, + {"crypto/internal/fips140/nistec/fiat", ")\x14A\x96\x01"}, + {"crypto/internal/fips140/pbkdf2", "\x03!\x05\t\aE\x15"}, + {"crypto/internal/fips140/rsa", "\x03\x1c\x05\x04\x01\x02\x0e\x01\x01\x02A\x15rH"}, + {"crypto/internal/fips140/sha256", "\x03!\x1e\x01\x065\x15\x81\x01"}, + {"crypto/internal/fips140/sha3", "\x03!\x19\x05\x01;\x96\x01N"}, + {"crypto/internal/fips140/sha512", "\x03!\x1e\x01\x065\x15\x81\x01"}, + {"crypto/internal/fips140/ssh", ")j"}, + {"crypto/internal/fips140/subtle", "\x1f\b\x1b\xd2\x01"}, + {"crypto/internal/fips140/tls12", "\x03!\x05\t\a\x02C\x15"}, + {"crypto/internal/fips140/tls13", "\x03!\x05\b\b\t<\x15"}, + {"crypto/internal/fips140cache", "\xc0\x02\r."}, {"crypto/internal/fips140deps", ""}, - {"crypto/internal/fips140deps/byteorder", "\xa0\x01"}, - {"crypto/internal/fips140deps/cpu", "\xb5\x01\a"}, - {"crypto/internal/fips140deps/godebug", "\xbd\x01"}, - {"crypto/internal/fips140deps/time", "\xcf\x02"}, - {"crypto/internal/fips140hash", "9\x1d4\xcb\x01"}, - {"crypto/internal/fips140only", "\x17\x13\x0e\x01\x01Pp"}, + {"crypto/internal/fips140deps/byteorder", "\xa9\x01"}, + {"crypto/internal/fips140deps/cpu", "\xbe\x01\b"}, + {"crypto/internal/fips140deps/godebug", "\xc7\x01"}, + {"crypto/internal/fips140deps/time", "\xe2\x02"}, + {"crypto/internal/fips140hash", ":\x1e;\xcf\x01"}, + {"crypto/internal/fips140only", "\x17\x14\x0e\x01\x01Xr"}, {"crypto/internal/fips140test", ""}, - {"crypto/internal/impl", "\xbe\x02"}, - {"crypto/internal/rand", "\x1b\x0f s=["}, - {"crypto/internal/randutil", "\xfa\x01\x12"}, - {"crypto/internal/sysrand", "tq! \r\r\x01\x01\r\x06"}, - {"crypto/internal/sysrand/internal/seccomp", "t"}, - {"crypto/md5", "\x0e8.\x16\x16i"}, - {"crypto/mlkem", "\x0e%"}, - {"crypto/mlkem/mlkemtest", "3\x13\b&"}, - {"crypto/pbkdf2", "6\x0f\x01.\x16"}, - {"crypto/rand", "\x1b\x0f\x1c\x03+\x86\x01\rN"}, - {"crypto/rc4", "& .\xc9\x01"}, - {"crypto/rsa", "\x0e\r\x01\v\x10\x0e\x01\x03\b\a\x1c\x03\x133=\f\x01"}, - {"crypto/sha1", "\x0e\r+\x02,\x16\x16\x15T"}, - {"crypto/sha256", "\x0e\r\x1dR"}, - {"crypto/sha3", "\x0e+Q\xcb\x01"}, - {"crypto/sha512", "\x0e\r\x1fP"}, - {"crypto/subtle", "\x1f\x1d\x9f\x01z"}, - {"crypto/tls", "\x03\b\x02\x01\x01\x01\x01\x02\x01\x01\x01\x02\x01\x01\x01\t\x01\x18\x01\x0f\x01\x03\x01\x01\x01\x01\x02\x01\x02\x01\x17\x02\x03\x13\x16\x15\b=\x16\x16\r\b\x01\x01\x01\x02\x01\x0e\x06\x02\x01\x0f"}, - {"crypto/tls/internal/fips140tls", "\x17\xaa\x02"}, - {"crypto/x509", "\x03\v\x01\x01\x01\x01\x01\x01\x01\x017\x06\x01\x01\x02\x05\x0e\x06\x02\x02\x03F\x03:\x01\x02\b\x01\x01\x02\a\x10\x05\x01\x06\a\b\x02\x01\x02\x0f\x02\x01\x01\x02\x03\x01"}, - {"crypto/x509/pkix", "j\x06\a\x90\x01H"}, - {"database/sql", "\x03\nQ\x16\x03\x83\x01\v\a\"\x05\b\x02\x03\x01\x0e\x02\x02\x02"}, - {"database/sql/driver", "\rg\x03\xb7\x01\x0f\x12"}, - {"debug/buildinfo", "\x03^\x02\x01\x01\b\a\x03g\x1a\x02\x01+\x0f "}, - {"debug/dwarf", "\x03j\a\x03\x83\x011\x11\x01\x01"}, - {"debug/elf", "\x03\x06W\r\a\x03g\x1b\x01\f \x17\x01\x17"}, - {"debug/gosym", "\x03j\n$\xa1\x01\x01\x01\x02"}, - {"debug/macho", "\x03\x06W\r\ng\x1c,\x17\x01"}, - {"debug/pe", "\x03\x06W\r\a\x03g\x1c,\x17\x01\x17"}, - {"debug/plan9obj", "m\a\x03g\x1c,"}, - {"embed", "t+B\x19\x01T"}, + {"crypto/internal/impl", "\xcb\x02"}, + {"crypto/internal/rand", "\x1b\x10 |>]"}, + {"crypto/internal/randutil", "\x85\x02\x12"}, + {"crypto/internal/sysrand", "~r!\"\r\r\x01\x01\r\x06"}, + {"crypto/internal/sysrand/internal/seccomp", "~"}, + {"crypto/md5", "\x0e97\x15\x16k"}, + {"crypto/mldsa", "\x0e%K\x87\x01"}, + {"crypto/mlkem", "\x0e&"}, + {"crypto/mlkem/mlkemtest", "4\x13\t."}, + {"crypto/pbkdf2", "7\x0f\x017\x15"}, + {"crypto/rand", "\x1b\x10\x1c\x034\x87\x01\rP"}, + {"crypto/rc4", "' 7\xcc\x01"}, + {"crypto/rsa", "\x0e\r\x01\f\x10\x0e\x01\x03\t\a$\x03\x124>\f\x01"}, + {"crypto/sha1", "\x0e\r,\x025\x15\x16\x15V"}, + {"crypto/sha256", "\x0e\r\x1eZ"}, + {"crypto/sha3", "\x0e,Y\xcf\x01"}, + {"crypto/sha512", "\x0e\r X"}, + {"crypto/subtle", "\x1f\x1e\xa9\x01|"}, + {"crypto/tls", "\x03\b\x02\x01\x01\x01\x01\x02\x01\x01\x01\x01\x01\x01\x01\x01\n\x01\x18\x01\x0f\x01\x01\x03\x01\x01\x01\x01\x02\x01\x02\x01\x1f\x02\x03\x12\x16\x15\t>\x16\x18\r\b\x01\x01\x01\x02\x01\x0e\x06\x03\x01\x15"}, + {"crypto/tls/internal/fips140tls", "\x17\xb7\x02"}, + {"crypto/x509", "\x03\v\x01\x01\x01\x01\x01\x01\x01\x01\x017\x01\x06\x01\x01\x02\x05\x0f\x06\t\x02\x03F\x03;\x01\x02\b\x01\x01\x02\a\x12\x05\x01\x06\a\b\x02\x01\x02\x0f\x02\x01\x01\x02\x04\x01"}, + {"crypto/x509/pkix", "m\x06\x0e\x91\x019\x11"}, + {"database/sql", "\x03\nS\x01\x1d\x03\x84\x01\v\a$\x05\b\x02\x03\x01\x0e\x02\x02\x02\x01"}, + {"database/sql/driver", "\rT\x1d\x03\xba\x01\x0f\x12\a"}, + {"database/sql/internal", ""}, + {"debug/buildinfo", "\x03a\x02\x01\x01\b\x0e\x03h\x1a\x02\x01-\x0f "}, + {"debug/dwarf", "\x03m\x0e\x03\x84\x013\x11\x01\x01"}, + {"debug/elf", "\x03\x06Z\r\x0e\x03h\x1b\x01\f\"\x17\x01\x17"}, + {"debug/gosym", "\x03m\x11#\xa5\x01\x01\x01\x02"}, + {"debug/macho", "\x03\x06Z\r\x11h\x1c.\x17\x01"}, + {"debug/pe", "\x03\x06Z\r\x0e\x03h\x1c.\x17\x01\x17"}, + {"debug/plan9obj", "p\x0e\x03h\x1c."}, + {"embed", "~*D\x19\x01V"}, {"embed/internal/embedtest", ""}, {"encoding", ""}, - {"encoding/ascii85", "\xfa\x01C"}, - {"encoding/asn1", "\x03q\x03g(\x01'\r\x02\x01\x11\x03\x01"}, - {"encoding/base32", "\xfa\x01A\x02"}, - {"encoding/base64", "\xa0\x01ZA\x02"}, - {"encoding/binary", "t\x86\x01\f(\r\x05"}, - {"encoding/csv", "\x02\x01q\x03\x83\x01D\x13\x02"}, - {"encoding/gob", "\x02f\x05\a\x03g\x1c\v\x01\x03\x1d\b\x12\x01\x10\x02"}, - {"encoding/hex", "t\x03\x83\x01A\x03"}, - {"encoding/json", "\x03\x01d\x04\b\x03\x83\x01\f(\r\x02\x01\x02\x11\x01\x01\x02"}, - {"encoding/pem", "\x03i\b\x86\x01A\x03"}, - {"encoding/xml", "\x02\x01e\f\x03\x83\x014\x05\n\x01\x02\x11\x02"}, - {"errors", "\xd0\x01\x85\x01"}, - {"expvar", "qLA\b\v\x15\r\b\x02\x03\x01\x12"}, - {"flag", "h\f\x03\x83\x01,\b\x05\b\x02\x01\x11"}, - {"fmt", "tF'\x19\f \b\r\x02\x03\x13"}, - {"go/ast", "\x03\x01s\x0f\x01s\x03)\b\r\x02\x01\x13\x02"}, - {"go/build", "\x02\x01q\x03\x01\x02\x02\b\x02\x01\x17\x1f\x04\x02\b\x1c\x13\x01+\x01\x04\x01\a\b\x02\x01\x13\x02\x02"}, - {"go/build/constraint", "t\xc9\x01\x01\x13\x02"}, - {"go/constant", "w\x10\x7f\x01\x024\x01\x02\x13"}, - {"go/doc", "\x04s\x01\x05\n=61\x10\x02\x01\x13\x02"}, - {"go/doc/comment", "\x03t\xc4\x01\x01\x01\x01\x13\x02"}, - {"go/format", "\x03t\x01\f\x01\x02sD"}, - {"go/importer", "y\a\x01\x02\x04\x01r9"}, - {"go/internal/gccgoimporter", "\x02\x01^\x13\x03\x04\f\x01p\x02,\x01\x05\x11\x01\r\b"}, - {"go/internal/gcimporter", "\x02u\x10\x010\x05\r0,\x15\x03\x02"}, - {"go/internal/scannerhooks", "\x87\x01"}, - {"go/internal/srcimporter", "w\x01\x01\v\x03\x01r,\x01\x05\x12\x02\x15"}, - {"go/parser", "\x03q\x03\x01\x02\b\x04\x01s\x01+\x06\x12"}, - {"go/printer", "w\x01\x02\x03\ns\f \x15\x02\x01\x02\f\x05\x02"}, - {"go/scanner", "\x03t\v\x05s2\x10\x01\x14\x02"}, - {"go/token", "\x04s\x86\x01>\x02\x03\x01\x10\x02"}, - {"go/types", "\x03\x01\x06j\x03\x01\x03\t\x03\x024\x063\x04\x03\t \x06\a\b\x01\x01\x01\x02\x01\x10\x02\x02"}, - {"go/version", "\xc2\x01|"}, - {"hash", "\xfa\x01"}, - {"hash/adler32", "t\x16\x16"}, - {"hash/crc32", "t\x16\x16\x15\x8b\x01\x01\x14"}, - {"hash/crc64", "t\x16\x16\xa0\x01"}, - {"hash/fnv", "t\x16\x16i"}, - {"hash/maphash", "\x8a\x01\x11<~"}, - {"html", "\xbe\x02\x02\x13"}, - {"html/template", "\x03n\x06\x19-=\x01\n!\x05\x01\x02\x03\f\x01\x02\r\x01\x03\x02"}, - {"image", "\x02r\x1fg\x0f4\x03\x01"}, + {"encoding/ascii85", "\x85\x02E"}, + {"encoding/asn1", "\x03{\x03h(\x01)\r\x02\x01\x11\x03\x01"}, + {"encoding/base32", "\x85\x02C\x02"}, + {"encoding/base64", "\xa9\x01\\C\x02"}, + {"encoding/binary", "~\x87\x01\f*\r\x05"}, + {"encoding/csv", "\x02\x01{\x03\x84\x01F\x13\x02"}, + {"encoding/gob", "\x02i\x05\x0e\x03h\x1c\v\x01\x03\x1f\b\x12\x01\x10\x02"}, + {"encoding/hex", "~\x03\x84\x01C\x03"}, + {"encoding/json", "\x03\x01g\n\x01\x01\x02\x01\x01\x03\x03\x84\x016\x0f\x01"}, + {"encoding/json/internal", "~"}, + {"encoding/json/internal/jsonflags", "u"}, + {"encoding/json/internal/jsonopts", "u\x01"}, + {"encoding/json/internal/jsontest", "\x03f\x15\x03\x83\x01\x01\x012\b\b\x03\x02\x0f"}, + {"encoding/json/internal/jsonwire", "\x04r\b\x87\x01\f7\x02\x01\x13\x01\x01"}, + {"encoding/json/jsontext", "\x03r\x01\x01\x02\x05\x87\x01\x03\t\x034\x02\x01\x02\x13"}, + {"encoding/json/v2", "\x03\x01g\x03\x01\x01\x03\x02\x01\x01\x02\x01\x04\x03\x84\x01\f\x03'\r\x02\x01\x02\x0f\x02\x02"}, + {"encoding/pem", "\x03l\x0f\x87\x01C\x03"}, + {"encoding/xml", "\x02\x01h\x13\x03\x84\x016\x05\n\x01\x02\x11\x02"}, + {"errors", "\xdb\x01\x87\x01"}, + {"expvar", "tSB\b\v\x17\r\b\x02\x03\x01\x12"}, + {"flag", "k\x13\x03\x84\x01.\b\x05\b\x02\x01\x11"}, + {"fmt", "~E)\x19\f\"\b\r\x02\x03\x13"}, + {"go/ast", "\x03\x01}\x0e\x01u\x03+\b\r\x02\x01\x13\x02"}, + {"go/build", "\x02\x01{\x03\x01\x02\x02\a\x02\x01\x17 \x04\x02\t\x1c\x13\x01-\x01\x04\x01\a\b\x02\x01\x13\x02\x02"}, + {"go/build/constraint", "~\xcc\x01\x01\x13\x02"}, + {"go/constant", "\x81\x01\x0f\x81\x01\x01\x026\x01\x02\x13"}, + {"go/doc", "\x04}\x01\x05\t>73\x10\x02\x01\x13\x02"}, + {"go/doc/comment", "\x03~\xc7\x01\x01\x01\x01\x13\x02"}, + {"go/format", "\x03~\x01\v\x01\x02uF"}, + {"go/importer", "\x83\x01\a\x01\x01\x04\x01t;"}, + {"go/internal/gccgoimporter", "\x02\x01a\x1a\x03\x04\v\x01r\x02.\x01\x05\x11\x01\r\b"}, + {"go/internal/gcimporter", "\x02\x7f\x0f\x010\x140.\x15\x03\x02"}, + {"go/internal/srcimporter", "\x81\x01\x01\x01\n\x03\x01t.\x01\x05\x12\x02\x15"}, + {"go/parser", "\x03{\x03\x01\x02\v\x01u\x01-\x06\x12"}, + {"go/printer", "\x81\x01\x01\x02\x03\tu\f\"\x15\x02\x01\x02\f\x05\x02"}, + {"go/scanner", "\x03~\x0fu4\x10\x01\x14\x02"}, + {"go/token", "\x04}\x87\x01@\x02\x03\x01\x10\x02"}, + {"go/types", "\x03\x01\x06t\x03\x01\x03\b\x03\x02\x0654\x04\x03\t\"\x06\a\b\x01\x01\x01\x02\x01\x10\x02\x02"}, + {"go/version", "\xcc\x01\x7f"}, + {"hash", "\x85\x02"}, + {"hash/adler32", "~\x15\x16"}, + {"hash/crc32", "~\x15\x16\x15\x8f\x01\x01\x14"}, + {"hash/crc64", "~\x15\x16\xa4\x01"}, + {"hash/fnv", "~\x15\x16k"}, + {"hash/maphash", "\x93\x01\x11>\x80\x01"}, + {"html", "\xcb\x02\x02\x13"}, + {"html/template", "\x03q\r\x18.>\x01\n#\x05\x01\x02\x03\n\x02\x01\x02\r\x01\x03\x02"}, + {"image", "\x02|\x1ei\x0f6\x03\x01"}, {"image/color", ""}, - {"image/color/palette", "\x93\x01"}, - {"image/draw", "\x92\x01\x01\x04"}, - {"image/gif", "\x02\x01\x05l\x03\x1b\x01\x01\x01\vZ\x0f"}, - {"image/internal/imageutil", "\x92\x01"}, - {"image/jpeg", "\x02r\x1e\x01\x04c"}, - {"image/png", "\x02\ad\n\x13\x02\x06\x01gC"}, - {"index/suffixarray", "\x03j\a\x86\x01\f+\n\x01"}, - {"internal/abi", "\xbc\x01\x99\x01"}, - {"internal/asan", "\xd5\x02"}, - {"internal/bisect", "\xb3\x02\r\x01"}, - {"internal/buildcfg", "wHg\x06\x02\x05\n\x01"}, - {"internal/bytealg", "\xb5\x01\xa0\x01"}, + {"image/color/palette", "\x9c\x01"}, + {"image/draw", "\x9b\x01\x01\x04"}, + {"image/gif", "\x02\x01\x05v\x03\x1a\x01\x01\x01\v\\\x0f"}, + {"image/internal/imageutil", "\x9b\x01"}, + {"image/jpeg", "\x02|\x1d\x01\x04e"}, + {"image/png", "\x02\ag\x11\x12\x02\x06\x01iE"}, + {"index/suffixarray", "\x03m\x0e\x87\x01\f-\n\x01"}, + {"internal/abi", "\xc6\x01\x9c\x01"}, + {"internal/asan", "\xe2\x02"}, + {"internal/bisect", "\xc0\x02\r\x01"}, + {"internal/buildcfg", "\x81\x01Hj\x06\x02\x05\n\x01"}, + {"internal/bytealg", "\xbe\x01\xa4\x01"}, {"internal/byteorder", ""}, {"internal/cfg", ""}, - {"internal/cgrouptest", "w[T\x06\x0f\x02\x01\x04\x01"}, - {"internal/chacha8rand", "\xa0\x01\x15\a\x99\x01"}, + {"internal/cgrouptest", "\x81\x01\\V\x06\x0f\x02\x01\x04\x01"}, + {"internal/chacha8rand", "\xa9\x01\x15\b\x9c\x01"}, {"internal/copyright", ""}, {"internal/coverage", ""}, {"internal/coverage/calloc", ""}, - {"internal/coverage/cfile", "q\x06\x17\x17\x01\x02\x01\x01\x01\x01\x01\x01\x01\"\x02',\x06\a\n\x01\x03\x0e\x06"}, - {"internal/coverage/cformat", "\x04s.\x04Q\v6\x01\x02\x0e"}, - {"internal/coverage/cmerge", "w.a"}, - {"internal/coverage/decodecounter", "m\n.\v\x02H,\x17\x18"}, - {"internal/coverage/decodemeta", "\x02k\n\x17\x17\v\x02H,"}, - {"internal/coverage/encodecounter", "\x02k\n.\f\x01\x02F\v!\x15"}, - {"internal/coverage/encodemeta", "\x02\x01j\n\x13\x04\x17\r\x02F,/"}, - {"internal/coverage/pods", "\x04s.\x81\x01\x06\x05\n\x02\x01"}, - {"internal/coverage/rtcov", "\xd5\x02"}, - {"internal/coverage/slicereader", "m\n\x83\x01["}, - {"internal/coverage/slicewriter", "w\x83\x01"}, - {"internal/coverage/stringtab", "w9\x04F"}, + {"internal/coverage/cfile", "t\r\x16\x17\x01\x02\x01\x01\x01\x01\x01\x01\x01$\x02'.\x06\a\n\x01\x03\x0e\x06"}, + {"internal/coverage/cformat", "\x04}-\x04S\v8\x01\x02\x0e"}, + {"internal/coverage/cmerge", "\x81\x01-c"}, + {"internal/coverage/decodecounter", "p\x11-\v\x02J.\x17\x18"}, + {"internal/coverage/decodemeta", "\x02n\x11\x16\x17\v\x02J."}, + {"internal/coverage/encodecounter", "\x02n\x11-\f\x01\x02H\v#\x15"}, + {"internal/coverage/encodemeta", "\x02\x01m\x11\x12\x04\x17\r\x02H./"}, + {"internal/coverage/pods", "\x04}-\x85\x01\x06\x05\n\x02\x01"}, + {"internal/coverage/rtcov", "\xe2\x02"}, + {"internal/coverage/slicereader", "p\x11\x84\x01]"}, + {"internal/coverage/slicewriter", "\x81\x01\x84\x01"}, + {"internal/coverage/stringtab", "\x81\x018\x04H"}, {"internal/coverage/test", ""}, {"internal/coverage/uleb128", ""}, - {"internal/cpu", "\xd5\x02"}, - {"internal/dag", "\x04s\xc4\x01\x03"}, - {"internal/diff", "\x03t\xc5\x01\x02"}, - {"internal/exportdata", "\x02\x01q\x03\x02e\x1c,\x01\x05\x11\x01\x02"}, - {"internal/filepathlite", "t+B\x1a@"}, - {"internal/fmtsort", "\x04\xaa\x02\r"}, - {"internal/fuzz", "\x03\nH\x18\x04\x03\x03\x01\f\x036=\f\x03\x1d\x01\x05\x02\x05\n\x01\x02\x01\x01\r\x04\x02"}, + {"internal/cpu", "\xe2\x02"}, + {"internal/dag", "\x04}\xc7\x01\x03"}, + {"internal/diff", "\x03~\xc8\x01\x02"}, + {"internal/exportdata", "\x02\x01{\x03\x02f\x1c.\x01\x05\x11\x01\x02"}, + {"internal/filepathlite", "~*D\x1aB"}, + {"internal/fmtsort", "\x04\xb7\x02\r"}, + {"internal/fuzz", "\x03\nJ\x19\x04\n\x03\x01\v\x037>\f\x03\x1f\x01\x05\x02\x05\n\x01\x02\x01\x01\r\x04\x02"}, + {"internal/gate", "\r"}, {"internal/goarch", ""}, - {"internal/godebug", "\x9d\x01!\x82\x01\x01\x14"}, + {"internal/godebug", "\xa6\x01\"\x85\x01\x01\x14"}, {"internal/godebugs", ""}, {"internal/goexperiment", ""}, {"internal/goos", ""}, - {"internal/goroot", "\xa6\x02\x01\x05\x12\x02"}, + {"internal/goroot", "\xb3\x02\x01\x05\x12\x02"}, {"internal/gover", "\x04"}, {"internal/goversion", ""}, - {"internal/lazyregexp", "\xa6\x02\v\r\x02"}, - {"internal/lazytemplate", "\xfa\x01,\x18\x02\r"}, - {"internal/msan", "\xd5\x02"}, + {"internal/lazyregexp", "\xb3\x02\v\r\x02"}, + {"internal/lazytemplate", "\x85\x02.\x18\x02\r"}, + {"internal/msan", "\xe2\x02"}, + {"internal/nettest", "\x03\nqG@\f\n\x12\x06\x15\x05\x0f"}, {"internal/nettrace", ""}, - {"internal/obscuretestdata", "l\x8e\x01,"}, - {"internal/oserror", "t"}, - {"internal/pkgbits", "\x03R\x18\a\x03\x04\fs\r\x1f\r\n\x01"}, + {"internal/obscuretestdata", "o\x96\x01."}, + {"internal/oserror", "~"}, + {"internal/pkgbits", "\x03T\x19\x0e\x03\x04\vu\r!\r\n\x01"}, {"internal/platform", ""}, - {"internal/poll", "tl\x05\x159\r\x01\x01\r\x06"}, - {"internal/profile", "\x03\x04m\x03\x83\x017\n\x01\x01\x01\x11"}, + {"internal/poll", "~m\x05\x15;\r\x01\x01\r\x06"}, + {"internal/profile", "\x03\x04w\x03\x84\x019\n\x01\x01\x01\x11"}, {"internal/profilerecord", ""}, - {"internal/race", "\x9b\x01\xba\x01"}, - {"internal/reflectlite", "\x9b\x01!;<\""}, - {"internal/runtime/atomic", "\xbc\x01\x99\x01"}, - {"internal/runtime/cgroup", "\x9f\x01=\x04u"}, - {"internal/runtime/exithook", "\xd1\x01\x84\x01"}, - {"internal/runtime/gc", "\xbc\x01"}, - {"internal/runtime/gc/internal/gen", "\nc\n\x18k\x04\v\x1d\b\x10\x02"}, - {"internal/runtime/gc/scan", "\xb5\x01\a\x18\az"}, - {"internal/runtime/maps", "\x9b\x01\x01 \n\t\t\x03z"}, - {"internal/runtime/math", "\xbc\x01"}, + {"internal/race", "\xa4\x01\xbe\x01"}, + {"internal/reflectlite", "\xa4\x01\"<>\""}, + {"internal/runtime/atomic", "\xc6\x01\x9c\x01"}, + {"internal/runtime/cgroup", "\xa8\x01?\x04w"}, + {"internal/runtime/exithook", "\xdc\x01\x86\x01"}, + {"internal/runtime/gc", "\xc6\x01"}, + {"internal/runtime/gc/internal/gen", "\nf\x11\x17m\x04\v\x1f\b\x10\x02"}, + {"internal/runtime/gc/scan", "\xbe\x01\b\x19\a|"}, + {"internal/runtime/maps", "\xa4\x01\x01\x04\x15\b\x03\a\n\t\x03.N"}, + {"internal/runtime/math", "\xc6\x01"}, {"internal/runtime/pprof/label", ""}, {"internal/runtime/startlinetest", ""}, - {"internal/runtime/sys", "\xbc\x01\x04"}, - {"internal/runtime/syscall/linux", "\xbc\x01\x99\x01"}, + {"internal/runtime/sys", "\xc6\x01\x04"}, + {"internal/runtime/syscall/linux", "\xc6\x01\x9c\x01"}, {"internal/runtime/wasitest", ""}, - {"internal/saferio", "\xfa\x01["}, - {"internal/singleflight", "\xc0\x02"}, - {"internal/strconv", "\x89\x02L"}, - {"internal/stringslite", "\x9f\x01\xb6\x01"}, - {"internal/sync", "\x9b\x01!\x13r\x14"}, - {"internal/synctest", "\x9b\x01\xba\x01"}, - {"internal/syscall/execenv", "\xc2\x02"}, - {"internal/syscall/unix", "\xb3\x02\x0e\x01\x13"}, - {"internal/sysinfo", "\x02\x01\xb2\x01E,\x18\x02"}, + {"internal/saferio", "\x85\x02]"}, + {"internal/singleflight", "\xcd\x02"}, + {"internal/strconv", "\x94\x02N"}, + {"internal/stringslite", "\xa8\x01\xba\x01"}, + {"internal/sync", "\xa4\x01\"\x14t\x14"}, + {"internal/synctest", "\xa4\x01\xbe\x01"}, + {"internal/syscall/execenv", "\xcf\x02"}, + {"internal/syscall/unix", "\xeb\x01U\x0e\x01\x13"}, + {"internal/sysinfo", "\x02\x01\xbb\x01G.\x18\x02"}, {"internal/syslist", ""}, - {"internal/testenv", "\x03\ng\x02\x01*\x1b\x0f0+\x01\x05\a\n\x01\x02\x02\x01\f"}, - {"internal/testhash", "\x03\x87\x01p\x118\f"}, - {"internal/testlog", "\xc0\x02\x01\x14"}, - {"internal/testpty", "t\x03\xaf\x01"}, - {"internal/trace", "\x02\x01\x01\x06c\a\x03w\x03\x03\x06\x03\t+\n\x01\x01\x01\x11\x06"}, - {"internal/trace/internal/testgen", "\x03j\nu\x03\x02\x03\x011\v\r\x11"}, - {"internal/trace/internal/tracev1", "\x03\x01i\a\x03}\x06\f5\x01"}, - {"internal/trace/raw", "\x02k\nz\x03\x06C\x01\x13"}, - {"internal/trace/testtrace", "\x02\x01q\x03q\x04\x03\x05\x01\x05,\v\x02\b\x02\x01\x05"}, + {"internal/testenv", "\x03\nq\x02\x01)\x1c\x100-\x01\x05\a\n\x01\x02\x02\x01\f"}, + {"internal/testhash", "\x03\x90\x01r\x11:\f"}, + {"internal/testlog", "\xcd\x02\x01\x14"}, + {"internal/testpty", "~\x03\xb2\x01"}, + {"internal/trace", "\x02\x01\x01\x06f\x0e\x03x\x03\x03\x06\x03\t-\n\x01\x01\x01\x11\x06"}, + {"internal/trace/internal/testgen", "\x03m\x11v\x03\x02\x03\x013\v\r\x11"}, + {"internal/trace/internal/tracev1", "\x03\x01l\x0e\x03~\x06\f7\x01"}, + {"internal/trace/raw", "\x02n\x11{\x03\x06E\x01\x13"}, + {"internal/trace/testtrace", "\x02\x01{\x03r\x04\x03\x05\x01\x05.\v\x02\b\x02\x01\x05"}, {"internal/trace/tracev2", ""}, - {"internal/trace/traceviewer", "\x02d\v\x06\x1a<\x1f\a\a\x04\b\v\x15\x01\x05\a\n\x01\x02\x0f"}, + {"internal/trace/traceviewer", "\x02g\v\r\x19>\x1f\a\a\x04\b\v\x17\x01\x05\a\n\x01\x02\x0f"}, {"internal/trace/traceviewer/format", ""}, - {"internal/trace/version", "wz\t"}, - {"internal/txtar", "\x03t\xaf\x01\x18"}, - {"internal/types/errors", "\xbd\x02"}, - {"internal/unsafeheader", "\xd5\x02"}, - {"internal/xcoff", "`\r\a\x03g\x1c,\x17\x01"}, - {"internal/zstd", "m\a\x03\x83\x01\x0f"}, - {"io", "t\xcc\x01"}, - {"io/fs", "t+*11\x10\x14\x04"}, - {"io/ioutil", "\xfa\x01\x01+\x15\x03"}, - {"iter", "\xcf\x01d\""}, - {"log", "w\x83\x01\x05'\r\r\x01\x0e"}, + {"internal/trace/version", "\x81\x01{\t"}, + {"internal/txtar", "\x03~\xb2\x01\x18"}, + {"internal/types/errors", "\xca\x02"}, + {"internal/unsafeheader", "\xe2\x02"}, + {"internal/xcoff", "c\r\x0e\x03h\x1c.\x17\x01"}, + {"internal/zstd", "p\x0e\x03\x84\x01\x0f"}, + {"io", "~\xcf\x01"}, + {"io/fs", "~*,13\x10\x14\x04"}, + {"io/ioutil", "\x85\x02\x01-\x15\x03"}, + {"iter", "\xda\x01f\""}, + {"log", "\x81\x01\x84\x01\x05)\r\r\x01\x0e"}, {"log/internal", ""}, - {"log/slog", "\x03\n[\t\x03\x03\x83\x01\x04\x01\x02\x02\x03(\x05\b\x02\x01\x02\x01\x0e\x02\x02\x02"}, + {"log/slog", "\x03\n^\t\n\x03H<\x04\x01\x02\x02\x03*\x05\b\x02\x01\x02\x01\x0e\x02\x02\x02"}, {"log/slog/internal", ""}, - {"log/slog/internal/benchmarks", "\rg\x03\x83\x01\x06\x03:\x12"}, - {"log/slog/internal/buffer", "\xc0\x02"}, - {"log/syslog", "t\x03\x87\x01\x12\x16\x18\x02\x0f"}, - {"maps", "\xfd\x01X"}, - {"math", "\xb5\x01TL"}, - {"math/big", "\x03q\x03)\x15E\f\x03\x020\x02\x01\x02\x15"}, - {"math/big/internal/asmgen", "\x03\x01s\x92\x012\x03"}, - {"math/bits", "\xd5\x02"}, - {"math/cmplx", "\x86\x02\x03"}, - {"math/rand", "\xbd\x01I:\x01\x14"}, - {"math/rand/v2", "t,\x03c\x03L"}, - {"mime", "\x02\x01i\b\x03\x83\x01\v!\x15\x03\x02\x11\x02"}, - {"mime/multipart", "\x02\x01N#\x03F=\v\x01\a\x02\x15\x02\x06\x0f\x02\x01\x17"}, - {"mime/quotedprintable", "\x02\x01t\x83\x01"}, - {"net", "\x04\tg+\x1e\n\x05\x13\x01\x01\x04\x15\x01%\x06\r\b\x05\x01\x01\r\x06\a"}, - {"net/http", "\x02\x01\x03\x01\x04\x02D\b\x13\x01\a\x03F=\x01\x03\a\x01\x03\x02\x02\x01\x02\x06\x02\x01\x01\n\x01\x01\x05\x01\x02\x05\b\x01\x01\x01\x02\x01\x0e\x02\x02\x02\b\x01\x01\x01"}, - {"net/http/cgi", "\x02W\x1b\x03\x83\x01\x04\a\v\x01\x13\x01\x01\x01\x04\x01\x05\x02\b\x02\x01\x11\x0e"}, - {"net/http/cookiejar", "\x04p\x03\x99\x01\x01\b\a\x05\x16\x03\x02\x0f\x04"}, - {"net/http/fcgi", "\x02\x01\n`\a\x03\x83\x01\x16\x01\x01\x14\x18\x02\x0f"}, - {"net/http/httptest", "\x02\x01\nL\x02\x1b\x01\x83\x01\x04\x12\x01\n\t\x02\x17\x01\x02\x0f\x0e"}, - {"net/http/httptrace", "\rLnI\x14\n!"}, - {"net/http/httputil", "\x02\x01\ng\x03\x83\x01\x04\x0f\x03\x01\x05\x02\x01\v\x01\x19\x02\x01\x0e\x0e"}, - {"net/http/internal", "\x02\x01q\x03\x83\x01"}, - {"net/http/internal/ascii", "\xbe\x02\x13"}, - {"net/http/internal/httpcommon", "\rg\x03\x9f\x01\x0e\x01\x17\x01\x01\x02\x1d\x02"}, - {"net/http/internal/testcert", "\xbe\x02"}, - {"net/http/pprof", "\x02\x01\nj\x19-\x02\x0e-\x04\x13\x14\x01\r\x04\x03\x01\x02\x01\x11"}, + {"log/slog/internal/benchmarks", "\rq\x03\x84\x01\x06\x03<\x12"}, + {"log/slog/internal/buffer", "\xcd\x02"}, + {"log/syslog", "~\x03\x88\x01\x12\x18\x18\x02\x0f"}, + {"maps", "\x88\x02Z"}, + {"math", "\xbe\x01VN"}, + {"math/big", "\x03{\x03(\x15G\f\x03\x022\x02\x01\x02\x15"}, + {"math/big/internal/asmgen", "\x03\x01}\x93\x014\x03"}, + {"math/bits", "\xe2\x02"}, + {"math/cmplx", "\x91\x02\x03"}, + {"math/rand", "\xc7\x01J<\x01\x14"}, + {"math/rand/v2", "~+\x03e\x03N"}, + {"mime", "\x02\x01l\x0f\x03\x84\x01\v#\x15\x03\x02\x11\x02"}, + {"mime/multipart", "\x02\x01P+\x03F>\v\x01\a\x02\x17\x02\x06\x0f\x02\x01\x17"}, + {"mime/quotedprintable", "\x02\x01~\x84\x01"}, + {"net", "\x04\tq*\x1f\v\x05\x13\x01\x01\x04\x15\x01'\x06\r\b\x05\x01\x01\r\x06\t"}, + {"net/http", "\x02\x01\x03\x01\x04\x02N\x14\x0f\x03F>\x01\x03\a\x01\x06\x01\x01\x02\x06\x02\x01\x01\f\x01\x01\x05\x01\x02\x05\b\x01\x01\x01\x02\x01\x0e\x02\x02\x02\n\x01\x03"}, + {"net/http/cgi", "\x02Y#\x03\x84\x01\x04\a\v\x01\x15\x01\x01\x01\x04\x01\x05\x02\b\x02\x01\x11\x10"}, + {"net/http/cookiejar", "\x04z\x03\x9a\x01\x01\b\t\x05\x16\x03\x02\x0f\x04"}, + {"net/http/fcgi", "\x02\x01\nc\x0e\x03\x84\x01\x16\x01\x01\x16\x18\x02\x0f"}, + {"net/http/httptest", "\x02\x01\nN\x02#\x01P4\x04\x12\x01\f\t\x02\r\n\x01\x02\x03\f\x06\n"}, + {"net/http/httptrace", "\rNwI\x16+"}, + {"net/http/httputil", "\x02\x01\nq\x03F>\x04\x0f\x03\x01\x05\x02\x01\r\x01\x19\x02\x01\x0e\x10"}, + {"net/http/internal", "\x02\x01m\x0e\x03\x84\x01"}, + {"net/http/internal/ascii", "\xcb\x02\x13"}, + {"net/http/internal/http2", "\x02\x01\x03\x01\x06F\b\x15\x0e\x03\x84\x01\x01\x03\b\x03\x02\x03\x02\x06\x02\x03\x01\n\x01\x01\b\x05\b\x02\x01\x02\x01\x0e\x10\x02\x02"}, + {"net/http/internal/httpcommon", "\rq\x03\xa0\x01\x10\x01\x17\x01\x01\x02\x1f\x02"}, + {"net/http/internal/httpsfv", "\xc8\x02\x02\x01\x11\x04"}, + {"net/http/internal/testcert", "\xcb\x02"}, + {"net/http/pprof", "\x02\x01\nt\x18.\x11-\x04\x13\x16\x01\r\x04\x03\x01\x02\x01\x11"}, {"net/internal/cgotest", ""}, - {"net/internal/socktest", "w\xc9\x01\x02"}, - {"net/mail", "\x02r\x03\x83\x01\x04\x0f\x03\x14\x1a\x02\x0f\x04"}, - {"net/netip", "\x04p+\x01f\x034\x17"}, - {"net/rpc", "\x02m\x05\x03\x10\ni\x04\x12\x01\x1d\r\x03\x02"}, - {"net/rpc/jsonrpc", "q\x03\x03\x83\x01\x16\x11\x1f"}, - {"net/smtp", "\x194\f\x13\b\x03\x83\x01\x16\x14\x1a"}, - {"net/textproto", "\x02\x01q\x03\x83\x01\f\n-\x01\x02\x15"}, - {"net/url", "t\x03Fc\v\x10\x02\x01\x17"}, - {"os", "t+\x01\x19\x03\x10\x14\x01\x03\x01\x05\x10\x018\b\x05\x01\x01\r\x06"}, - {"os/exec", "\x03\ngI'\x01\x15\x01+\x06\a\n\x01\x03\x01\r"}, - {"os/exec/internal/fdtest", "\xc2\x02"}, - {"os/signal", "\r\x99\x02\x15\x05\x02"}, - {"os/user", "\x02\x01q\x03\x83\x01,\r\n\x01\x02"}, - {"path", "t+\xb4\x01"}, - {"path/filepath", "t+\x1aB+\r\b\x03\x04\x11"}, - {"plugin", "t"}, - {"reflect", "t'\x04\x1d\x13\b\x04\x05\x17\x06\t-\n\x03\x11\x02\x02"}, + {"net/internal/socktest", "\x81\x01\xcc\x01\x02"}, + {"net/mail", "\x02|\x03\x84\x01\x04\x0f\x03\x16\x1a\x02\x0f\x04"}, + {"net/netip", "\x04z*\x01h\x036\x17"}, + {"net/rpc", "\x02p\f\x03\x0f\nk\x04\x12\x01\x1f\r\x03\x02"}, + {"net/rpc/jsonrpc", "t\n\x03\x84\x01\x16\x13\x1f"}, + {"net/smtp", "\x195\r\x14\x0f\x03\x84\x01\x16\x16\x1a"}, + {"net/textproto", "\x02\x01{\x03\x84\x01\f\n/\x01\x02\x15"}, + {"net/url", "~\x03Ff\v\x10\x02\x01\x17"}, + {"os", "~*\x01\x19\x04\x11\x14\x01\x03\x01\x05\x10\x01:\b\x05\x01\x01\r\x06"}, + {"os/exec", "\x03\nqI(\x01\x15\x01-\x06\a\n\x01\x03\x01\r"}, + {"os/exec/internal/fdtest", "\xcf\x02"}, + {"os/signal", "\r\xa6\x02\x15\x05\x02"}, + {"os/user", "\x02\x01{\x03\x84\x01.\r\n\x01\x02"}, + {"path", "~*\xb8\x01"}, + {"path/filepath", "~*\x1aD-\r\b\x03\x04\x11"}, + {"plugin", "~"}, + {"reflect", "~&\x04\x1e\x03\x11\b\x04\x05\x17\x06\t/\n\x03\x11\x02\x02"}, {"reflect/internal/example1", ""}, {"reflect/internal/example2", ""}, - {"regexp", "\x03\xf7\x018\t\x02\x01\x02\x11\x02"}, - {"regexp/syntax", "\xbb\x02\x01\x01\x01\x02\x11\x02"}, - {"runtime", "\x9b\x01\x04\x01\x03\f\x06\a\x02\x01\x01\x0e\x03\x01\x01\x01\x02\x01\x01\x01\x02\x01\x04\x01\x10\x18L"}, - {"runtime/coverage", "\xa7\x01S"}, - {"runtime/debug", "wUZ\r\b\x02\x01\x11\x06"}, - {"runtime/metrics", "\xbe\x01H-\""}, - {"runtime/pprof", "\x02\x01\x01\x03\x06`\a\x03$$\x0f\v!\f \r\b\x01\x01\x01\x02\x02\n\x03\x06"}, - {"runtime/race", "\xb9\x02"}, + {"regexp", "\x03\x82\x02\x037\t\x02\x01\x02\x11\x02"}, + {"regexp/syntax", "\xc8\x02\x01\x01\x01\x02\x11\x02"}, + {"runtime", "\xa4\x01\x04\x01\x03\f\x06\b\x02\x01\x01\x0f\x03\x01\x01\x01\x02\x01\x01\x01\x02\x01\x04\x01\x10\x18N"}, + {"runtime/coverage", "\xb0\x01U"}, + {"runtime/debug", "\x81\x01V\\\r\b\x02\x01\x11\x06"}, + {"runtime/metrics", "\xc8\x01I/\""}, + {"runtime/pprof", "\x02\x01\x01\x03\x06c\x0e\x03#5\v!\f\"\r\b\x01\x01\x01\x02\x02\n\x03\x06"}, + {"runtime/race", "\xc6\x02"}, {"runtime/race/internal/amd64v1", ""}, - {"runtime/trace", "\rg\x03z\t9\b\x05\x01\x0e\x06"}, - {"slices", "\x04\xf9\x01\fL"}, - {"sort", "\xd0\x0192"}, - {"strconv", "t+A\x01r"}, - {"strings", "t'\x04B\x19\x03\f7\x11\x02\x02"}, + {"runtime/trace", "\rq\x03{\t;\b\x05\x01\x0e\x06"}, + {"slices", "\x04\x84\x02\fN"}, + {"sort", "\xdb\x0194"}, + {"strconv", "~*C\x01t"}, + {"strings", "~&\x04D\x19\x03\f9\x11\x02\x02"}, {"structs", ""}, - {"sync", "\xcf\x01\x13\x01P\x0e\x14"}, - {"sync/atomic", "\xd5\x02"}, - {"syscall", "t(\x03\x01\x1c\n\x03\x06\r\x04S\b\x05\x01\x14"}, - {"testing", "\x03\ng\x02\x01X\x17\x14\f\x05\x1b\x06\x02\x05\x02\x05\x01\x02\x01\x02\x01\x0e\x02\x04"}, - {"testing/cryptotest", "QOZ\x124\x03\x12"}, - {"testing/fstest", "t\x03\x83\x01\x01\n&\x10\x03\t\b"}, - {"testing/internal/testdeps", "\x02\v\xae\x01/\x10,\x03\x05\x03\x06\a\x02\x0f"}, - {"testing/iotest", "\x03q\x03\x83\x01\x04"}, - {"testing/quick", "v\x01\x8f\x01\x05#\x10\x11"}, - {"testing/slogtest", "\rg\x03\x89\x01.\x05\x10\f"}, - {"testing/synctest", "\xe3\x01`\x12"}, - {"text/scanner", "\x03t\x83\x01,+\x02"}, - {"text/tabwriter", "w\x83\x01Y"}, - {"text/template", "t\x03C@\x01\n \x01\x05\x01\x02\x05\v\x02\x0e\x03\x02"}, - {"text/template/parse", "\x03t\xbc\x01\n\x01\x13\x02"}, - {"time", "t+\x1e$(*\r\x02\x13"}, - {"time/tzdata", "t\xce\x01\x13"}, + {"sync", "\xda\x01\x02\x11\x01R\x0e\x14"}, + {"sync/atomic", "\xe2\x02"}, + {"syscall", "~'\x03\x01\x1d\n\x04\x06\r\x04U\b\x05\x01\x14"}, + {"testing", "\x03\nq\x02\x01Y\x17\x14\f\x05\x1d\x06\x02\x05\x02\x05\x01\x02\x01\x02\x01\x0e\x02\x04"}, + {"testing/cryptotest", "SV\\\x126\x03\x12"}, + {"testing/fstest", "~\x03\x84\x01\x01\n(\x10\x03\t\b"}, + {"testing/internal/testdeps", "\x02\v\xb7\x011\x10.\x03\x05\x03\x06\a\x02\x0f"}, + {"testing/iotest", "\x03{\x03\x84\x01\x04"}, + {"testing/quick", "\x80\x01\x01\x90\x01\x05%\x10\x11"}, + {"testing/slogtest", "\rq\x03\x8a\x010\x05\x10\f"}, + {"testing/synctest", "\xee\x01b\f\x06"}, + {"text/scanner", "\x03~\x84\x01.+\x02"}, + {"text/tabwriter", "\x81\x01\x84\x01["}, + {"text/template", "~\x03BB\x01\n\"\x01\x05\x01\x02\x05\v\x02\x0e\x03\x02"}, + {"text/template/parse", "\x03~\xbf\x01\n\x01\x13\x02"}, + {"time", "~*D(,\r\x02\x13"}, + {"time/tzdata", "~\xd1\x01\x13"}, {"unicode", ""}, {"unicode/utf16", ""}, {"unicode/utf8", ""}, - {"unique", "\x9b\x01!%\x01Q\r\x01\x14\x12"}, + {"unique", "\xa4\x01\"&\x01S\r\x01\x14\x19"}, {"unsafe", ""}, - {"vendor/golang.org/x/crypto/chacha20", "\x10]\a\x95\x01*'"}, - {"vendor/golang.org/x/crypto/chacha20poly1305", "\x10\aV\a\xe2\x01\x04\x01\a"}, - {"vendor/golang.org/x/crypto/cryptobyte", "j\n\x03\x90\x01'!\n"}, + {"uuid", "\x03\x01O\x1d\x03\v\xcf\x01\x0f"}, + {"vendor/golang.org/x/crypto/chacha20", "\x10`\x0e\x96\x01,)"}, + {"vendor/golang.org/x/crypto/chacha20poly1305", "\x10\aY\x0e\xe6\x01\x05\x01\f"}, + {"vendor/golang.org/x/crypto/cryptobyte", "m\x11\x03\x91\x01)!\v"}, {"vendor/golang.org/x/crypto/cryptobyte/asn1", ""}, - {"vendor/golang.org/x/crypto/internal/alias", "\xd5\x02"}, - {"vendor/golang.org/x/crypto/internal/poly1305", "X\x15\x9c\x01"}, - {"vendor/golang.org/x/net/dns/dnsmessage", "t\xc7\x01"}, - {"vendor/golang.org/x/net/http/httpguts", "\x90\x02\x14\x1a\x15\r"}, - {"vendor/golang.org/x/net/http/httpproxy", "t\x03\x99\x01\x10\x05\x01\x18\x15\r"}, - {"vendor/golang.org/x/net/http2/hpack", "\x03q\x03\x83\x01F"}, - {"vendor/golang.org/x/net/idna", "w\x8f\x018\x15\x10\x02\x01"}, - {"vendor/golang.org/x/net/nettest", "\x03j\a\x03\x83\x01\x11\x05\x16\x01\f\n\x01\x02\x02\x01\f"}, - {"vendor/golang.org/x/sys/cpu", "\xa6\x02\r\n\x01\x17"}, - {"vendor/golang.org/x/text/secure/bidirule", "t\xdf\x01\x11\x01"}, - {"vendor/golang.org/x/text/transform", "\x03q\x86\x01Y"}, - {"vendor/golang.org/x/text/unicode/bidi", "\x03\bl\x87\x01>\x17"}, - {"vendor/golang.org/x/text/unicode/norm", "m\n\x83\x01F\x13\x11"}, - {"weak", "\x9b\x01\x98\x01\""}, + {"vendor/golang.org/x/crypto/hkdf", "\x18\x01e\x15r"}, + {"vendor/golang.org/x/crypto/internal/alias", "\xe2\x02"}, + {"vendor/golang.org/x/crypto/internal/poly1305", "Z\x16\xa4\x01"}, + {"vendor/golang.org/x/net/dns/dnsmessage", "~\xca\x01"}, + {"vendor/golang.org/x/net/http/httpguts", "\x9b\x02\x16\x1a\x15\x10"}, + {"vendor/golang.org/x/net/http/httpproxy", "~\x03\x9a\x01\x12\x05\x01\x18\x15\x10"}, + {"vendor/golang.org/x/net/http2/hpack", "\x03{\x03\x84\x01H"}, + {"vendor/golang.org/x/net/http3", "\x9c\x02@\x06\x0f\x04"}, + {"vendor/golang.org/x/net/idna", "\x81\x01\x90\x01:\x13\x02\x17\x02\x01"}, + {"vendor/golang.org/x/net/internal/http3", "\rN#\x03\x84\x01\v\x04\a\x01\x05\x10\x01\x16\x02\x01\x02\x0f\x10\x02\x04\x03"}, + {"vendor/golang.org/x/net/internal/httpcommon", "\rq\x03\xa0\x01\x10\x01\x17\x01\x01\x02\x1f\x02"}, + {"vendor/golang.org/x/net/internal/quic/quicwire", "p"}, + {"vendor/golang.org/x/net/nettest", "\x03m\x0e\x03\x84\x01\x11\x05\x18\x01\f\n\x01\x02\x02\x01\f"}, + {"vendor/golang.org/x/net/quic", "\x03\n\x01\x01\x01\t:\x04\x04\x15\x03\v\x03\x12r\x06\x06\x06\x04\x12\x06\x15\x02\x01\x02\x01\x01\r\x06\x02\x01\x01\x02\v"}, + {"vendor/golang.org/x/sys/cpu", "\xb3\x02\r\n\x01\x17"}, + {"vendor/golang.org/x/text/secure/bidirule", "~\xe2\x01\x18\x01"}, + {"vendor/golang.org/x/text/transform", "\x03{\x87\x01["}, + {"vendor/golang.org/x/text/unicode/bidi", "\x03\bv\x88\x01@\x17"}, + {"vendor/golang.org/x/text/unicode/norm", "p\x11\x84\x01H\x13\x18"}, + {"weak", "\xa4\x01\x9c\x01\""}, } // bootstrap is the list of bootstrap packages extracted from cmd/dist. @@ -408,6 +428,7 @@ "cmd/compile/internal/logopt": true, "cmd/compile/internal/loong64": true, "cmd/compile/internal/loopvar": true, + "cmd/compile/internal/midway": true, "cmd/compile/internal/mips": true, "cmd/compile/internal/mips64": true, "cmd/compile/internal/noder": true, @@ -512,6 +533,7 @@ "internal/race": true, "internal/runtime/gc": true, "internal/saferio": true, + "internal/strconv": true, "internal/syscall/unix": true, "internal/types/errors": true, "internal/unsafeheader": true,
diff --git a/internal/stdlib/manifest.go b/internal/stdlib/manifest.go index 33e4f50..2180c29 100644 --- a/internal/stdlib/manifest.go +++ b/internal/stdlib/manifest.go
@@ -270,6 +270,7 @@ {"ContainsRune", Func, 7, "func(b []byte, r rune) bool"}, {"Count", Func, 0, "func(s []byte, sep []byte) int"}, {"Cut", Func, 18, "func(s []byte, sep []byte) (before []byte, after []byte, found bool)"}, + {"CutLast", Func, 27, "func(s []byte, sep []byte) (before []byte, after []byte, found bool)"}, {"CutPrefix", Func, 20, "func(s []byte, prefix []byte) (after []byte, found bool)"}, {"CutSuffix", Func, 20, "func(s []byte, suffix []byte) (before []byte, found bool)"}, {"Equal", Func, 0, "func(a []byte, b []byte) bool"}, @@ -538,6 +539,7 @@ {"MD4", Const, 0, ""}, {"MD5", Const, 0, ""}, {"MD5SHA1", Const, 0, ""}, + {"MLDSAMu", Const, 27, ""}, {"MessageSigner", Type, 25, ""}, {"PrivateKey", Type, 0, ""}, {"PublicKey", Type, 2, ""}, @@ -812,6 +814,40 @@ {"Size", Const, 0, ""}, {"Sum", Func, 2, "func(data []byte) [16]byte"}, }, + "crypto/mldsa": { + {"(*Options).HashFunc", Method, 27, ""}, + {"(*PrivateKey).Bytes", Method, 27, ""}, + {"(*PrivateKey).Equal", Method, 27, ""}, + {"(*PrivateKey).Public", Method, 27, ""}, + {"(*PrivateKey).PublicKey", Method, 27, ""}, + {"(*PrivateKey).Sign", Method, 27, ""}, + {"(*PrivateKey).SignDeterministic", Method, 27, ""}, + {"(*PublicKey).Bytes", Method, 27, ""}, + {"(*PublicKey).Equal", Method, 27, ""}, + {"(*PublicKey).Parameters", Method, 27, ""}, + {"(Parameters).PublicKeySize", Method, 27, ""}, + {"(Parameters).SignatureSize", Method, 27, ""}, + {"(Parameters).String", Method, 27, ""}, + {"GenerateKey", Func, 27, "func(params Parameters) (*PrivateKey, error)"}, + {"MLDSA44", Func, 27, "func() Parameters"}, + {"MLDSA44PublicKeySize", Const, 27, ""}, + {"MLDSA44SignatureSize", Const, 27, ""}, + {"MLDSA65", Func, 27, "func() Parameters"}, + {"MLDSA65PublicKeySize", Const, 27, ""}, + {"MLDSA65SignatureSize", Const, 27, ""}, + {"MLDSA87", Func, 27, "func() Parameters"}, + {"MLDSA87PublicKeySize", Const, 27, ""}, + {"MLDSA87SignatureSize", Const, 27, ""}, + {"NewPrivateKey", Func, 27, "func(params Parameters, seed []byte) (*PrivateKey, error)"}, + {"NewPublicKey", Func, 27, "func(params Parameters, encoding []byte) (*PublicKey, error)"}, + {"Options", Type, 27, ""}, + {"Options.Context", Field, 27, ""}, + {"Parameters", Type, 27, ""}, + {"PrivateKey", Type, 27, ""}, + {"PrivateKeySize", Const, 27, ""}, + {"PublicKey", Type, 27, ""}, + {"Verify", Func, 27, "func(pk *PublicKey, message []byte, signature []byte, opts *Options) error"}, + }, "crypto/mlkem": { {"(*DecapsulationKey1024).Bytes", Method, 24, ""}, {"(*DecapsulationKey1024).Decapsulate", Method, 24, ""}, @@ -1120,6 +1156,7 @@ {"ConnectionState.ECHAccepted", Field, 23, ""}, {"ConnectionState.HandshakeComplete", Field, 0, ""}, {"ConnectionState.HelloRetryRequest", Field, 26, ""}, + {"ConnectionState.LocalCertificate", Field, 27, ""}, {"ConnectionState.NegotiatedProtocol", Field, 0, ""}, {"ConnectionState.NegotiatedProtocolIsMutual", Field, 0, ""}, {"ConnectionState.OCSPResponse", Field, 5, ""}, @@ -1152,6 +1189,10 @@ {"InsecureCipherSuites", Func, 14, "func() []*CipherSuite"}, {"Listen", Func, 0, "func(network string, laddr string, config *Config) (net.Listener, error)"}, {"LoadX509KeyPair", Func, 0, "func(certFile string, keyFile string) (Certificate, error)"}, + {"MLDSA44", Const, 27, ""}, + {"MLDSA65", Const, 27, ""}, + {"MLDSA87", Const, 27, ""}, + {"MLKEM1024", Const, 27, ""}, {"NewLRUClientSessionCache", Func, 3, "func(capacity int) ClientSessionCache"}, {"NewListener", Func, 0, "func(inner net.Listener, config *Config) net.Listener"}, {"NewResumptionState", Func, 21, "func(ticket []byte, state *SessionState) (*ClientSessionState, error)"}, @@ -1166,6 +1207,7 @@ {"ParseSessionState", Func, 21, "func(data []byte) (*SessionState, error)"}, {"QUICClient", Func, 21, "func(config *QUICConfig) *QUICConn"}, {"QUICConfig", Type, 21, ""}, + {"QUICConfig.ClientHelloInfoConn", Field, 27, ""}, {"QUICConfig.EnableSessionEvents", Field, 23, ""}, {"QUICConfig.TLSConfig", Field, 21, ""}, {"QUICConn", Type, 21, ""}, @@ -1334,6 +1376,7 @@ {"Certificate.PublicKeyAlgorithm", Field, 0, ""}, {"Certificate.Raw", Field, 0, ""}, {"Certificate.RawIssuer", Field, 0, ""}, + {"Certificate.RawSignatureAlgorithm", Field, 27, ""}, {"Certificate.RawSubject", Field, 0, ""}, {"Certificate.RawSubjectPublicKeyInfo", Field, 0, ""}, {"Certificate.RawTBSCertificate", Field, 0, ""}, @@ -1362,6 +1405,7 @@ {"CertificateRequest.PublicKey", Field, 3, ""}, {"CertificateRequest.PublicKeyAlgorithm", Field, 3, ""}, {"CertificateRequest.Raw", Field, 3, ""}, + {"CertificateRequest.RawSignatureAlgorithm", Field, 27, ""}, {"CertificateRequest.RawSubject", Field, 3, ""}, {"CertificateRequest.RawSubjectPublicKeyInfo", Field, 3, ""}, {"CertificateRequest.RawTBSCertificateRequest", Field, 3, ""}, @@ -1422,6 +1466,10 @@ {"KeyUsageKeyEncipherment", Const, 0, ""}, {"MD2WithRSA", Const, 0, ""}, {"MD5WithRSA", Const, 0, ""}, + {"MLDSA", Const, 27, ""}, + {"MLDSA44", Const, 27, ""}, + {"MLDSA65", Const, 27, ""}, + {"MLDSA87", Const, 27, ""}, {"MarshalECPrivateKey", Func, 2, "func(key *ecdsa.PrivateKey) ([]byte, error)"}, {"MarshalPKCS1PrivateKey", Func, 0, "func(key *rsa.PrivateKey) []byte"}, {"MarshalPKCS1PublicKey", Func, 10, "func(key *rsa.PublicKey) []byte"}, @@ -1468,6 +1516,7 @@ {"RevocationList.Number", Field, 15, ""}, {"RevocationList.Raw", Field, 19, ""}, {"RevocationList.RawIssuer", Field, 19, ""}, + {"RevocationList.RawSignatureAlgorithm", Field, 27, ""}, {"RevocationList.RawTBSRevocationList", Field, 19, ""}, {"RevocationList.RevokedCertificateEntries", Field, 21, ""}, {"RevocationList.RevokedCertificates", Field, 15, ""}, @@ -1648,6 +1697,7 @@ {"(Scanner).Scan", Method, 0, ""}, {"ColumnType", Type, 8, ""}, {"Conn", Type, 9, ""}, + {"ConvertAssign", Func, 27, "func(scanCtx driver.ScanContext, dest any, src driver.Value) error"}, {"DB", Type, 0, ""}, {"DBStats", Type, 5, ""}, {"DBStats.Idle", Field, 11, ""}, @@ -1744,6 +1794,11 @@ {"(Rows).Next", Method, 0, ""}, {"(RowsAffected).LastInsertId", Method, 0, ""}, {"(RowsAffected).RowsAffected", Method, 0, ""}, + {"(RowsColumnScanner).Close", Method, 27, ""}, + {"(RowsColumnScanner).Columns", Method, 27, ""}, + {"(RowsColumnScanner).Next", Method, 27, ""}, + {"(RowsColumnScanner).NextRow", Method, 27, ""}, + {"(RowsColumnScanner).ScanColumn", Method, 27, ""}, {"(RowsColumnTypeDatabaseTypeName).Close", Method, 8, ""}, {"(RowsColumnTypeDatabaseTypeName).ColumnTypeDatabaseTypeName", Method, 8, ""}, {"(RowsColumnTypeDatabaseTypeName).Columns", Method, 8, ""}, @@ -1815,12 +1870,14 @@ {"ResultNoRows", Var, 0, ""}, {"Rows", Type, 0, ""}, {"RowsAffected", Type, 0, ""}, + {"RowsColumnScanner", Type, 27, ""}, {"RowsColumnTypeDatabaseTypeName", Type, 8, ""}, {"RowsColumnTypeLength", Type, 8, ""}, {"RowsColumnTypeNullable", Type, 8, ""}, {"RowsColumnTypePrecisionScale", Type, 8, ""}, {"RowsColumnTypeScanType", Type, 8, ""}, {"RowsNextResultSet", Type, 8, ""}, + {"ScanContext", Type, 27, ""}, {"SessionResetter", Type, 10, ""}, {"Stmt", Type, 0, ""}, {"StmtExecContext", Type, 8, ""}, @@ -5038,24 +5095,32 @@ {"(*InvalidUnmarshalError).Error", Method, 0, ""}, {"(*MarshalerError).Error", Method, 0, ""}, {"(*MarshalerError).Unwrap", Method, 13, ""}, + {"(*Number).UnmarshalJSONFrom", Method, 27, ""}, {"(*RawMessage).MarshalJSON", Method, 0, ""}, {"(*RawMessage).UnmarshalJSON", Method, 0, ""}, {"(*SyntaxError).Error", Method, 0, ""}, {"(*UnmarshalFieldError).Error", Method, 0, ""}, {"(*UnmarshalTypeError).Error", Method, 0, ""}, + {"(*UnmarshalTypeError).Unwrap", Method, 27, ""}, {"(*UnsupportedTypeError).Error", Method, 0, ""}, {"(*UnsupportedValueError).Error", Method, 0, ""}, {"(Delim).String", Method, 5, ""}, {"(Marshaler).MarshalJSON", Method, 0, ""}, {"(Number).Float64", Method, 1, ""}, {"(Number).Int64", Method, 1, ""}, + {"(Number).MarshalJSONTo", Method, 27, ""}, {"(Number).String", Method, 1, ""}, {"(RawMessage).MarshalJSON", Method, 8, ""}, {"(Unmarshaler).UnmarshalJSON", Method, 0, ""}, + {"CallMethodsWithLegacySemantics", Func, 27, "func(v bool) Options"}, {"Compact", Func, 0, "func(dst *bytes.Buffer, src []byte) error"}, {"Decoder", Type, 0, ""}, + {"DefaultOptionsV1", Func, 27, "func() Options"}, {"Delim", Type, 5, ""}, {"Encoder", Type, 0, ""}, + {"FormatByteArrayAsArray", Func, 27, "func(v bool) Options"}, + {"FormatBytesWithLegacySemantics", Func, 27, "func(v bool) Options"}, + {"FormatDurationAsNano", Func, 27, "func(v bool) Options"}, {"HTMLEscape", Func, 0, "func(dst *bytes.Buffer, src []byte)"}, {"Indent", Func, 0, "func(dst *bytes.Buffer, src []byte, prefix string, indent string) error"}, {"InvalidUTF8Error", Type, 0, ""}, @@ -5068,19 +5133,29 @@ {"MarshalerError", Type, 0, ""}, {"MarshalerError.Err", Field, 0, ""}, {"MarshalerError.Type", Field, 0, ""}, + {"MatchCaseSensitiveDelimiter", Func, 27, "func(v bool) Options"}, + {"MergeWithLegacySemantics", Func, 27, "func(v bool) Options"}, {"NewDecoder", Func, 0, "func(r io.Reader) *Decoder"}, {"NewEncoder", Func, 0, "func(w io.Writer) *Encoder"}, {"Number", Type, 1, ""}, + {"OmitEmptyWithLegacySemantics", Func, 27, "func(v bool) Options"}, + {"Options", Type, 27, ""}, + {"ParseBytesWithLooseRFC4648", Func, 27, "func(v bool) Options"}, + {"ParseTimeWithLooseRFC3339", Func, 27, "func(v bool) Options"}, {"RawMessage", Type, 0, ""}, + {"ReportErrorsWithLegacySemantics", Func, 27, "func(v bool) Options"}, + {"StringifyWithLegacySemantics", Func, 27, "func(v bool) Options"}, {"SyntaxError", Type, 0, ""}, {"SyntaxError.Offset", Field, 0, ""}, {"Token", Type, 5, ""}, {"Unmarshal", Func, 0, "func(data []byte, v any) error"}, + {"UnmarshalArrayFromAnyLength", Func, 27, "func(v bool) Options"}, {"UnmarshalFieldError", Type, 0, ""}, {"UnmarshalFieldError.Field", Field, 0, ""}, {"UnmarshalFieldError.Key", Field, 0, ""}, {"UnmarshalFieldError.Type", Field, 0, ""}, {"UnmarshalTypeError", Type, 0, ""}, + {"UnmarshalTypeError.Err", Field, 27, ""}, {"UnmarshalTypeError.Field", Field, 8, ""}, {"UnmarshalTypeError.Offset", Field, 5, ""}, {"UnmarshalTypeError.Struct", Field, 8, ""}, @@ -5094,6 +5169,158 @@ {"UnsupportedValueError.Value", Field, 0, ""}, {"Valid", Func, 9, "func(data []byte) bool"}, }, + "encoding/json/jsontext": { + {"(*Decoder).InputOffset", Method, 27, ""}, + {"(*Decoder).Options", Method, 27, ""}, + {"(*Decoder).PeekKind", Method, 27, ""}, + {"(*Decoder).ReadToken", Method, 27, ""}, + {"(*Decoder).ReadValue", Method, 27, ""}, + {"(*Decoder).Reset", Method, 27, ""}, + {"(*Decoder).SkipValue", Method, 27, ""}, + {"(*Decoder).StackDepth", Method, 27, ""}, + {"(*Decoder).StackIndex", Method, 27, ""}, + {"(*Decoder).StackPointer", Method, 27, ""}, + {"(*Decoder).UnreadBuffer", Method, 27, ""}, + {"(*Encoder).AvailableBuffer", Method, 27, ""}, + {"(*Encoder).Options", Method, 27, ""}, + {"(*Encoder).OutputOffset", Method, 27, ""}, + {"(*Encoder).Reset", Method, 27, ""}, + {"(*Encoder).StackDepth", Method, 27, ""}, + {"(*Encoder).StackIndex", Method, 27, ""}, + {"(*Encoder).StackPointer", Method, 27, ""}, + {"(*Encoder).WriteToken", Method, 27, ""}, + {"(*Encoder).WriteValue", Method, 27, ""}, + {"(*SyntacticError).Error", Method, 27, ""}, + {"(*SyntacticError).Unwrap", Method, 27, ""}, + {"(*Value).Canonicalize", Method, 27, ""}, + {"(*Value).Compact", Method, 27, ""}, + {"(*Value).Format", Method, 27, ""}, + {"(*Value).Indent", Method, 27, ""}, + {"(*Value).UnmarshalJSON", Method, 27, ""}, + {"(Kind).String", Method, 27, ""}, + {"(Pointer).AppendToken", Method, 27, ""}, + {"(Pointer).Contains", Method, 27, ""}, + {"(Pointer).IsValid", Method, 27, ""}, + {"(Pointer).LastToken", Method, 27, ""}, + {"(Pointer).Parent", Method, 27, ""}, + {"(Pointer).Tokens", Method, 27, ""}, + {"(Token).Bool", Method, 27, ""}, + {"(Token).Clone", Method, 27, ""}, + {"(Token).Float", Method, 27, ""}, + {"(Token).Float32", Method, 27, ""}, + {"(Token).Int", Method, 27, ""}, + {"(Token).Kind", Method, 27, ""}, + {"(Token).String", Method, 27, ""}, + {"(Token).Uint", Method, 27, ""}, + {"(Value).Clone", Method, 27, ""}, + {"(Value).IsValid", Method, 27, ""}, + {"(Value).Kind", Method, 27, ""}, + {"(Value).MarshalJSON", Method, 27, ""}, + {"(Value).String", Method, 27, ""}, + {"AllowDuplicateNames", Func, 27, "func(v bool) Options"}, + {"AllowInvalidUTF8", Func, 27, "func(v bool) Options"}, + {"AppendFloat", Func, 27, "func(dst []byte, src float64, bits int) []byte"}, + {"AppendFormat", Func, 27, "func[Bytes ~[]byte | ~string](dst []byte, src Bytes, opts ...Options) ([]byte, error)"}, + {"AppendQuote", Func, 27, "func[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error)"}, + {"AppendUnquote", Func, 27, "func[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error)"}, + {"BeginArray", Var, 27, ""}, + {"BeginObject", Var, 27, ""}, + {"Bool", Func, 27, "func(b bool) Token"}, + {"CanonicalizeRawFloats", Func, 27, "func(v bool) Options"}, + {"CanonicalizeRawInts", Func, 27, "func(v bool) Options"}, + {"Decoder", Type, 27, ""}, + {"Encoder", Type, 27, ""}, + {"EndArray", Var, 27, ""}, + {"EndObject", Var, 27, ""}, + {"ErrDuplicateName", Var, 27, ""}, + {"ErrNonStringName", Var, 27, ""}, + {"EscapeForHTML", Func, 27, "func(v bool) Options"}, + {"EscapeForJS", Func, 27, "func(v bool) Options"}, + {"False", Var, 27, ""}, + {"Float", Func, 27, "func(n float64) Token"}, + {"Float32", Func, 27, "func(n float32) Token"}, + {"Int", Func, 27, "func(n int64) Token"}, + {"Internal", Var, 27, ""}, + {"Kind", Type, 27, ""}, + {"KindBeginArray", Const, 27, ""}, + {"KindBeginObject", Const, 27, ""}, + {"KindEndArray", Const, 27, ""}, + {"KindEndObject", Const, 27, ""}, + {"KindFalse", Const, 27, ""}, + {"KindInvalid", Const, 27, ""}, + {"KindNull", Const, 27, ""}, + {"KindNumber", Const, 27, ""}, + {"KindString", Const, 27, ""}, + {"KindTrue", Const, 27, ""}, + {"Multiline", Func, 27, "func(v bool) Options"}, + {"NewDecoder", Func, 27, "func(r io.Reader, opts ...Options) *Decoder"}, + {"NewEncoder", Func, 27, "func(w io.Writer, opts ...Options) *Encoder"}, + {"Null", Var, 27, ""}, + {"Options", Type, 27, ""}, + {"Pointer", Type, 27, ""}, + {"PreserveRawStrings", Func, 27, "func(v bool) Options"}, + {"ReorderRawObjects", Func, 27, "func(v bool) Options"}, + {"SpaceAfterColon", Func, 27, "func(v bool) Options"}, + {"SpaceAfterComma", Func, 27, "func(v bool) Options"}, + {"String", Func, 27, "func(s string) Token"}, + {"SyntacticError", Type, 27, ""}, + {"SyntacticError.ByteOffset", Field, 27, ""}, + {"SyntacticError.Err", Field, 27, ""}, + {"SyntacticError.JSONPointer", Field, 27, ""}, + {"Token", Type, 27, ""}, + {"True", Var, 27, ""}, + {"Uint", Func, 27, "func(n uint64) Token"}, + {"Value", Type, 27, ""}, + {"WithIndent", Func, 27, "func(indent string) Options"}, + {"WithIndentPrefix", Func, 27, "func(prefix string) Options"}, + }, + "encoding/json/v2": { + {"(*SemanticError).Error", Method, 27, ""}, + {"(*SemanticError).Unwrap", Method, 27, ""}, + {"(Marshaler).MarshalJSON", Method, 27, ""}, + {"(MarshalerTo).MarshalJSONTo", Method, 27, ""}, + {"(Unmarshaler).UnmarshalJSON", Method, 27, ""}, + {"(UnmarshalerFrom).UnmarshalJSONFrom", Method, 27, ""}, + {"DefaultOptionsV2", Func, 27, "func() Options"}, + {"Deterministic", Func, 27, "func(v bool) Options"}, + {"ErrUnknownName", Var, 27, ""}, + {"FormatNilMapAsNull", Func, 27, "func(v bool) Options"}, + {"FormatNilSliceAsNull", Func, 27, "func(v bool) Options"}, + {"GetOption", Func, 27, "func[T any](opts Options, setter func(T) Options) (T, bool)"}, + {"JoinMarshalers", Func, 27, "func(ms ...*Marshalers) *Marshalers"}, + {"JoinOptions", Func, 27, "func(srcs ...Options) Options"}, + {"JoinUnmarshalers", Func, 27, "func(us ...*Unmarshalers) *Unmarshalers"}, + {"Marshal", Func, 27, "func(in any, opts ...Options) (out []byte, err error)"}, + {"MarshalEncode", Func, 27, "func(out *jsontext.Encoder, in any, opts ...Options) (err error)"}, + {"MarshalFunc", Func, 27, "func[T any](fn func(T) ([]byte, error)) *Marshalers"}, + {"MarshalToFunc", Func, 27, "func[T any](fn func(*jsontext.Encoder, T) error) *Marshalers"}, + {"MarshalWrite", Func, 27, "func(out io.Writer, in any, opts ...Options) (err error)"}, + {"Marshaler", Type, 27, ""}, + {"MarshalerTo", Type, 27, ""}, + {"Marshalers", Type, 27, ""}, + {"MatchCaseInsensitiveNames", Func, 27, "func(v bool) Options"}, + {"OmitZeroStructFields", Func, 27, "func(v bool) Options"}, + {"Options", Type, 27, ""}, + {"RejectUnknownMembers", Func, 27, "func(v bool) Options"}, + {"SemanticError", Type, 27, ""}, + {"SemanticError.ByteOffset", Field, 27, ""}, + {"SemanticError.Err", Field, 27, ""}, + {"SemanticError.GoType", Field, 27, ""}, + {"SemanticError.JSONKind", Field, 27, ""}, + {"SemanticError.JSONPointer", Field, 27, ""}, + {"SemanticError.JSONValue", Field, 27, ""}, + {"StringifyNumbers", Func, 27, "func(v bool) Options"}, + {"Unmarshal", Func, 27, "func(in []byte, out any, opts ...Options) (err error)"}, + {"UnmarshalDecode", Func, 27, "func(in *jsontext.Decoder, out any, opts ...Options) (err error)"}, + {"UnmarshalFromFunc", Func, 27, "func[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers"}, + {"UnmarshalFunc", Func, 27, "func[T any](fn func([]byte, T) error) *Unmarshalers"}, + {"UnmarshalRead", Func, 27, "func(in io.Reader, out any, opts ...Options) (err error)"}, + {"Unmarshaler", Type, 27, ""}, + {"UnmarshalerFrom", Type, 27, ""}, + {"Unmarshalers", Type, 27, ""}, + {"WithMarshalers", Func, 27, "func(v *Marshalers) Options"}, + {"WithUnmarshalers", Func, 27, "func(v *Unmarshalers) Options"}, + }, "encoding/pem": { {"Block", Type, 0, ""}, {"Block.Bytes", Field, 0, ""}, @@ -6002,6 +6229,7 @@ {"Shift", Func, 5, "func(x Value, op token.Token, s uint) Value"}, {"Sign", Func, 5, "func(x Value) int"}, {"String", Const, 5, ""}, + {"StringLen", Func, 27, "func(x Value) int64"}, {"StringVal", Func, 5, "func(x Value) string"}, {"ToComplex", Func, 6, "func(x Value) Value"}, {"ToFloat", Func, 6, "func(x Value) Value"}, @@ -6183,6 +6411,7 @@ {"(*ErrorList).Add", Method, 0, ""}, {"(*ErrorList).RemoveMultiples", Method, 0, ""}, {"(*ErrorList).Reset", Method, 0, ""}, + {"(*Scanner).End", Method, 27, ""}, {"(*Scanner).Init", Method, 0, ""}, {"(*Scanner).Scan", Method, 0, ""}, {"(Error).Error", Method, 0, ""}, @@ -6222,6 +6451,7 @@ {"(*File).SetLines", Method, 0, ""}, {"(*File).SetLinesForContent", Method, 0, ""}, {"(*File).Size", Method, 0, ""}, + {"(*File).String", Method, 27, ""}, {"(*FileSet).AddExistingFiles", Method, 25, ""}, {"(*FileSet).AddFile", Method, 0, ""}, {"(*FileSet).Base", Method, 0, ""}, @@ -6529,6 +6759,7 @@ {"(*Tuple).Variables", Method, 24, ""}, {"(*TypeList).At", Method, 18, ""}, {"(*TypeList).Len", Method, 18, ""}, + {"(*TypeList).String", Method, 27, ""}, {"(*TypeList).Types", Method, 24, ""}, {"(*TypeName).Exported", Method, 5, ""}, {"(*TypeName).Id", Method, 5, ""}, @@ -6547,6 +6778,7 @@ {"(*TypeParam).Underlying", Method, 18, ""}, {"(*TypeParamList).At", Method, 18, ""}, {"(*TypeParamList).Len", Method, 18, ""}, + {"(*TypeParamList).String", Method, 27, ""}, {"(*TypeParamList).TypeParams", Method, 24, ""}, {"(*Union).Len", Method, 18, ""}, {"(*Union).String", Method, 18, ""}, @@ -6571,9 +6803,14 @@ {"(Checker).PkgNameOf", Method, 22, ""}, {"(Checker).TypeOf", Method, 5, ""}, {"(Error).Error", Method, 5, ""}, + {"(Hasher).Equal", Method, 27, ""}, + {"(Hasher).Hash", Method, 27, ""}, + {"(HasherIgnoreTags).Equal", Method, 27, ""}, + {"(HasherIgnoreTags).Hash", Method, 27, ""}, {"(Importer).Import", Method, 5, ""}, {"(ImporterFrom).Import", Method, 6, ""}, {"(ImporterFrom).ImportFrom", Method, 6, ""}, + {"(Instance).String", Method, 27, ""}, {"(Object).Exported", Method, 5, ""}, {"(Object).Id", Method, 5, ""}, {"(Object).Name", Method, 5, ""}, @@ -6643,6 +6880,8 @@ {"Float32", Const, 5, ""}, {"Float64", Const, 5, ""}, {"Func", Type, 5, ""}, + {"Hasher", Type, 27, ""}, + {"HasherIgnoreTags", Type, 27, ""}, {"Id", Func, 5, "func(pkg *Package, name string) string"}, {"Identical", Func, 5, "func(x Type, y Type) bool"}, {"IdenticalIgnoreTags", Func, 8, "func(x Type, y Type) bool"}, @@ -6877,9 +7116,13 @@ {"(*Hash).Write", Method, 14, ""}, {"(*Hash).WriteByte", Method, 14, ""}, {"(*Hash).WriteString", Method, 14, ""}, + {"(ComparableHasher).Equal", Method, 27, ""}, + {"(ComparableHasher).Hash", Method, 27, ""}, {"Bytes", Func, 19, "func(seed Seed, b []byte) uint64"}, {"Comparable", Func, 24, "func[T comparable](seed Seed, v T) uint64"}, + {"ComparableHasher", Type, 27, ""}, {"Hash", Type, 14, ""}, + {"Hasher", Type, 27, ""}, {"MakeSeed", Func, 14, "func() Seed"}, {"Seed", Type, 14, ""}, {"String", Func, 19, "func(seed Seed, s string) uint64"}, @@ -8035,6 +8278,7 @@ {"(*Int).CmpAbs", Method, 10, ""}, {"(*Int).Div", Method, 0, ""}, {"(*Int).DivMod", Method, 0, ""}, + {"(*Int).Divide", Method, 27, ""}, {"(*Int).Exp", Method, 0, ""}, {"(*Int).FillBytes", Method, 15, ""}, {"(*Int).Float64", Method, 21, ""}, @@ -8119,9 +8363,11 @@ {"Accuracy", Type, 5, ""}, {"AwayFromZero", Const, 5, ""}, {"Below", Const, 5, ""}, + {"Ceil", Const, 27, ""}, {"ErrNaN", Type, 5, ""}, {"Exact", Const, 5, ""}, {"Float", Type, 5, ""}, + {"Floor", Const, 27, ""}, {"Int", Type, 0, ""}, {"Jacobi", Func, 5, "func(x *Int, y *Int) int"}, {"MaxBase", Const, 0, ""}, @@ -8133,12 +8379,14 @@ {"NewRat", Func, 0, "func(a int64, b int64) *Rat"}, {"ParseFloat", Func, 5, "func(s string, base int, prec uint, mode RoundingMode) (f *Float, b int, err error)"}, {"Rat", Type, 0, ""}, + {"Round", Const, 27, ""}, {"RoundingMode", Type, 5, ""}, {"ToNearestAway", Const, 5, ""}, {"ToNearestEven", Const, 5, ""}, {"ToNegativeInf", Const, 5, ""}, {"ToPositiveInf", Const, 5, ""}, {"ToZero", Const, 5, ""}, + {"Trunc", Const, 27, ""}, {"Word", Type, 0, ""}, }, "math/bits": { @@ -8290,6 +8538,7 @@ {"(*Rand).Int64", Method, 22, ""}, {"(*Rand).Int64N", Method, 22, ""}, {"(*Rand).IntN", Method, 22, ""}, + {"(*Rand).N", Method, 27, ""}, {"(*Rand).NormFloat64", Method, 22, ""}, {"(*Rand).Perm", Method, 22, ""}, {"(*Rand).Shuffle", Method, 22, ""}, @@ -8985,7 +9234,7 @@ {"NoBody", Var, 8, ""}, {"NotFound", Func, 0, "func(w ResponseWriter, r *Request)"}, {"NotFoundHandler", Func, 0, "func() Handler"}, - {"ParseCookie", Func, 23, "func(line string) ([]*Cookie, error)"}, + {"ParseCookie", Func, 23, "func(line string) (#rv1 []*Cookie, #rv2 error)"}, {"ParseHTTPVersion", Func, 0, "func(vers string) (major int, minor int, ok bool)"}, {"ParseSetCookie", Func, 23, "func(line string) (*Cookie, error)"}, {"ParseTime", Func, 1, "func(text string) (t time.Time, err error)"}, @@ -9061,6 +9310,7 @@ {"Server.BaseContext", Field, 13, ""}, {"Server.ConnContext", Field, 13, ""}, {"Server.ConnState", Field, 3, ""}, + {"Server.DisableClientPriority", Field, 27, ""}, {"Server.DisableGeneralOptionsHandler", Field, 20, ""}, {"Server.ErrorLog", Field, 3, ""}, {"Server.HTTP2", Field, 24, ""}, @@ -9226,6 +9476,7 @@ {"NewRequestWithContext", Func, 23, "func(ctx context.Context, method string, target string, body io.Reader) *http.Request"}, {"NewServer", Func, 0, "func(handler http.Handler) *Server"}, {"NewTLSServer", Func, 0, "func(handler http.Handler) *Server"}, + {"NewTestServer", Func, 27, "func(t testing.TB, handler http.Handler) *Server"}, {"NewUnstartedServer", Func, 0, "func(handler http.Handler) *Server"}, {"ResponseRecorder", Type, 0, ""}, {"ResponseRecorder.Body", Field, 0, ""}, @@ -9596,6 +9847,7 @@ {"(*Error).Timeout", Method, 6, ""}, {"(*Error).Unwrap", Method, 13, ""}, {"(*URL).AppendBinary", Method, 24, ""}, + {"(*URL).Clone", Method, 27, ""}, {"(*URL).EscapedFragment", Method, 15, ""}, {"(*URL).EscapedPath", Method, 5, ""}, {"(*URL).Hostname", Method, 8, ""}, @@ -9616,6 +9868,7 @@ {"(EscapeError).Error", Method, 0, ""}, {"(InvalidHostError).Error", Method, 6, ""}, {"(Values).Add", Method, 0, ""}, + {"(Values).Clone", Method, 27, ""}, {"(Values).Del", Method, 0, ""}, {"(Values).Encode", Method, 0, ""}, {"(Values).Get", Method, 0, ""}, @@ -10793,6 +11046,7 @@ {"ContainsRune", Func, 0, "func(s string, r rune) bool"}, {"Count", Func, 0, "func(s string, substr string) int"}, {"Cut", Func, 18, "func(s string, sep string) (before string, after string, found bool)"}, + {"CutLast", Func, 27, "func(s string, sep string) (before string, after string, found bool)"}, {"CutPrefix", Func, 20, "func(s string, prefix string) (after string, found bool)"}, {"CutSuffix", Func, 20, "func(s string, suffix string) (before string, found bool)"}, {"EqualFold", Func, 0, "func(s string, t string) bool"}, @@ -17476,6 +17730,7 @@ {"TestHandler", Func, 21, "func(h slog.Handler, results func() []map[string]any) error"}, }, "testing/synctest": { + {"Sleep", Func, 27, "func(d time.Duration)"}, {"Test", Func, 25, "func(t *testing.T, f func(*testing.T))"}, {"Wait", Func, 25, "func()"}, }, @@ -17980,6 +18235,7 @@ {"Bassa_Vah", Var, 4, ""}, {"Batak", Var, 0, ""}, {"Bengali", Var, 0, ""}, + {"Beria_Erfe", Var, 27, ""}, {"Bhaiksuki", Var, 7, ""}, {"Bidi_Control", Var, 0, ""}, {"Bopomofo", Var, 0, ""}, @@ -18029,6 +18285,7 @@ {"Extender", Var, 0, ""}, {"FoldCategory", Var, 0, ""}, {"FoldScript", Var, 0, ""}, + {"Garay", Var, 27, ""}, {"Georgian", Var, 0, ""}, {"Glagolitic", Var, 0, ""}, {"Gothic", Var, 0, ""}, @@ -18038,6 +18295,7 @@ {"Gujarati", Var, 0, ""}, {"Gunjala_Gondi", Var, 13, ""}, {"Gurmukhi", Var, 0, ""}, + {"Gurung_Khema", Var, 27, ""}, {"Han", Var, 0, ""}, {"Hangul", Var, 0, ""}, {"Hanifi_Rohingya", Var, 13, ""}, @@ -18049,6 +18307,9 @@ {"Hyphen", Var, 0, ""}, {"IDS_Binary_Operator", Var, 0, ""}, {"IDS_Trinary_Operator", Var, 0, ""}, + {"IDS_Unary_Operator", Var, 27, ""}, + {"ID_Compat_Math_Continue", Var, 27, ""}, + {"ID_Compat_Math_Start", Var, 27, ""}, {"Ideographic", Var, 0, ""}, {"Imperial_Aramaic", Var, 0, ""}, {"In", Func, 2, "func(r rune, ranges ...*RangeTable) bool"}, @@ -18082,6 +18343,7 @@ {"Khmer", Var, 0, ""}, {"Khojki", Var, 4, ""}, {"Khudawadi", Var, 4, ""}, + {"Kirat_Rai", Var, 27, ""}, {"L", Var, 0, ""}, {"LC", Var, 25, ""}, {"Lao", Var, 0, ""}, @@ -18125,6 +18387,7 @@ {"Miao", Var, 1, ""}, {"Mn", Var, 0, ""}, {"Modi", Var, 4, ""}, + {"Modifier_Combining_Mark", Var, 27, ""}, {"Mongolian", Var, 0, ""}, {"Mro", Var, 4, ""}, {"Multani", Var, 5, ""}, @@ -18145,6 +18408,7 @@ {"Nyiakeng_Puachue_Hmong", Var, 14, ""}, {"Ogham", Var, 0, ""}, {"Ol_Chiki", Var, 0, ""}, + {"Ol_Onal", Var, 27, ""}, {"Old_Hungarian", Var, 5, ""}, {"Old_Italic", Var, 0, ""}, {"Old_North_Arabian", Var, 4, ""}, @@ -18214,6 +18478,7 @@ {"Sharada", Var, 1, ""}, {"Shavian", Var, 0, ""}, {"Siddham", Var, 4, ""}, + {"Sidetic", Var, 27, ""}, {"SignWriting", Var, 5, ""}, {"SimpleFold", Func, 0, "func(r rune) rune"}, {"Sinhala", Var, 0, ""}, @@ -18227,6 +18492,7 @@ {"Space", Var, 0, ""}, {"SpecialCase", Type, 0, ""}, {"Sundanese", Var, 0, ""}, + {"Sunuwar", Var, 27, ""}, {"Syloti_Nagri", Var, 0, ""}, {"Symbol", Var, 0, ""}, {"Syriac", Var, 0, ""}, @@ -18235,6 +18501,7 @@ {"Tai_Le", Var, 0, ""}, {"Tai_Tham", Var, 0, ""}, {"Tai_Viet", Var, 0, ""}, + {"Tai_Yo", Var, 27, ""}, {"Takri", Var, 1, ""}, {"Tamil", Var, 0, ""}, {"Tangsa", Var, 21, ""}, @@ -18252,7 +18519,10 @@ {"ToLower", Func, 0, "func(r rune) rune"}, {"ToTitle", Func, 0, "func(r rune) rune"}, {"ToUpper", Func, 0, "func(r rune) rune"}, + {"Todhri", Var, 27, ""}, + {"Tolong_Siki", Var, 27, ""}, {"Toto", Var, 21, ""}, + {"Tulu_Tigalari", Var, 27, ""}, {"TurkishCase", Var, 0, ""}, {"Ugaritic", Var, 0, ""}, {"Unified_Ideograph", Var, 0, ""}, @@ -18320,6 +18590,21 @@ {"String", Func, 0, ""}, {"StringData", Func, 0, ""}, }, + "uuid": { + {"(*UUID).UnmarshalText", Method, 27, ""}, + {"(UUID).AppendText", Method, 27, ""}, + {"(UUID).Compare", Method, 27, ""}, + {"(UUID).MarshalText", Method, 27, ""}, + {"(UUID).String", Method, 27, ""}, + {"Max", Func, 27, "func() UUID"}, + {"MustParse", Func, 27, "func(s string) UUID"}, + {"New", Func, 27, "func() UUID"}, + {"NewV4", Func, 27, "func() UUID"}, + {"NewV7", Func, 27, "func() UUID"}, + {"Nil", Func, 27, "func() UUID"}, + {"Parse", Func, 27, "func(s string) (UUID, error)"}, + {"UUID", Type, 27, ""}, + }, "weak": { {"(Pointer).Value", Method, 24, ""}, {"Make", Func, 24, "func[T any](ptr *T) Pointer[T]"},
diff --git a/internal/stdlib/testdata/nethttp.deps b/internal/stdlib/testdata/nethttp.deps index 31edb38..f47774d 100644 --- a/internal/stdlib/testdata/nethttp.deps +++ b/internal/stdlib/testdata/nethttp.deps
@@ -50,8 +50,6 @@ internal/fmtsort internal/oserror path -internal/bisect -internal/godebug syscall time io/fs @@ -62,7 +60,6 @@ internal/testlog os fmt -sort compress/flate encoding/binary hash @@ -71,6 +68,8 @@ container/list context crypto +internal/bisect +internal/godebug crypto/internal/fips140deps/godebug crypto/internal/fips140 crypto/internal/fips140/alias @@ -96,19 +95,18 @@ crypto/cipher crypto/internal/boring/sig crypto/internal/boring -math/rand/v2 -crypto/internal/randutil -crypto/internal/rand -math/rand -math/big -crypto/rand crypto/aes crypto/des crypto/internal/fips140/nistec/fiat crypto/internal/fips140/nistec crypto/internal/fips140/ecdh crypto/internal/fips140/edwards25519/field +math/rand/v2 +crypto/internal/randutil +crypto/internal/rand crypto/ecdh +math/rand +math/big crypto/elliptic crypto/internal/boring/bbig crypto/internal/fips140/bigmod @@ -126,6 +124,7 @@ crypto/ecdsa crypto/internal/fips140/edwards25519 crypto/internal/fips140/ed25519 +crypto/rand crypto/ed25519 crypto/internal/fips140/hkdf crypto/hkdf @@ -142,6 +141,8 @@ crypto/internal/fips140/tls12 crypto/internal/fips140/tls13 crypto/md5 +crypto/internal/fips140/mldsa +crypto/mldsa crypto/rc4 crypto/internal/fips140/rsa crypto/rsa @@ -162,6 +163,7 @@ net/url path/filepath crypto/x509 +sort crypto/tls vendor/golang.org/x/text/transform log/internal @@ -173,12 +175,14 @@ net/textproto vendor/golang.org/x/net/http/httpguts vendor/golang.org/x/net/http/httpproxy -vendor/golang.org/x/net/http2/hpack mime mime/quotedprintable mime/multipart net/http/httptrace net/http/internal net/http/internal/ascii +vendor/golang.org/x/net/http2/hpack net/http/internal/httpcommon +net/http/internal/httpsfv +net/http/internal/http2 net/http
diff --git a/internal/stdlib/testdata/nethttp.imports b/internal/stdlib/testdata/nethttp.imports index 9f2860f..d920811 100644 --- a/internal/stdlib/testdata/nethttp.imports +++ b/internal/stdlib/testdata/nethttp.imports
@@ -4,15 +4,12 @@ compress/gzip container/list context -crypto/rand crypto/tls encoding/base64 -encoding/binary errors fmt vendor/golang.org/x/net/http/httpguts vendor/golang.org/x/net/http/httpproxy -vendor/golang.org/x/net/http2/hpack vendor/golang.org/x/net/idna internal/godebug io @@ -20,15 +17,14 @@ log maps math -math/bits -math/rand +math/rand/v2 mime mime/multipart net net/http/httptrace net/http/internal net/http/internal/ascii -net/http/internal/httpcommon +net/http/internal/http2 net/textproto net/url os
diff --git a/internal/typesinternal/element.go b/internal/typesinternal/element.go index 5fe4d8a..89eeea1 100644 --- a/internal/typesinternal/element.go +++ b/internal/typesinternal/element.go
@@ -37,6 +37,10 @@ tmset := msets.MethodSet(T) for method := range tmset.Methods() { sig := method.Type().(*types.Signature) + if sig.TypeParams() != nil { + continue // skip type-parameterized methods + } + // It is tempting to call visit(sig, false) // but, as noted in golang.org/cl/65450043, // the Signature.Recv field is ignored by @@ -123,10 +127,10 @@ case *types.TypeParam, *types.Union: // forEachReachable must not be called on parameterized types. - panic(T) + panic(fmt.Sprintf("ForEachElement called on type containing %T", T)) default: - panic(T) + panic(fmt.Sprintf("ForEachElement called on unexpected type %T", T)) } } visit(T, false)
diff --git a/internal/typesinternal/element_test.go b/internal/typesinternal/element_test.go index 7d360db..7e70d0b 100644 --- a/internal/typesinternal/element_test.go +++ b/internal/typesinternal/element_test.go
@@ -15,6 +15,7 @@ "testing" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/typesinternal" ) @@ -45,6 +46,23 @@ ` func TestForEachElement(t *testing.T) { + // Add generic method, if go1.27. + // It doesn't change the outcome: + // complex64 is not expected in the result. + elementSrc := elementSrc + if testenv.Go1Point() >= 27 { + elementSrc += ` +// generic method: T.generic[U] is not explored. +func (T) generic[U any](complex64) {} + +// method of generic type +type H[T any] uintptr +func (H[T]) m(T) uint16 +type Hf64 = H[float64] +` + // (still available: uint8 uint64 float32) + } + fset := token.NewFileSet() f, err := parser.ParseFile(fset, "a.go", elementSrc, 0) if err != nil { @@ -56,10 +74,11 @@ t.Fatal(err) // type error } - tests := []struct { + type testcase struct { name string // name of a type alias whose RHS type's elements to compute want []string // strings of types that are/are not elements (! => not) - }{ + } + tests := []testcase{ // simple type {"A", []string{"int"}}, @@ -80,7 +99,7 @@ // the result does not include the struct type itself. // (This follows the Go toolchain behavior, and finesses the need // to create wrapper methods for that struct type.) - {"C", []string{"T", "*T", "int", "uint", "complex128", "!struct{x int}"}}, + {"C", []string{"T", "*T", "int", "uint", "complex128", "!complex64", "!struct{x int}"}}, // alias type {"D", []string{"int"}}, @@ -96,6 +115,12 @@ // struct with embedded field that has methods {"G", []string{"*U", "struct{U}", "uint32", "U"}}, } + if testenv.Go1Point() >= 27 { + tests = append(tests, []testcase{ + // H[float64].m is a ground type, so it is visited, giving us uint16. + {"Hf64", []string{"*H[float64]", "H[float64]", "float64", "uint16"}}, + }...) + } var msets typeutil.MethodSetCache for _, test := range tests { tname, ok := pkg.Scope().Lookup(test.name).(*types.TypeName)
diff --git a/internal/typesinternal/types.go b/internal/typesinternal/types.go index 6582cc8..d2c0b4c 100644 --- a/internal/typesinternal/types.go +++ b/internal/typesinternal/types.go
@@ -22,6 +22,7 @@ "go/ast" "go/token" "go/types" + "iter" "reflect" "golang.org/x/tools/go/ast/inspector" @@ -242,3 +243,30 @@ } return "unknown symbol" } + +// ImplicitFieldSelections returns the sequence of implicit embedded fields +// traversed by the given selection. It skips the final leaf field or method. +// The boolean component indicates whether the traversal traversed a pointer. +func ImplicitFieldSelections(seln types.Selection) iter.Seq2[*types.Var, bool] { + return func(yield func(*types.Var, bool) bool) { + var ( + t = seln.Recv() + indices = seln.Index() + ) + for _, idx := range indices[:len(indices)-1] { + ptr, isPtr := t.Underlying().(*types.Pointer) + if isPtr { + t = ptr.Elem() + } + structType, ok := t.Underlying().(*types.Struct) + if !ok { + break + } + field := structType.Field(idx) + if !yield(field, isPtr) { + break + } + t = field.Type() + } + } +}
diff --git a/internal/typesinternal/zerovalue.go b/internal/typesinternal/zerovalue.go index d612a71..706ad33 100644 --- a/internal/typesinternal/zerovalue.go +++ b/internal/typesinternal/zerovalue.go
@@ -259,13 +259,13 @@ case *types.Signature: var params []*ast.Field for v := range t.Params().Variables() { + var names []*ast.Ident + if v.Name() != "" { + names = []*ast.Ident{ast.NewIdent(v.Name())} + } params = append(params, &ast.Field{ - Type: TypeExpr(v.Type(), qual), - Names: []*ast.Ident{ - { - Name: v.Name(), - }, - }, + Type: TypeExpr(v.Type(), qual), + Names: names, }) } if t.Variadic() { @@ -328,10 +328,10 @@ return expr case *types.Struct: - return ast.NewIdent(t.String()) + return ast.NewIdent(types.TypeString(t, qual)) case *types.Interface: - return ast.NewIdent(t.String()) + return ast.NewIdent(types.TypeString(t, qual)) case *types.Union: if t.Len() == 0 {
diff --git a/internal/xcontext/xcontext.go b/internal/xcontext/xcontext.go deleted file mode 100644 index 641dfe5..0000000 --- a/internal/xcontext/xcontext.go +++ /dev/null
@@ -1,23 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package xcontext is a package to offer the extra functionality we need -// from contexts that is not available from the standard context package. -package xcontext - -import ( - "context" - "time" -) - -// Detach returns a context that keeps all the values of its parent context -// but detaches from the cancellation and error handling. -func Detach(ctx context.Context) context.Context { return detachedContext{ctx} } - -type detachedContext struct{ parent context.Context } - -func (v detachedContext) Deadline() (time.Time, bool) { return time.Time{}, false } -func (v detachedContext) Done() <-chan struct{} { return nil } -func (v detachedContext) Err() error { return nil } -func (v detachedContext) Value(key any) any { return v.parent.Value(key) }
diff --git a/present/style_test.go b/present/style_test.go index cef5a62..80648bf 100644 --- a/present/style_test.go +++ b/present/style_test.go
@@ -62,7 +62,6 @@ {"(_a_)", "(<i>a</i>)"}, {"((_a_), _b_, _c_).", "((<i>a</i>), <i>b</i>, <i>c</i>)."}, {"(_a)", "(_a)"}, - {"(_a)", "(_a)"}, {"_Why_use_scoped__ptr_? Use plain ***ptr* instead.", "<i>Why use scoped_ptr</i>? Use plain <b>*ptr</b> instead."}, {"_hey_ [[http://golang.org][*Gophers*]] *around*", `<i>hey</i> <a href="http://golang.org" target="_blank"><b>Gophers</b></a> <b>around</b>`},
diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index 3d21ce6..720ecc1 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go
@@ -8,8 +8,13 @@ // interface, and this fact is necessary for the package to be // well-typed. // -// It requires well-typed inputs. -package satisfy // import "golang.org/x/tools/refactor/satisfy" +// 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 +// already done by the type checker, which would make this package +// redundant. +package satisfy // NOTES: // @@ -40,6 +45,7 @@ "go/ast" "go/token" "go/types" + "iter" "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/internal/typeparams" @@ -127,13 +133,16 @@ case *ast.CallExpr: // x, err := f(args) - sig := typeparams.CoreType(f.expr(e.Fun)).(*types.Signature) - f.call(sig, e.Args) + if sig := hasUnderlyingTermOf[*types.Signature](f.expr(e.Fun)); sig != nil { + f.call(sig, e.Args) + } case *ast.IndexExpr: // y, ok := x[i] x := f.expr(e.X) - f.assign(f.expr(e.Index), typeparams.CoreType(x).(*types.Map).Key()) + if m := hasUnderlyingTermOf[*types.Map](x); m != nil { + f.assign(f.expr(e.Index), m.Key()) + } case *ast.TypeAssertExpr: // y, ok := x.(T) @@ -216,16 +225,19 @@ f.expr(args[1]) } else { // append(x, y, z) - tElem := typeparams.CoreType(s).(*types.Slice).Elem() - for _, arg := range args[1:] { - f.assign(tElem, f.expr(arg)) + if s := hasUnderlyingTermOf[*types.Slice](s); s != nil { + for _, arg := range args[1:] { + f.assign(s.Elem(), f.expr(arg)) + } } } case "delete": m := f.expr(args[0]) k := f.expr(args[1]) - f.assign(typeparams.CoreType(m).(*types.Map).Key(), k) + if m := hasUnderlyingTermOf[*types.Map](m); m != nil { + f.assign(m.Key(), k) + } default: // ordinary call @@ -357,38 +369,37 @@ f.sig = saved case *ast.CompositeLit: - switch T := typeparams.CoreType(typeparams.Deref(tv.Type)).(type) { - 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)) - } else { - f.assign(T.Field(i).Type(), f.expr(elem)) + for term := range terms(typeparams.Deref(tv.Type)) { + switch T := term.Underlying().(type) { + 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)) + } else { + f.assign(T.Field(i).Type(), f.expr(elem)) + } } - } - case *types.Map: - for _, elem := range e.Elts { - elem := elem.(*ast.KeyValueExpr) - f.assign(T.Key(), f.expr(elem.Key)) - f.assign(T.Elem(), f.expr(elem.Value)) - } - - case *types.Array, *types.Slice: - tElem := T.(interface { - Elem() types.Type - }).Elem() - for _, elem := range e.Elts { - if kv, ok := elem.(*ast.KeyValueExpr); ok { - // ignore the key - f.assign(tElem, f.expr(kv.Value)) - } else { - f.assign(tElem, f.expr(elem)) + case *types.Map: + for _, elem := range e.Elts { + elem := elem.(*ast.KeyValueExpr) + f.assign(T.Key(), f.expr(elem.Key)) + f.assign(T.Elem(), f.expr(elem.Value)) } - } - default: - panic(fmt.Sprintf("unexpected composite literal type %T: %v", tv.Type, tv.Type.String())) + case *types.Array, *types.Slice: + tElem := T.(interface{ Elem() types.Type }).Elem() + for _, elem := range e.Elts { + if kv, ok := elem.(*ast.KeyValueExpr); ok { + // ignore the key + f.assign(tElem, f.expr(kv.Value)) + } else { + f.assign(tElem, f.expr(elem)) + } + } + default: + panic(fmt.Sprintf("unexpected composite literal type %T: %v", tv.Type, tv.Type.String())) + } } case *ast.ParenExpr: @@ -411,8 +422,8 @@ // x[i] or m[k] -- index or lookup operation x := f.expr(e.X) i := f.expr(e.Index) - if ux, ok := typeparams.CoreType(x).(*types.Map); ok { - f.assign(ux.Key(), i) + if m := hasUnderlyingTermOf[*types.Map](x); m != nil { + f.assign(m.Key(), i) } } @@ -464,7 +475,9 @@ } // ordinary call - f.call(typeparams.CoreType(f.expr(e.Fun)).(*types.Signature), e.Args) + if sig := hasUnderlyingTermOf[*types.Signature](f.expr(e.Fun)); sig != nil { + f.call(sig, e.Args) + } } case *ast.StarExpr: @@ -524,7 +537,9 @@ case *ast.SendStmt: ch := f.expr(s.Chan) val := f.expr(s.Value) - f.assign(typeparams.CoreType(ch).(*types.Chan).Elem(), val) + if c := hasUnderlyingTermOf[*types.Chan](ch); c != nil { + f.assign(c.Elem(), val) + } case *ast.IncDecStmt: f.expr(s.X) @@ -671,36 +686,36 @@ if s.Tok == token.ASSIGN { if s.Key != nil { k := f.expr(s.Key) - var xelem types.Type // Keys of array, *array, slice, string aren't interesting // since the RHS key type is just an int. - switch ux := typeparams.CoreType(x).(type) { - case *types.Chan: - xelem = ux.Elem() - case *types.Map: - xelem = ux.Key() - } - if xelem != nil { - f.assign(k, xelem) + for term := range terms(x) { + switch term := term.Underlying().(type) { + case *types.Chan: + f.assign(k, term.Elem()) + case *types.Map: + f.assign(k, term.Key()) + } } } if s.Value != nil { val := f.expr(s.Value) - var xelem types.Type // Values of type strings aren't interesting because // the RHS value type is just a rune. - switch ux := typeparams.CoreType(x).(type) { - case *types.Array: - xelem = ux.Elem() - case *types.Map: - xelem = ux.Elem() - case *types.Pointer: // *array - xelem = typeparams.CoreType(typeparams.Deref(ux)).(*types.Array).Elem() - case *types.Slice: - xelem = ux.Elem() - } - if xelem != nil { - f.assign(val, xelem) + for term := range terms(x) { + switch term := term.Underlying().(type) { + case *types.Pointer: + for term := range terms(term.Elem()) { + if array, ok := term.Underlying().(*types.Array); ok { + f.assign(val, array.Elem()) + } + } + case *types.Array: + f.assign(val, term.Elem()) + case *types.Map: + f.assign(val, term.Elem()) + case *types.Slice: + f.assign(val, term.Elem()) + } } } } @@ -726,3 +741,40 @@ _, ok := info.Instances[id] return ok } + +// -- type-set helpers -- + +// hasUnderlyingTermOf reports whether terms(t) contains a +// term whose underlying type has top-level type constructor T +// (e.g. *types.Map for a map). +// If so, it returns an arbitrary one. +// Otherwise it returns the zero value (nil). +// +// This arbitrariness would be a hazard if this function were +// published more widely, but in this package it is used only for +// structural operations (indexing, etc) that require the relevant +// component types to be identical across all terms in the operand's +// type set. +func hasUnderlyingTermOf[T types.Type](t types.Type) T { + for term := range terms(t) { + if under, ok := term.Underlying().(T); ok { + return under + } + } + return *new(T) // e.g. (*types.Map)(nil) +} + +// terms returns the sequence of terms in the type set of t. +// The boolean reports whether the term has a tilde. +// TODO(adonovan): replace with solution to go.dev/issue/61013. +func terms(t types.Type) iter.Seq2[types.Type, bool] { + return func(yield func(types.Type, bool) bool) { + if terms, err := typeparams.NormalTerms(t); err == nil { + for _, term := range terms { + if !yield(term.Type(), term.Tilde()) { + break + } + } + } + } +}
diff --git a/refactor/satisfy/find_test.go b/refactor/satisfy/find_test.go index 2563df8..f9ea44c 100644 --- a/refactor/satisfy/find_test.go +++ b/refactor/satisfy/find_test.go
@@ -227,6 +227,31 @@ } } +// TestIssue79734 is a regression test for a crash caused by +// inappropriate use of the obsolete CoreType operator on operands +// that (legally) have no core type; see go.dev/issue/79734. +func TestIssue79734(t *testing.T) { + const src = `package p + +type I interface { f() } +type Key struct{} +func (Key) f() {} +type Val1 struct{} +type Val2 struct{} + +func _[Map interface{ map[I]Val1 | map[I]Val2 }](m Map) { + delete(m, Key{}) +} +` + got := constraints(t, src) + want := []string{ + "p.I <- p.Key", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("found unexpected constraints: got %s, want %s", got, want) + } +} + func constraints(t *testing.T, src string) []string { // parse fset := token.NewFileSet()