From 5be823e8041a7591b59c4f200bf05de958704df7 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 14 Mar 2025 13:33:03 -0400 Subject: [PATCH 01/60] go/analysis/passes/printf: disallow %q of int This CL causes the printf checker to report a diagnostic if an arbitrary integer other than byte or rune is formatted with %q. This is a common mistake. (However, uses of %c and %U with such integers are rarely mistaken.) The check only applies to files using go1.26 or later. + test with go1.25 and go1.26 Fixes golang/go#72850 Change-Id: Ic09a1d726e99608e3067f5e1a37bacc3a835724f Reviewed-on: https://go-review.googlesource.com/c/tools/+/657957 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/analysis/passes/printf/printf.go | 16 ++++++++++++---- go/analysis/passes/printf/printf_test.go | 2 +- go/analysis/passes/printf/testdata/src/a/a.go | 4 +++- .../printf/testdata/src/issue72850/a_go125.go | 11 +++++++++++ .../printf/testdata/src/issue72850/a_go126.go | 11 +++++++++++ go/analysis/passes/printf/types.go | 13 ++++++++----- 6 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 go/analysis/passes/printf/testdata/src/issue72850/a_go125.go create mode 100644 go/analysis/passes/printf/testdata/src/issue72850/a_go126.go diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 337bc6af9c6..1eac2589bfa 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -612,7 +612,7 @@ func checkPrintf(pass *analysis.Pass, fileVersion string, kind Kind, call *ast.C // breaking existing tests and CI scripts. if idx == len(call.Args)-1 && fileVersion != "" && // fail open - versions.AtLeast(fileVersion, "go1.24") { + versions.AtLeast(fileVersion, versions.Go1_24) { pass.Report(analysis.Diagnostic{ Pos: formatArg.Pos(), @@ -662,7 +662,7 @@ func checkPrintf(pass *analysis.Pass, fileVersion string, kind Kind, call *ast.C anyIndex = true } rng := opRange(formatArg, op) - if !okPrintfArg(pass, call, rng, &maxArgIndex, firstArg, name, op) { + if !okPrintfArg(pass, fileVersion, call, rng, &maxArgIndex, firstArg, name, op) { // One error per format is enough. return } @@ -707,6 +707,7 @@ type printfArgType int const ( argBool printfArgType = 1 << iota + argByte argInt argRune argString @@ -751,7 +752,7 @@ var printVerbs = []printVerb{ {'o', sharpNumFlag, argInt | argPointer}, {'O', sharpNumFlag, argInt | argPointer}, {'p', "-#", argPointer}, - {'q', " -+.0#", argRune | argInt | argString}, + {'q', " -+.0#", argRune | argInt | argString}, // note: when analyzing go1.26 code, argInt => argByte {'s', " -+.0", argString}, {'t', "-", argBool}, {'T', "-", anyType}, @@ -765,7 +766,7 @@ var printVerbs = []printVerb{ // okPrintfArg compares the operation to the arguments actually present, // reporting any discrepancies it can discern, maxArgIndex was the index of the highest used index. // If the final argument is ellipsissed, there's little it can do for that. -func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, rng analysis.Range, maxArgIndex *int, firstArg int, name string, operation *fmtstr.Operation) (ok bool) { +func okPrintfArg(pass *analysis.Pass, fileVersion string, call *ast.CallExpr, rng analysis.Range, maxArgIndex *int, firstArg int, name string, operation *fmtstr.Operation) (ok bool) { verb := operation.Verb.Verb var v printVerb found := false @@ -777,6 +778,13 @@ func okPrintfArg(pass *analysis.Pass, call *ast.CallExpr, rng analysis.Range, ma } } + // When analyzing go1.26 code, rune and byte are the only %q integers (#72850). + if verb == 'q' && + fileVersion != "" && // fail open + versions.AtLeast(fileVersion, versions.Go1_26) { + v.typ = argRune | argByte | argString + } + // Could verb's arg implement fmt.Formatter? // Skip check for the %w verb, which requires an error. formatter := false diff --git a/go/analysis/passes/printf/printf_test.go b/go/analysis/passes/printf/printf_test.go index 1ce9c28c103..c4f21edf318 100644 --- a/go/analysis/passes/printf/printf_test.go +++ b/go/analysis/passes/printf/printf_test.go @@ -19,7 +19,7 @@ func Test(t *testing.T) { printf.Analyzer.Flags.Set("funcs", "Warn,Warnf") analysistest.Run(t, testdata, printf.Analyzer, - "a", "b", "nofmt", "nonconst", "typeparams", "issue68744", "issue70572") + "a", "b", "nofmt", "nonconst", "typeparams", "issue68744", "issue70572", "issue72850") } func TestNonConstantFmtString_Go123(t *testing.T) { diff --git a/go/analysis/passes/printf/testdata/src/a/a.go b/go/analysis/passes/printf/testdata/src/a/a.go index 1fc3748c796..a66f0ac8aa8 100644 --- a/go/analysis/passes/printf/testdata/src/a/a.go +++ b/go/analysis/passes/printf/testdata/src/a/a.go @@ -83,7 +83,7 @@ func PrintfTests() { fmt.Printf("%o %o", 3, i) fmt.Printf("%O %O", 3, i) fmt.Printf("%p", p) - fmt.Printf("%q %q %q %q", 3, i, 'x', r) + fmt.Printf("%q %q %q", byte(i), 'x', r) fmt.Printf("%s %s %s", "hi", s, []byte{65}) fmt.Printf("%t %t", true, b) fmt.Printf("%T %T", 3, i) @@ -171,6 +171,8 @@ func PrintfTests() { fmt.Printf("%q %q", multi()...) // ok fmt.Printf("%#q", `blah`) // ok fmt.Printf("%#b", 3) // ok + fmt.Printf("%q", 3) // ok before go1.26 (#72850) + // printf("now is the time", "buddy") // no error "a.printf call has arguments but no formatting directives" Printf("now is the time", "buddy") // want "a.Printf call has arguments but no formatting directives" Printf("hi") // ok diff --git a/go/analysis/passes/printf/testdata/src/issue72850/a_go125.go b/go/analysis/passes/printf/testdata/src/issue72850/a_go125.go new file mode 100644 index 00000000000..76aec67c03f --- /dev/null +++ b/go/analysis/passes/printf/testdata/src/issue72850/a_go125.go @@ -0,0 +1,11 @@ +//go:build !go1.26 + +package a + +import "fmt" + +var ( + _ = fmt.Sprintf("%q", byte(65)) // ok + _ = fmt.Sprintf("%q", rune(65)) // ok + _ = fmt.Sprintf("%q", 123) // ok: pre-1.26 code allows %q on any integer +) diff --git a/go/analysis/passes/printf/testdata/src/issue72850/a_go126.go b/go/analysis/passes/printf/testdata/src/issue72850/a_go126.go new file mode 100644 index 00000000000..f4c25b210be --- /dev/null +++ b/go/analysis/passes/printf/testdata/src/issue72850/a_go126.go @@ -0,0 +1,11 @@ +//go:build go1.26 + +package a + +import "fmt" + +var ( + _ = fmt.Sprintf("%q", byte(65)) // ok + _ = fmt.Sprintf("%q", rune(65)) // ok + _ = fmt.Sprintf("%q", 123) // want `fmt.Sprintf format %q has arg 123 of wrong type int` +) diff --git a/go/analysis/passes/printf/types.go b/go/analysis/passes/printf/types.go index 8aa3962d09e..2cc5c23f12b 100644 --- a/go/analysis/passes/printf/types.go +++ b/go/analysis/passes/printf/types.go @@ -227,14 +227,20 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool { types.Bool: return m.t&argBool != 0 + case types.Byte: + return m.t&(argInt|argByte) != 0 + + case types.Rune, types.UntypedRune: + return m.t&(argInt|argRune) != 0 + case types.UntypedInt, types.Int, types.Int8, types.Int16, - types.Int32, + // see case Rune for int32 types.Int64, types.Uint, - types.Uint8, + // see case Byte for uint8 types.Uint16, types.Uint32, types.Uint64, @@ -258,9 +264,6 @@ func (m *argMatcher) match(typ types.Type, topLevel bool) bool { case types.UnsafePointer: return m.t&(argPointer|argInt) != 0 - case types.UntypedRune: - return m.t&(argInt|argRune) != 0 - case types.UntypedNil: return false From 5fe2dd892022c5a1f12c338a006ad4128bda033e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 13 Nov 2025 10:32:02 -0500 Subject: [PATCH 02/60] gopls/internal/test/marker: avoid BasicLit in fill_struct.txt In preparation for a go1.26 API change to BasicLit, this CL changes a test to use UnaryExpr instead so that we can avoid the need for two variants of the test. For golang/go#76031 Change-Id: Id608d5d7c36f63fa512d947fabe3596f86e07ea5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/720240 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- .../testdata/codeaction/fill_struct.txt | 20 +++++++++---------- .../codeaction/fill_struct_resolve.txt | 20 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt index 5a50978ad5e..8b5bab47f84 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt @@ -184,11 +184,11 @@ type pointerBuiltinStruct struct { var _ = pointerBuiltinStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a33) -var _ = []ast.BasicLit{ +var _ = []ast.UnaryExpr{ {}, //@codeaction("}", "refactor.rewrite.fillStruct", edit=a34) } -var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) +var _ = []ast.UnaryExpr{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) -- @a31/a3.go -- @@ -17 +17,4 @@ -var _ = Bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a31) @@ -221,17 +221,17 @@ var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edi @@ -39 +39,5 @@ - {}, //@codeaction("}", "refactor.rewrite.fillStruct", edit=a34) + { -+ ValuePos: 0, -+ Kind: 0, -+ Value: "", ++ OpPos: 0, ++ Op: 0, ++ X: nil, + }, //@codeaction("}", "refactor.rewrite.fillStruct", edit=a34) -- @a35/a3.go -- @@ -42 +42,5 @@ --var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) -+var _ = []ast.BasicLit{{ -+ ValuePos: 0, -+ Kind: 0, -+ Value: "", +-var _ = []ast.UnaryExpr{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) ++var _ = []ast.UnaryExpr{{ ++ OpPos: 0, ++ Op: 0, ++ X: nil, +}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) -- a4.go -- package fillstruct 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 9c1f8f728ca..f2dc7a15a33 100644 --- a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt @@ -174,11 +174,11 @@ type pointerBuiltinStruct struct { var _ = pointerBuiltinStruct{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a33) -var _ = []ast.BasicLit{ +var _ = []ast.UnaryExpr{ {}, //@codeaction("}", "refactor.rewrite.fillStruct", edit=a34) } -var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) +var _ = []ast.UnaryExpr{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) -- @a31/a3.go -- @@ -17 +17,4 @@ -var _ = Bar{} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a31) @@ -211,17 +211,17 @@ var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edi @@ -39 +39,5 @@ - {}, //@codeaction("}", "refactor.rewrite.fillStruct", edit=a34) + { -+ ValuePos: 0, -+ Kind: 0, -+ Value: "", ++ OpPos: 0, ++ Op: 0, ++ X: nil, + }, //@codeaction("}", "refactor.rewrite.fillStruct", edit=a34) -- @a35/a3.go -- @@ -42 +42,5 @@ --var _ = []ast.BasicLit{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) -+var _ = []ast.BasicLit{{ -+ ValuePos: 0, -+ Kind: 0, -+ Value: "", +-var _ = []ast.UnaryExpr{{}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) ++var _ = []ast.UnaryExpr{{ ++ OpPos: 0, ++ Op: 0, ++ X: nil, +}} //@codeaction("}", "refactor.rewrite.fillStruct", edit=a35) -- a4.go -- package fillstruct From 1b60160268f92da58fcda9132718627f4760e004 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Mon, 3 Nov 2025 15:22:19 -0500 Subject: [PATCH 03/60] internal/diff/lcs: remove unused exported DiffStrings The exported function DiffStrings is only used in one internal test. There is no corresponding use anywhere else in the tools repository Change-Id: I4952fae1611ae7f76b71d50402ab1b3e627eaeaa Reviewed-on: https://go-review.googlesource.com/c/tools/+/717460 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- internal/diff/lcs/old.go | 4 ---- internal/diff/lcs/old_test.go | 13 ++++--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/internal/diff/lcs/old.go b/internal/diff/lcs/old.go index 7b7c5cc677b..5acc68e1db2 100644 --- a/internal/diff/lcs/old.go +++ b/internal/diff/lcs/old.go @@ -16,10 +16,6 @@ type Diff struct { ReplStart, ReplEnd int // offset of replacement text in B } -// DiffStrings returns the differences between two strings. -// It does not respect rune boundaries. -func DiffStrings(a, b string) []Diff { return diff(stringSeqs{a, b}) } - // DiffBytes returns the differences between two byte sequences. // It does not respect rune boundaries. func DiffBytes(a, b []byte) []Diff { return diff(bytesSeqs{a, b}) } diff --git a/internal/diff/lcs/old_test.go b/internal/diff/lcs/old_test.go index 7381954398e..00c8fa37127 100644 --- a/internal/diff/lcs/old_test.go +++ b/internal/diff/lcs/old_test.go @@ -133,18 +133,13 @@ func TestRandOld(t *testing.T) { // to ensure at least minimal parity of the three representations. func TestDiffAPI(t *testing.T) { for _, test := range []struct { - a, b string - wantStrings, wantBytes, wantRunes string + a, b string + wantBytes, wantRunes string }{ - {"abcXdef", "abcxdef", "[{3 4 3 4}]", "[{3 4 3 4}]", "[{3 4 3 4}]"}, // ASCII - {"abcωdef", "abcΩdef", "[{3 5 3 5}]", "[{3 5 3 5}]", "[{3 4 3 4}]"}, // non-ASCII + {"abcXdef", "abcxdef", "[{3 4 3 4}]", "[{3 4 3 4}]"}, // ASCII + {"abcωdef", "abcΩdef", "[{3 5 3 5}]", "[{3 4 3 4}]"}, // non-ASCII } { - gotStrings := fmt.Sprint(DiffStrings(test.a, test.b)) - if gotStrings != test.wantStrings { - t.Errorf("DiffStrings(%q, %q) = %v, want %v", - test.a, test.b, gotStrings, test.wantStrings) - } gotBytes := fmt.Sprint(DiffBytes([]byte(test.a), []byte(test.b))) if gotBytes != test.wantBytes { t.Errorf("DiffBytes(%q, %q) = %v, want %v", From c6ecbb79739b316941a5e0df520556c3deeaeaf1 Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Wed, 15 Oct 2025 14:21:21 -0400 Subject: [PATCH 04/60] gopls/internal/golang: implement package move Implement package move via package rename. Updates the logic for verifying if a package rename is valid with the following checks: 1. Moving a package across the module boundary is not allowed 2. Merging two packages is not allowed; can't rename a package to a path that already exists. 3. Specifying a relative path (not yet supported) 4. Moving package to an internal directory - not allowed if any importers are outside the internal directory renamePackage is refactored to take the effective new package directory, import path, and name, since for a package move the package name is not sufficient to construct the new package directory. Previously, renaming a package foo would also rename/move subpackages of foo. Now, subpackages are not renamed by default. When we have dialog support, users will be able to specify if they do want to also rename subpackages. Updates golang/go#57171 Fixes golang/go#75812 by detecting when the package path already exists and displaying an error "invalid package identifier: "package_name" already exists"" Change-Id: I5a38cc0a3ea4bd2ec63f5d5a28a240194feff327 Reviewed-on: https://go-review.googlesource.com/c/tools/+/712160 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/rename.go | 194 +++++++++++------ gopls/internal/golang/rename_check.go | 75 ++++++- gopls/internal/mcp/rename_symbol.go | 2 +- gopls/internal/protocol/edits.go | 10 + gopls/internal/server/rename.go | 5 +- .../internal/test/integration/fake/editor.go | 25 ++- .../test/integration/misc/rename_test.go | 99 +++++++-- gopls/internal/test/marker/marker_test.go | 17 +- .../marker/testdata/rename/packagedecl.txt | 196 +++++++++++++++++- 9 files changed, 523 insertions(+), 100 deletions(-) diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index db03a93dc1f..5c1d575c046 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -52,6 +52,7 @@ import ( "go/token" "go/types" "maps" + "os" "path" "path/filepath" "regexp" @@ -72,6 +73,7 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/cursorutil" + "golang.org/x/tools/gopls/internal/util/pathutil" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/diff" @@ -417,9 +419,8 @@ func editsToDocChanges(ctx context.Context, snapshot *cache.Snapshot, edits map[ } // Rename returns a map of TextEdits for each file modified when renaming a -// given identifier within a package and a boolean value of true for renaming -// package and false otherwise. -func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, newName string) ([]protocol.DocumentChange, error) { +// given identifier within a package. +func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, newName string, renameSubpkgs bool) ([]protocol.DocumentChange, error) { ctx, done := event.Start(ctx, "golang.Rename") defer done() @@ -449,23 +450,32 @@ func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp pro return nil, err } - var editMap map[protocol.DocumentURI][]diff.Edit + var ( + editMap map[protocol.DocumentURI][]diff.Edit + newPkgDir string // declared here so it can be used later to rename the package's enclosing directory + ) if inPackageName { countRenamePackage.Inc() - if !isValidPackagePath(pkg.String(), newName) { - return nil, fmt.Errorf("invalid package path: %q (package moves are not yet supported, see go.dev/issue/57171)", newName) + var ( + newPkgName PackageName + newPkgPath PackagePath + err error + ) + if newPkgDir, newPkgName, newPkgPath, err = checkPackageRename(snapshot.Options(), pkg, f, newName); err != nil { + return nil, err + } + editMap, err = renamePackage(ctx, snapshot, f, newPkgName, newPkgPath, newPkgDir, renameSubpkgs) + if err != nil { + return nil, err } - // Only the last element of the path is required as input for [renamePackageName]. - newName = path.Base(newName) - editMap, err = renamePackageName(ctx, snapshot, f, PackageName(newName)) } else { if !isValidIdentifier(newName) { return nil, fmt.Errorf("invalid identifier to rename: %q", newName) } editMap, err = renameOrdinary(ctx, snapshot, f.URI(), pp, newName) - } - if err != nil { - return nil, err + if err != nil { + return nil, err + } } // Convert edits to protocol form. @@ -512,13 +522,49 @@ func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp pro if err != nil { return nil, err } - // Update the last component of the file's enclosing directory. if inPackageName { oldDir := f.URI().DirPath() - newDir := filepath.Join(filepath.Dir(oldDir), path.Base(newName)) - changes = append(changes, protocol.DocumentChangeRename( - protocol.URIFromPath(oldDir), - protocol.URIFromPath(newDir))) + if renameSubpkgs { + // Update the last component of the file's enclosing directory. + changes = append(changes, protocol.DocumentChangeRename( + protocol.URIFromPath(oldDir), + protocol.URIFromPath(newPkgDir))) + } else { + // Not moving subpackages, so we need to move the files individually + // Move each file from oldDir to newPkgDir + files, err := os.ReadDir(oldDir) + if err != nil { + return nil, err + } + hasNestedDir := false + for _, f := range files { + if !f.IsDir() { + changes = append(changes, + protocol.DocumentChangeRename( + protocol.URIFromPath(filepath.Join(oldDir, f.Name())), + protocol.URIFromPath(filepath.Join(newPkgDir, f.Name()))), + ) + } else { + hasNestedDir = true + } + } + // Delete oldPkgDir if it is now empty and newPkgDir is not a subdirectory of oldPkgDir. + // protocol.DeleteFile, when used with a directory, will only delete the directory + // if it is empty as long as the "recursive" option is set to its default value, false. + // TODO(mkalil): The above should be true according to the LSP spec but in VSCode + // we could get "Renamed failed to apply edits" errors when we include the edit + // to delete a directory that can't be deleted. For now, check if the oldDir has + // any nested directories before trying to delete it. + // TODO(mkalil): We could be deleting more directories. For example, if + // we have a package at a/b/c/d and we move it to a/z, it only deletes + // directory d and would leave directories b and c even if they are now + // empty. + oldPkgDir := f.URI().DirPath() + if !pathutil.InDir(oldPkgDir, newPkgDir) && !hasNestedDir { + changes = append(changes, + protocol.DocumentChangeDelete(protocol.URIFromPath(oldPkgDir))) + } + } } return changes, nil } @@ -912,32 +958,39 @@ func renameExported(pkgs []*cache.Package, declPkgPath PackagePath, declObjPath return allEdits, nil } -// renamePackageName renames package declarations, imports, and go.mod files. -func renamePackageName(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName) (map[protocol.DocumentURI][]diff.Edit, error) { +// renamePackage renames package declarations, imports, and go.mod files. +// +// f is the file originating the rename, and therefore f.URI().Dir() is the +// current package directory. newName, newPath, and newDir describe the renaming. +func renamePackage(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName, newPath PackagePath, newDir string, renameSubpkgs bool) (map[protocol.DocumentURI][]diff.Edit, error) { // Rename the package decl and all imports. - renamingEdits, err := renamePackage(ctx, s, f, newName) + renamingEdits := make(map[protocol.DocumentURI][]diff.Edit) + err := updatePackageDeclsAndImports(ctx, s, f, newName, newPath, renamingEdits, renameSubpkgs) if err != nil { return nil, err } - oldBase := f.URI().DirPath() - newPkgDir := filepath.Join(filepath.Dir(oldBase), string(newName)) + err = updateModFiles(ctx, s, f.URI().DirPath(), newDir, renamingEdits, renameSubpkgs) + if err != nil { + return nil, err + } - // Update any affected replace directives in go.mod files. - // TODO(adonovan): extract into its own function. - // - // Get all workspace modules. - // TODO(adonovan): should this operate on all go.mod files, - // irrespective of whether they are included in the workspace? + return renamingEdits, nil +} + +// Update any affected replace directives in go.mod files. +// TODO(adonovan): should this operate on all go.mod files, +// irrespective of whether they are included in the workspace? +func updateModFiles(ctx context.Context, s *cache.Snapshot, oldDir string, newPkgDir string, renamingEdits map[protocol.DocumentURI][]diff.Edit, renameSubpkgs bool) error { modFiles := s.View().ModFiles() for _, m := range modFiles { fh, err := s.ReadFile(ctx, m) if err != nil { - return nil, err + return err } pm, err := s.ParseMod(ctx, fh) if err != nil { - return nil, err + return err } modFileDir := pm.URI.DirPath() @@ -948,15 +1001,16 @@ func renamePackageName(ctx context.Context, s *cache.Snapshot, f file.Handle, ne if !strings.HasPrefix(r.New.Path, "/") && !strings.HasPrefix(r.New.Path, "./") && !strings.HasPrefix(r.New.Path, "../") { continue } - - replacedPath := r.New.Path + replacedDir := r.New.Path if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { - replacedPath = filepath.Join(modFileDir, r.New.Path) + replacedDir = filepath.Join(modFileDir, r.New.Path) } // TODO: Is there a risk of converting a '\' delimited replacement to a '/' delimited replacement? - if !strings.HasPrefix(filepath.ToSlash(replacedPath)+"/", filepath.ToSlash(oldBase)+"/") { - continue // not affected by the package renaming + + if renameSubpkgs && !strings.HasPrefix(filepath.ToSlash(replacedDir)+"/", filepath.ToSlash(oldDir)+"/") || + !renameSubpkgs && !(filepath.ToSlash(replacedDir) == filepath.ToSlash(oldDir)) { + continue //not affected by the package renanming } affectedReplaces = append(affectedReplaces, r) @@ -967,20 +1021,20 @@ func renamePackageName(ctx context.Context, s *cache.Snapshot, f file.Handle, ne } copied, err := modfile.Parse("", pm.Mapper.Content, nil) if err != nil { - return nil, err + return err } for _, r := range affectedReplaces { - replacedPath := r.New.Path + replacedDir := r.New.Path if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { - replacedPath = filepath.Join(modFileDir, r.New.Path) + replacedDir = filepath.Join(modFileDir, r.New.Path) } - suffix := strings.TrimPrefix(replacedPath, oldBase) + suffix := strings.TrimPrefix(replacedDir, oldDir) newReplacedPath, err := filepath.Rel(modFileDir, newPkgDir+suffix) if err != nil { - return nil, err + return err } newReplacedPath = filepath.ToSlash(newReplacedPath) @@ -990,72 +1044,67 @@ func renamePackageName(ctx context.Context, s *cache.Snapshot, f file.Handle, ne } if err := copied.AddReplace(r.Old.Path, "", newReplacedPath, ""); err != nil { - return nil, err + return err } } copied.Cleanup() newContent, err := copied.Format() if err != nil { - return nil, err + return err } // Calculate the edits to be made due to the change. edits := diff.Bytes(pm.Mapper.Content, newContent) renamingEdits[pm.URI] = append(renamingEdits[pm.URI], edits...) } - - return renamingEdits, nil + return nil } -// renamePackage computes all workspace edits required to rename the package +// updatePackageDeclsAndImports computes all workspace edits required to rename the package // described by the given metadata, to newName, by renaming its package // directory. // // It updates package clauses and import paths for the renamed package as well // as any other packages affected by the directory renaming among all packages // known to the snapshot. -func renamePackage(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName) (map[protocol.DocumentURI][]diff.Edit, error) { +func updatePackageDeclsAndImports(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName, newPkgPath PackagePath, renamingEdits map[protocol.DocumentURI][]diff.Edit, renameSubpkgs bool) error { if strings.HasSuffix(string(newName), "_test") { - return nil, fmt.Errorf("cannot rename to _test package") + return fmt.Errorf("cannot rename to _test package") } // We need metadata for the relevant package and module paths. // These should be the same for all packages containing the file. meta, err := s.NarrowestMetadataForFile(ctx, f.URI()) if err != nil { - return nil, err + return err } oldPkgPath := meta.PkgPath if meta.Module == nil { - return nil, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PkgPath) + return fmt.Errorf("cannot rename package: missing module information for package %q", meta.PkgPath) } modulePath := PackagePath(meta.Module.Path) if modulePath == oldPkgPath { - return nil, fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath) + return fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath) } - newPathPrefix := path.Join(path.Dir(string(oldPkgPath)), string(newName)) - // We must inspect all packages, not just direct importers, - // because we also rename subpackages, which may be unrelated. + // because we might also rename subpackages, which may be unrelated. // (If the renamed package imports a subpackage it may require // edits to both its package and import decls.) allMetadata, err := s.AllMetadata(ctx) if err != nil { - return nil, err + return err } - // Rename package and import declarations in all relevant packages. - edits := make(map[protocol.DocumentURI][]diff.Edit) for _, mp := range allMetadata { // Special case: x_test packages for the renamed package will not have the // package path as a dir prefix, but still need their package clauses // renamed. if mp.PkgPath == oldPkgPath+"_test" { - if err := renamePackageClause(ctx, mp, s, newName+"_test", edits); err != nil { - return nil, err + if err := renamePackageClause(ctx, mp, s, newName+"_test", renamingEdits); err != nil { + return err } continue } @@ -1063,13 +1112,14 @@ func renamePackage(ctx context.Context, s *cache.Snapshot, f file.Handle, newNam // Subtle: check this condition before checking for valid module info // below, because we should not fail this operation if unrelated packages // lack module info. - if !strings.HasPrefix(string(mp.PkgPath)+"/", string(oldPkgPath)+"/") { + if renameSubpkgs && !pathutil.InDir(string(oldPkgPath), string(mp.PkgPath)) || + !renameSubpkgs && mp.PkgPath != oldPkgPath { continue // not affected by the package renaming } if mp.Module == nil { // This check will always fail under Bazel. - return nil, fmt.Errorf("cannot rename package: missing module information for package %q", mp.PkgPath) + return fmt.Errorf("cannot rename package: missing module information for package %q", mp.PkgPath) } if modulePath != PackagePath(mp.Module.Path) { @@ -1078,24 +1128,24 @@ func renamePackage(ctx context.Context, s *cache.Snapshot, f file.Handle, newNam // Renaming a package consists of changing its import path and package name. suffix := strings.TrimPrefix(string(mp.PkgPath), string(oldPkgPath)) - newPath := newPathPrefix + suffix + newImportPath := string(newPkgPath) + suffix pkgName := mp.Name if mp.PkgPath == oldPkgPath { pkgName = newName - if err := renamePackageClause(ctx, mp, s, newName, edits); err != nil { - return nil, err + if err := renamePackageClause(ctx, mp, s, newName, renamingEdits); err != nil { + return err } } - imp := ImportPath(newPath) // TODO(adonovan): what if newPath has vendor/ prefix? - if err := renameImports(ctx, s, mp, imp, pkgName, edits); err != nil { - return nil, err + imp := ImportPath(newImportPath) // TODO(adonovan): what if newImportPath has vendor/ prefix? + if err := renameImports(ctx, s, mp, imp, pkgName, renamingEdits); err != nil { + return err } } - return edits, nil + return nil } // renamePackageClause computes edits renaming the package clause of files in @@ -1162,6 +1212,16 @@ func renameImports(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.P continue // not the import we're looking for } + // If we are moving a package from a non-internal directory to + // an internal directory, and there is an importer located in a + // different module, then it is an invalid import. + // + // We do not prevent moves from an internal directory to a + // non-internal directory. + if !metadata.IsValidImport(rdep.PkgPath, metadata.PackagePath(newPath), snapshot.View().Type() != cache.GoPackagesDriverView) { + return fmt.Errorf("invalid: package move would result in illegal internal import") + } + // If the import does not explicitly specify // a local name, then we need to invoke the // type checker to locate references to update. diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 6d2e521f97d..2370c6e0a55 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -35,8 +35,11 @@ package golang import ( "fmt" "go/ast" + "go/build" "go/token" "go/types" + "io/fs" + "os" "path/filepath" "reflect" "strings" @@ -46,6 +49,8 @@ import ( "golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" @@ -913,15 +918,67 @@ func isValidIdentifier(id string) bool { return token.Lookup(id) == token.IDENT } -// isValidPackagePath reports whether newPath is a valid new path for the -// package currently at oldPath. For now, we only support renames that -// do not result in a package move. -// TODO(mkalil): support package renames with arbitrary package paths, including -// relative paths. -func isValidPackagePath(oldPath, newPath string) bool { - // We prompt with the full package path, but some users may delete this and - // just enter a package identifier, which we should still support. - return isValidIdentifier(newPath) || filepath.Dir(oldPath) == filepath.Dir(newPath) +// checkPackageRename returns the effective new package directory, path and name +// resulting from renaming the current package to newName, which may be an +// identifier or a package path. An error is returned if the renaming is +// invalid. +func checkPackageRename(opts *settings.Options, curPkg *cache.Package, f file.Handle, newName string) (newPkgDir string, newPkgName PackageName, newPkgPath PackagePath, err error) { + // Path unchanged + if newName == curPkg.String() || newName == string(curPkg.Metadata().Name) { + return f.URI().DirPath(), curPkg.Metadata().Name, curPkg.Metadata().PkgPath, nil + } + // TODO(mkalil): support relative paths + if build.IsLocalImport(newName) { + return "", "", "", fmt.Errorf("specifying relative paths in package rename not yet supported") + } + // When package move is enabled, we prompt with the full package path. Users + // can either submit a full package path or just provide the package + // identifier. + validIdent := isValidIdentifier(newName) + if validIdent { + // Not an attempted move. Check if a directory already exists at that + // path, in which case we should not allow renaming. + root := filepath.Dir(f.URI().DirPath()) + newPkgDir = filepath.Join(root, newName) + _, err := os.Stat(newPkgDir) + if err == nil { + // Directory already exists, return an error. + return "", "", "", fmt.Errorf("invalid package identifier: %q already exists", newName) + } + parentPkgPath := strings.TrimSuffix(string(curPkg.Metadata().PkgPath), string(curPkg.Metadata().Name)) // leaves a trailing slash + newPkgPath = PackagePath(parentPkgPath + newName) + newPkgName = PackageName(newName) + return newPkgDir, newPkgName, newPkgPath, nil + } + if !opts.PackageMove { + return "", "", "", fmt.Errorf("a full package path is specified but internal setting 'packageMove' is not enabled") + } + // Don't allow moving packages across module boundaries. + curModPath := curPkg.Metadata().Module.Path + if !strings.HasPrefix(newName+"/", curModPath+"/") { + return "", "", "", fmt.Errorf("invalid package path %q; cannot move package across module boundary", newName) + } + // Don't support package merging. If a directory already exists + // at that path, we should not allow renaming. + newPathAfterMod := strings.TrimPrefix(newName, curModPath) //newName is a package path here. + modDir := curPkg.Metadata().Module.Dir + newPkgDir = filepath.Join(modDir, filepath.FromSlash(newPathAfterMod)) + // Trim the starting slash, which is not considered valid in fs.ValidPath. + isValidDir := fs.ValidPath(strings.TrimPrefix(newPkgDir, string(filepath.Separator))) + if !isValidDir { + return "", "", "", fmt.Errorf("invalid package path %q", newName) + } + newPkgName = PackageName(filepath.Base(newPkgDir)) + _, err = os.Stat(newPkgDir) + if err == nil { + // Directory or file already exists at this path; return an error. + return "", "", "", fmt.Errorf("invalid package path: %q already exists", newName) + } + // Verify that the new package name is a valid identifier. + if !isValidIdentifier(string(newPkgName)) { + return "", "", "", fmt.Errorf("invalid package name %q", newPkgName) + } + return newPkgDir, newPkgName, PackagePath(newName), nil } // isLocal reports whether obj is local to some function. diff --git a/gopls/internal/mcp/rename_symbol.go b/gopls/internal/mcp/rename_symbol.go index e2fce0bfe2d..56297818b0d 100644 --- a/gopls/internal/mcp/rename_symbol.go +++ b/gopls/internal/mcp/rename_symbol.go @@ -38,7 +38,7 @@ func (h *handler) renameSymbolHandler(ctx context.Context, req *mcp.CallToolRequ if err != nil { return nil, nil, err } - changes, err := golang.Rename(ctx, snapshot, fh, loc.Range.Start, params.NewName) + changes, err := golang.Rename(ctx, snapshot, fh, loc.Range.Start, params.NewName, false) if err != nil { return nil, nil, err } diff --git a/gopls/internal/protocol/edits.go b/gopls/internal/protocol/edits.go index c5d3592a8ee..2ed476cbe01 100644 --- a/gopls/internal/protocol/edits.go +++ b/gopls/internal/protocol/edits.go @@ -140,6 +140,16 @@ func DocumentChangeCreate(uri DocumentURI) DocumentChange { } } +// DocumentChangeDelete constructs a DocumentChange that deletes a file. +func DocumentChangeDelete(uri DocumentURI) DocumentChange { + return DocumentChange{ + DeleteFile: &DeleteFile{ + Kind: "delete", + URI: uri, + }, + } +} + // DocumentChangeRename constructs a DocumentChange that renames a file. func DocumentChangeRename(src, dst DocumentURI) DocumentChange { return DocumentChange{ diff --git a/gopls/internal/server/rename.go b/gopls/internal/server/rename.go index c7abc4fd77b..304fa7f21b9 100644 --- a/gopls/internal/server/rename.go +++ b/gopls/internal/server/rename.go @@ -30,7 +30,10 @@ func (s *server) Rename(ctx context.Context, params *protocol.RenameParams) (*pr return nil, fmt.Errorf("cannot rename in file of type %s", kind) } - changes, err := golang.Rename(ctx, snapshot, fh, params.Position, params.NewName) + // TODO(mkalil): By default, we don't move subpackages on a package rename. + // When dialog support is enabled, the user will be able to specify whether + // they do want to move subpackages. + changes, err := golang.Rename(ctx, snapshot, fh, params.Position, params.NewName, false) if err != nil { return nil, err } diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index b8b3e98352e..313e5cdbbec 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -1542,7 +1542,9 @@ func (e *Editor) applyWorkspaceEdit(ctx context.Context, wsedit *protocol.Worksp case change.RenameFile != nil: old := uriToPath(change.RenameFile.OldURI) new := uriToPath(change.RenameFile.NewURI) - return e.RenameFile(ctx, old, new) + if err := e.RenameFile(ctx, old, new); err != nil { + return err + } case change.CreateFile != nil: path := uriToPath(change.CreateFile.URI) @@ -1551,7 +1553,26 @@ func (e *Editor) applyWorkspaceEdit(ctx context.Context, wsedit *protocol.Worksp } case change.DeleteFile != nil: - path := uriToPath(change.CreateFile.URI) + path := uriToPath(change.DeleteFile.URI) + // The specified URI could be a file or a directory. + files, err := e.sandbox.Workdir.ListFiles(path) + if err != nil { + return err + } + if len(files) > 0 { + // Directory is not empty. If "Recursive" is true, we delete + // every file in the folder. Otherwise, we continue without + // deleting the directory. + if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive { + for _, f := range files { + _ = e.CloseBuffer(ctx, f) // returns error if not open + if err := e.sandbox.Workdir.RemoveFile(ctx, f); err != nil { + return err // e.g. doesn't exist + } + } + } + continue + } _ = e.CloseBuffer(ctx, path) // returns error if not open if err := e.sandbox.Workdir.RemoveFile(ctx, path); err != nil { return err // e.g. doesn't exist diff --git a/gopls/internal/test/integration/misc/rename_test.go b/gopls/internal/test/integration/misc/rename_test.go index e3116e1dd2a..32a1b4ca92b 100644 --- a/gopls/internal/test/integration/misc/rename_test.go +++ b/gopls/internal/test/integration/misc/rename_test.go @@ -181,10 +181,14 @@ func main() { env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") // Check if the new package name exists. + /// Don't rename subpackages. + // TODO(mkalil): update the test framework to handle variable input + // for renameSubpkgs. env.RegexpSearch("nested/a.go", "package nested") env.RegexpSearch("main.go", `nested2 "mod.com/nested"`) - env.RegexpSearch("main.go", "mod.com/nested/nested") - env.RegexpSearch("main.go", `nested1 "mod.com/nested/x"`) + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", "mod.com/lib/nested") + env.RegexpSearch("main.go", `nested1 "mod.com/lib/x"`) }) } @@ -223,7 +227,7 @@ func main() { // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") env.RegexpSearch("main.go", "mod.com/nested") - env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) + env.RegexpSearch("main.go", `lib1 "mod.com/lib/nested"`) }) } @@ -262,7 +266,8 @@ func main() { // Check if the new package name exists. env.RegexpSearch("nested/a.go", "package nested") env.RegexpSearch("main.go", "mod.com/nested") - env.RegexpSearch("main.go", `foo "mod.com/nested/nested"`) + // Don't rename subpackages. + env.RegexpSearch("main.go", `foo "mod.com/lib/nested"`) }) } @@ -307,7 +312,7 @@ func main() { env.RegexpSearch("lib1/a.go", "package lib1") env.RegexpSearch("lib1/b.go", "package lib1") env.RegexpSearch("main.go", "mod.com/lib1") - env.RegexpSearch("main.go", "mod.com/lib1/nested") + env.RegexpSearch("main.go", "mod.com/lib/nested") }) } @@ -477,7 +482,7 @@ func main() { env.RegexpSearch("lib1/a.go", "package lib1") env.RegexpSearch("lib1/b.go", "package lib1") env.RegexpSearch("main.go", "mod.com/lib1") - env.RegexpSearch("main.go", "mod.com/lib1/nested") + env.RegexpSearch("main.go", "mod.com/lib/nested") // Check if the test package is renamed env.RegexpSearch("lib1/a_test.go", "package lib1_test") @@ -554,15 +559,16 @@ func main() { env.Rename(env.RegexpSearch("foo/foo.go", "foo"), "foox") env.RegexpSearch("foox/foo.go", "package foox") - env.OpenFile("foox/bar/bar.go") - env.OpenFile("foox/bar/go.mod") + // Don't rename subpackages. + env.OpenFile("foo/bar/bar.go") + env.OpenFile("foo/bar/go.mod") env.RegexpSearch("main.go", "mod.com/foo/bar") env.RegexpSearch("main.go", "mod.com/foox") env.RegexpSearch("main.go", "foox.Bar()") - env.RegexpSearch("go.mod", "./foox/bar") - env.RegexpSearch("go.mod", "./foox/baz") + env.RegexpSearch("go.mod", "./foo/bar") + env.RegexpSearch("go.mod", "./foo/baz") }) } @@ -603,7 +609,8 @@ func main() { env.RegexpSearch("nested/a.go", "package nested") env.RegexpSearch("main.go", "mod.com/nested") env.RegexpSearch("main.go", `lib1 "mod.com/nested"`) - env.RegexpSearch("main.go", `lib2 "mod.com/nested/nested"`) + // Don't rename subpackages. + env.RegexpSearch("main.go", `lib2 "mod.com/lib/nested"`) }) } @@ -644,7 +651,8 @@ func main() { env.RegexpSearch("nested/a.go", "package nested") env.RegexpSearch("main.go", "mod.com/nested") env.RegexpSearch("main.go", `_ "mod.com/nested"`) - env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) + // Don't rename subpackages. + env.RegexpSearch("main.go", `lib1 "mod.com/lib/nested"`) }) } @@ -799,7 +807,7 @@ const C = lib.A + nested.B -- testdata/libx/a.go -- package libx -import "mod.com/libx/nested" +import "mod.com/lib/nested" const A = 1 + nested.B -- testdata/other/other.go -- @@ -807,7 +815,7 @@ package other import ( "mod.com/libx" - "mod.com/libx/nested" + "mod.com/lib/nested" ) const C = libx.A + nested.B @@ -896,11 +904,72 @@ func main() { // Check if the new package name exists. env.RegexpSearch("lib1/a.go", "package lib1") - env.RegexpSearch("lib1/a.go", "mod.com/lib1/internal/utils") + // Don't rename subpackages. + env.RegexpSearch("lib1/a.go", "mod.com/lib/internal/utils") env.RegexpSearch("main.go", `import "mod.com/lib1"`) env.RegexpSearch("main.go", "lib1.print()") }) } +func TestRenamePackage_InvalidPackageMove(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.21 +-- test1/test1.go -- +package test1 + +-- test2/test2.go -- +package test2 + +` + WithOptions(Settings{"packageMove": true}).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("test1/test1.go") + loc := env.RegexpSearch("test1/test1.go", "test1") + badNames := []string{"test2", "mod.com/test2", "differentmod.com/test2", + "mod.com/$$$", "mod*.$+", "mod.commmm"} + expectedErrMatches := []string{"invalid package identifier", "already exists", + "cannot move package across module boundary", "invalid package name", + "invalid package path", "cannot move package across module boundary"} + for i, badName := range badNames { + err := env.Editor.Rename(env.Ctx, loc, badName) + if err == nil { + t.Errorf("Rename to %s succeeded, want non-nil error", badName) + } else if !strings.Contains(err.Error(), expectedErrMatches[i]) { + t.Errorf("Rename to %s produced incorrect error message, got %s, want %s", badName, err.Error(), expectedErrMatches[i]) + } + } + }) +} + +func TestRenamePackage_PackageMoveDisabled(t *testing.T) { + const files = ` +-- go.mod -- +module mod.com + +go 1.21 +-- test1/test1.go -- +package test1 + +-- test2/test2.go -- +package test2 + +` + WithOptions(Settings{"packageMove": false}).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("test1/test1.go") + loc := env.RegexpSearch("test1/test1.go", "test1") + badNames := []string{"test2", "mod.com/test3"} + expectedErrMatches := []string{"invalid package identifier", "setting 'packageMove' is not enabled"} + for i, badName := range badNames { + err := env.Editor.Rename(env.Ctx, loc, badName) + if err == nil { + t.Errorf("Rename to %s succeeded, want non-nil error", badName) + } else if !strings.Contains(err.Error(), expectedErrMatches[i]) { + t.Errorf("Rename to %s produced incorrect error message, got %s, want %s", badName, err.Error(), expectedErrMatches[i]) + } + } + }) +} // checkTestdata checks that current buffer contents match their corresponding // expected content in the testdata directory. diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index aa8c8b5db9c..47ca885af38 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -2145,7 +2145,22 @@ func changedFiles(env *integration.Env, changes []protocol.DocumentChange) (map[ case change.DeleteFile != nil: uri := change.DeleteFile.URI if _, err := read(uri); err != nil { - return nil, fmt.Errorf("DeleteFile %s: file does not exist", uri) + // The specified URI could be a file or a directory. + files := env.ListFiles(uriToPath(uri)) + if files == nil { + return nil, fmt.Errorf("DeleteFile %s: file does not exist", uri) + } + if len(files) > 0 { + // Directory is not empty. If "Recursive" is true, we delete + // every file in the folder. Otherwise, we continue without + // deleting the directory. + if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive { + for _, f := range files { + write(protocol.URIFromPath(f), nil) + } + } + continue + } } write(uri, nil) diff --git a/gopls/internal/test/marker/testdata/rename/packagedecl.txt b/gopls/internal/test/marker/testdata/rename/packagedecl.txt index d27688c8351..389bb662544 100644 --- a/gopls/internal/test/marker/testdata/rename/packagedecl.txt +++ b/gopls/internal/test/marker/testdata/rename/packagedecl.txt @@ -1,4 +1,16 @@ -This test verifies the behavior of renaming a package declaration. +This test verifies the behavior of renaming a package declaration when package move is enabled. +It tests the following: +- Invalid renames: + - Moving across a module boundary + - Moving to a package that already exists + +- Renames edge cases: + - Naming collisions + - Renaming to internal package + - Not allowed when there is some importer of the package to be moved + that is not in the internal package + - Impact of renaming on _test packages + - Renaming with the exact same path - no edits produced -- settings.json -- { @@ -8,19 +20,195 @@ This test verifies the behavior of renaming a package declaration. -- flags -- -ignore_extra_diags +-- go.work -- +use . +use ./othermoddir + -- go.mod -- module golang.org/lsptests/rename +replace golang.org/lsptests/six/four => ./six/four + go 1.20 -- one/one.go -- -package one //@ rename("one", "golang.org/lsptests/rename/one", sameName), rename("one", "golang.org/lsptests/rename/two", newNameSameDir), renameerr("one", "golang.org/lsptests/otherdir/one", re"invalid package path") +package one //@ rename("one", "golang.org/lsptests/rename/one", sameName), rename("one", "golang.org/lsptests/rename/two", newNameSameDir), renameerr("one", "golang.org/lsptests/otherdir/one", re"cannot move package across module boundary") + +-- three/three.go -- +package three //@ renameerr("three", "golang.org/lsptests/othermod/three", re"cannot move package across module boundary"), renameerr("three", "golang.org/lsptests/rename/one", re"already exists") + + +-- six/six.go -- +package six //@ renameerr("six", "golang.org/lsptests/rename/$$$", re"invalid package name") + +import "golang.org/lsptests/rename/six/four" + +func Foo() { + four.Bar() +} + +-- six/four/four.go -- +package four //@ rename("four", "golang.org/lsptests/rename/five", moveUpOneDir), renameerr("four", "../relative", re"specifying relative paths in package rename not yet supported") + +func Bar() {} + +-- seven/seven/seven.go -- +package seven + +import +( + "golang.org/lsptests/rename/six/four" + five "golang.org/lsptests/rename/six" +) + +func foo() { + four.Bar() +} + +-- importsnine/importsnine.go -- +package importsnine //@ rename("importsnine", "golang.org/lsptests/rename/nine/internal/importsnine", allowedInternal) // only importers of this package are already in the internal package, so okay to move it + +import ( + "golang.org/lsptests/rename/nine/nine/nine" +) + +-- nine/nine/nine/nine.go -- +package nine //@ renameerr("nine", "golang.org/lsptests/rename/nine/internal/nine", re"package move would result in illegal internal import") + +-- nine/internal/helpers.go -- +package internal + +import "golang.org/lsptests/rename/importsnine" + +-- seven/eight/eight.go -- +package eight //@ rename("eight", "golang.org/lsptests/rename/six/eight", differentDir), rename("eight", "golang.org/lsptests/rename/nine/nine/nine/eight", moveDownTwoDirs) +-- fix/fix.go -- +package fix //@ rename("fix", "golang.org/lsptests/rename/newFix", renameWithTestPkg) + +import ( + "golang.org/lsptests/rename/seven/eight" + ) +-- seven/eight/other/other.go -- +package other //@ rename("other", "golang.org/lsptests/rename/seven/eight/other/newDir1/newDir2", createNewDir) + +-- fix/fix_test.go -- +package fix_test +-- othermoddir/othermod/other.go -- +package othermod + +import "golang.org/lsptests/rename/nine/nine/nine" + + +-- othermoddir/go.mod -- +module mod.com/other + + +-- @moveDownTwoDirs/nine/nine/nine/eight/eight.go -- +@@ -0,0 +1 @@ ++package eight //@ rename("eight", "golang.org/lsptests/rename/six/eight", differentDir), rename("eight", "golang.org/lsptests/rename/nine/nine/nine/eight", moveDownTwoDirs) +-- @moveDownTwoDirs/seven/eight/eight.go -- +@@ -1 +0,0 @@ +-package eight //@ rename("eight", "golang.org/lsptests/rename/six/eight", differentDir), rename("eight", "golang.org/lsptests/rename/nine/nine/nine/eight", moveDownTwoDirs) +-- @moveUpOneDir/five/four.go -- +@@ -0,0 +1,4 @@ ++package five //@ rename("four", "golang.org/lsptests/rename/five", moveUpOneDir), renameerr("four", "../relative", re"specifying relative paths in package rename not yet supported") ++ ++func Bar() {} ++ +-- @moveUpOneDir/six/four/four.go -- +@@ -1,4 +0,0 @@ +-package four //@ rename("four", "golang.org/lsptests/rename/five", moveUpOneDir), renameerr("four", "../relative", re"specifying relative paths in package rename not yet supported") +- +-func Bar() {} +- +-- @moveUpOneDir/seven/seven/seven.go -- +@@ -5 +5 @@ +- "golang.org/lsptests/rename/six/four" ++ five1 "golang.org/lsptests/rename/five" +@@ -10 +10 @@ +- four.Bar() ++ five1.Bar() -- @newNameSameDir/one/one.go -- @@ -1,2 +0,0 @@ --package one //@ rename("one", "golang.org/lsptests/rename/one", sameName), rename("one", "golang.org/lsptests/rename/two", newNameSameDir), renameerr("one", "golang.org/lsptests/otherdir/one", re"invalid package path") +-package one //@ rename("one", "golang.org/lsptests/rename/one", sameName), rename("one", "golang.org/lsptests/rename/two", newNameSameDir), renameerr("one", "golang.org/lsptests/otherdir/one", re"cannot move package across module boundary") - -- @newNameSameDir/two/one.go -- @@ -0,0 +1,2 @@ -+package two //@ rename("one", "golang.org/lsptests/rename/one", sameName), rename("one", "golang.org/lsptests/rename/two", newNameSameDir), renameerr("one", "golang.org/lsptests/otherdir/one", re"invalid package path") ++package two //@ rename("one", "golang.org/lsptests/rename/one", sameName), rename("one", "golang.org/lsptests/rename/two", newNameSameDir), renameerr("one", "golang.org/lsptests/otherdir/one", re"cannot move package across module boundary") + -- @sameName/one/one.go -- +-- @moveUpOneDir/six/six.go -- +@@ -3 +3 @@ +-import "golang.org/lsptests/rename/six/four" ++import "golang.org/lsptests/rename/five" +@@ -6 +6 @@ +- four.Bar() ++ five.Bar() +-- @moveUpOneDir/go.mod -- +@@ -3 +3 @@ +-replace golang.org/lsptests/six/four => ./six/four ++replace golang.org/lsptests/six/four => ./five +-- @differentDir/seven/eight/eight.go -- +@@ -1 +0,0 @@ +-package eight //@ rename("eight", "golang.org/lsptests/rename/six/eight", differentDir), rename("eight", "golang.org/lsptests/rename/nine/nine/nine/eight", moveDownTwoDirs) +-- @differentDir/six/eight/eight.go -- +@@ -0,0 +1 @@ ++package eight //@ rename("eight", "golang.org/lsptests/rename/six/eight", differentDir), rename("eight", "golang.org/lsptests/rename/nine/nine/nine/eight", moveDownTwoDirs) +-- @differentDir/fix/fix.go -- +@@ -4 +4 @@ +- "golang.org/lsptests/rename/seven/eight" ++ "golang.org/lsptests/rename/six/eight" +-- @moveDownTwoDirs/fix/fix.go -- +@@ -4 +4 @@ +- "golang.org/lsptests/rename/seven/eight" ++ "golang.org/lsptests/rename/nine/nine/nine/eight" +-- @allowedInternal/importsnine/importsnine.go -- +@@ -1,6 +0,0 @@ +-package importsnine //@ rename("importsnine", "golang.org/lsptests/rename/nine/internal/importsnine", allowedInternal) // only importers of this package are already in the internal package, so okay to move it +- +-import ( +- "golang.org/lsptests/rename/nine/nine/nine" +-) +- +-- @allowedInternal/nine/internal/helpers.go -- +@@ -3 +3 @@ +-import "golang.org/lsptests/rename/importsnine" ++import "golang.org/lsptests/rename/nine/internal/importsnine" +-- @allowedInternal/nine/internal/importsnine/importsnine.go -- +@@ -0,0 +1,6 @@ ++package importsnine //@ rename("importsnine", "golang.org/lsptests/rename/nine/internal/importsnine", allowedInternal) // only importers of this package are already in the internal package, so okay to move it ++ ++import ( ++ "golang.org/lsptests/rename/nine/nine/nine" ++) ++ +-- @renameWithTestPkg/fix/fix.go -- +@@ -1,6 +0,0 @@ +-package fix //@ rename("fix", "golang.org/lsptests/rename/newFix", renameWithTestPkg) +- +-import ( +- "golang.org/lsptests/rename/seven/eight" +- ) +- +-- @renameWithTestPkg/fix/fix_test.go -- +@@ -1 +0,0 @@ +-package fix_test +-- @renameWithTestPkg/newFix/fix.go -- +@@ -0,0 +1,6 @@ ++package newFix //@ rename("fix", "golang.org/lsptests/rename/newFix", renameWithTestPkg) ++ ++import ( ++ "golang.org/lsptests/rename/seven/eight" ++ ) ++ +-- @renameWithTestPkg/newFix/fix_test.go -- +@@ -0,0 +1 @@ ++package newFix_test +-- @createNewDir/seven/eight/other/other.go -- +@@ -1,2 +0,0 @@ +-package other //@ rename("other", "golang.org/lsptests/rename/seven/eight/other/newDir1/newDir2", createNewDir) +- +-- @createNewDir/seven/eight/other/newDir1/newDir2/other.go -- +@@ -0,0 +1,2 @@ ++package newDir2 //@ rename("other", "golang.org/lsptests/rename/seven/eight/other/newDir1/newDir2", createNewDir) ++ From 59ff18ce4883962411f85ec9c50abe27ca57108d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 13 Nov 2025 15:05:15 -0500 Subject: [PATCH 05/60] internal/analysis/driverutil: ApplyFix: better "generated" error This CL improves the error message returned by ApplyFix in the case of partial success when some fixes were skipped because they would edit generated files. They are not treated as failed fixes (and re-running will not help), but instead a separate log message is reported that details how many were skipped for this reason. Also, pluralize nouns correctly. For golang/go#75948 For golang/go#71859 Change-Id: Id14d569187e3adf57b7484c21476173e4e2e7c12 Reviewed-on: https://go-review.googlesource.com/c/tools/+/720360 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- .../internal/checker/testdata/conflict.txt | 2 +- .../internal/checker/testdata/generated.txt | 35 +++++++++++++++++++ .../internal/checker/testdata/importdup.txt | 2 +- go/analysis/unitchecker/unitchecker_test.go | 10 ++++-- internal/analysis/driverutil/fix.go | 34 ++++++++++++++---- 5 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 go/analysis/internal/checker/testdata/generated.txt diff --git a/go/analysis/internal/checker/testdata/conflict.txt b/go/analysis/internal/checker/testdata/conflict.txt index c4a4b13b9ab..103c7855a7e 100644 --- a/go/analysis/internal/checker/testdata/conflict.txt +++ b/go/analysis/internal/checker/testdata/conflict.txt @@ -6,7 +6,7 @@ checker -marker -fix example.com/a exit 1 -stderr applied 1 of 3 fixes; 1 files updated...Re-run +stderr applied 1 of 3 fixes; 1 file updated...Re-run -- go.mod -- module example.com diff --git a/go/analysis/internal/checker/testdata/generated.txt b/go/analysis/internal/checker/testdata/generated.txt new file mode 100644 index 00000000000..1f899ab4d43 --- /dev/null +++ b/go/analysis/internal/checker/testdata/generated.txt @@ -0,0 +1,35 @@ +# This test exercises skipping of fixes that edit generated +# files, and the summary logging thereof. + +checker -rename -marker -fix -v example.com/a +stderr skipped 1 fix that would edit generated files +stderr applied 2 fixes, updated 1 file +exit 0 + +-- go.mod -- +module example.com +go 1.22 + +-- a/a.go -- +package a + +var A int //@ fix("A", "A2") + +-- a/gena.go -- +// Code generated by hand. DO NOT EDIT. + +package a + +var B int //@ fix2("B", "B2") + +-- want/a/a.go -- +package a + +var A2 int //@ fix("A", "A2") + +-- want/a/gena.go -- +// Code generated by hand. DO NOT EDIT. + +package a + +var B int //@ fix2("B", "B2") diff --git a/go/analysis/internal/checker/testdata/importdup.txt b/go/analysis/internal/checker/testdata/importdup.txt index 4c144a61221..7d1d9f99a6b 100644 --- a/go/analysis/internal/checker/testdata/importdup.txt +++ b/go/analysis/internal/checker/testdata/importdup.txt @@ -2,7 +2,7 @@ # identical insertions--are coalesced. checker -marker -fix -v example.com/a -stderr applied 2 fixes, updated 1 files +stderr applied 2 fixes, updated 1 file exit 0 -- go.mod -- diff --git a/go/analysis/unitchecker/unitchecker_test.go b/go/analysis/unitchecker/unitchecker_test.go index a6902597b7e..a22aea09ce6 100644 --- a/go/analysis/unitchecker/unitchecker_test.go +++ b/go/analysis/unitchecker/unitchecker_test.go @@ -256,10 +256,14 @@ var _ = fmt.Sprintf(msg) }) t.Run("d-fix", func(t *testing.T) { testenv.NeedsGo1Point(t, 26) - code, _, stderr := vet(t, "-fix", "golang.org/fake/d") - exitcode(t, code, 1) + code, _, stderr := vet(t, "-fix", "-v", "golang.org/fake/d") + exitcode(t, code, 0) contains(t, "d/d.go", `fmt.Sprintf("%s", msg)`) // fixed contains(t, "d/dgen.go", `fmt.Sprintf(msg)`) // fix not applied to generated file - substring(t, "stderr", stderr, "applied 1 of 2 fixes; 1 files updated") + // TODO(adonovan): plumb -v from go vet/fix down to unitchecker. + if false { + substring(t, "stderr", stderr, "skipped 1 fix that would edit generated files") + substring(t, "stderr", stderr, "applied 1 of 2 fixes; 1 files updated") + } }) } diff --git a/internal/analysis/driverutil/fix.go b/internal/analysis/driverutil/fix.go index 763650c7419..ef06cf9bde2 100644 --- a/internal/analysis/driverutil/fix.go +++ b/internal/analysis/driverutil/fix.go @@ -160,7 +160,9 @@ func ApplyFixes(actions []FixAction, printDiff, verbose bool) error { var ( accumulatedEdits = make(map[string][]diff.Edit) filePkgs = make(map[string]*types.Package) // maps each file to an arbitrary package that includes it - goodFixes = 0 + + goodFixes = 0 // number of fixes cleanly applied + skippedFixes = 0 // number of fixes skipped (because e.g. edits a generated file) ) fixloop: for _, fixact := range fixes { @@ -168,6 +170,7 @@ fixloop: for _, edit := range fixact.fix.TextEdits { file := fixact.act.FileSet.File(edit.Pos) if generated[file] { + skippedFixes++ continue fixloop } } @@ -227,7 +230,7 @@ fixloop: log.Printf("%s: fix %s applied", fixact.act.Name, fixact.fix.Message) } } - badFixes := len(fixes) - goodFixes + badFixes := len(fixes) - goodFixes - skippedFixes // number of fixes that could not be applied // Show diff or update files to final state. var files []string @@ -305,15 +308,25 @@ fixloop: // TODO(adonovan): should we log that n files were updated in case of total victory? if badFixes > 0 || filesUpdated < totalFiles { if printDiff { - return fmt.Errorf("%d of %d fixes skipped (e.g. due to conflicts)", badFixes, len(fixes)) + return fmt.Errorf("%d of %s skipped (e.g. due to conflicts)", + badFixes, + plural(len(fixes), "fix", "fixes")) } else { - return fmt.Errorf("applied %d of %d fixes; %d files updated. (Re-run the command to apply more.)", - goodFixes, len(fixes), filesUpdated) + return fmt.Errorf("applied %d of %s; %s updated. (Re-run the command to apply more.)", + goodFixes, + plural(len(fixes), "fix", "fixes"), + plural(filesUpdated, "file", "files")) } } if verbose { - log.Printf("applied %d fixes, updated %d files", len(fixes), filesUpdated) + if skippedFixes > 0 { + log.Printf("skipped %s that would edit generated files", + plural(skippedFixes, "fix", "fixes")) + } + log.Printf("applied %s, updated %s", + plural(len(fixes), "fix", "fixes"), + plural(filesUpdated, "file", "files")) } return nil @@ -425,3 +438,12 @@ func removeUnneededImports(fset *token.FileSet, pkg *types.Package, file *ast.Fi del() } } + +// plural returns "n nouns", selecting the plural form as approriate. +func plural(n int, singular, plural string) string { + if n == 1 { + return "1 " + singular + } else { + return fmt.Sprintf("%d %s", n, plural) + } +} From 41c94d8a4cc2f4b2635239facd0d84733a0e520d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 13 Nov 2025 18:58:03 -0500 Subject: [PATCH 06/60] internal/refactor/inline: avoid expensive Info.Uses scan This CL further optimizes (beyond the attempt in CL 717540) the computation of whether a given PkgName is used only once. The previous attempt reduced the scan of Info.Uses from once per import to once per inline operation, but if a //go:fix inline-annotated function is called a lot, this is still a lot of work. The Caller struct now allows a client to provide an optimized implementation of the CountUses query. The inline analyzer uses the pre-computed typeindex.Index to make it essentially free. The gopls inline code action does not provide it, so it falls back to the slow implementation, but that should be ok since it's only a single call site and the operation is explicitly invoked. The "inline all" operation deals with multiple callsites, but again it is explicitly invoked. The key issue here is the cost of always-on background analysis, not user-initiated code actions. (I noticed from the profile that AST printing is also a significant cost. I wonder if we should make this analyzer use gopls' lazy-edits mechanism, at least when it is running within gopls, to defer these costs. Or, better, stop formatting and reparsing so much when computing edits.) Fixes golang/go#75773 Change-Id: I9a6f3275c524cc3841d5f1b3f8016509bacf7efe Reviewed-on: https://go-review.googlesource.com/c/tools/+/720440 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Auto-Submit: Alan Donovan --- go/analysis/passes/inline/inline.go | 16 +++++++++--- gopls/internal/golang/inline.go | 13 +++++----- gopls/internal/golang/inline_all.go | 13 +++++----- internal/moreiters/iters.go | 8 ++++++ internal/refactor/inline/inline.go | 40 ++++++++++++++--------------- 5 files changed, 54 insertions(+), 36 deletions(-) diff --git a/go/analysis/passes/inline/inline.go b/go/analysis/passes/inline/inline.go index 1b3cb108c6d..402d7e86bb2 100644 --- a/go/analysis/passes/inline/inline.go +++ b/go/analysis/passes/inline/inline.go @@ -21,6 +21,7 @@ import ( "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/diff" "golang.org/x/tools/internal/moreiters" @@ -28,6 +29,7 @@ import ( "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor/inline" "golang.org/x/tools/internal/typesinternal" + "golang.org/x/tools/internal/typesinternal/typeindex" ) //go:embed doc.go @@ -43,7 +45,10 @@ var Analyzer = &analysis.Analyzer{ (*goFixInlineConstFact)(nil), (*goFixInlineAliasFact)(nil), }, - Requires: []*analysis.Analyzer{inspect.Analyzer}, + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + typeindexanalyzer.Analyzer, + }, } var allowBindingDecl bool @@ -55,8 +60,9 @@ func init() { // analyzer holds the state for this analysis. type analyzer struct { - pass *analysis.Pass - root inspector.Cursor + pass *analysis.Pass + root inspector.Cursor + index *typeindex.Index // memoization of repeated calls for same file. fileContent map[string][]byte // memoization of fact imports (nil => no fact) @@ -69,6 +75,7 @@ func run(pass *analysis.Pass) (any, error) { a := &analyzer{ pass: pass, root: pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Root(), + index: pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index), fileContent: make(map[string][]byte), inlinableFuncs: make(map[*types.Func]*inline.Callee), inlinableConsts: make(map[*types.Const]*goFixInlineConstFact), @@ -184,6 +191,9 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur inspector.Cursor) { File: curFile, Call: call, Content: content, + CountUses: func(pkgname *types.PkgName) int { + return moreiters.Len(a.index.Uses(pkgname)) + }, } res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard}) if err != nil { diff --git a/gopls/internal/golang/inline.go b/gopls/internal/golang/inline.go index 7cde0318522..e1a3a4331e6 100644 --- a/gopls/internal/golang/inline.go +++ b/gopls/internal/golang/inline.go @@ -110,12 +110,13 @@ func inlineCall(ctx context.Context, snapshot *cache.Snapshot, callerPkg *cache. // Inline the call. caller := &inline.Caller{ - Fset: callerPkg.FileSet(), - Types: callerPkg.Types(), - Info: callerPkg.TypesInfo(), - File: callerPGF.File, - Call: call, - Content: callerPGF.Src, + Fset: callerPkg.FileSet(), + Types: callerPkg.Types(), + Info: callerPkg.TypesInfo(), + File: callerPGF.File, + Call: call, + Content: callerPGF.Src, + CountUses: nil, // (use inefficient default implementation) } res, err := inline.Inline(caller, callee, &inline.Options{Logf: logf}) diff --git a/gopls/internal/golang/inline_all.go b/gopls/internal/golang/inline_all.go index 2ac34acfb2a..73e3504a76e 100644 --- a/gopls/internal/golang/inline_all.go +++ b/gopls/internal/golang/inline_all.go @@ -224,12 +224,13 @@ func inlineAllCalls(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pa currentCall := 0 for currentCall < len(calls) { caller := &inline.Caller{ - Fset: fset, - Types: tpkg, - Info: tinfo, - File: file, - Call: calls[currentCall], - Content: content, + Fset: fset, + Types: tpkg, + Info: tinfo, + File: file, + Call: calls[currentCall], + Content: content, + CountUses: nil, // TODO(adonovan): opt: amortize across callInfo.pkg } res, err := inline.Inline(caller, callee, opts) if err != nil { diff --git a/internal/moreiters/iters.go b/internal/moreiters/iters.go index 69c76ccb9b6..9e4aaf94855 100644 --- a/internal/moreiters/iters.go +++ b/internal/moreiters/iters.go @@ -45,3 +45,11 @@ func Any[T any](seq iter.Seq[T], pred func(T) bool) bool { } return false } + +// Len returns the number of elements in the sequence (by iterating). +func Len[T any](seq iter.Seq[T]) (n int) { + for range seq { + n++ + } + return +} diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 03ef0714e00..73c46e8a481 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -42,6 +42,10 @@ type Caller struct { Call *ast.CallExpr Content []byte // source of file containing + // CountUses is an optional optimized computation of + // the number of times pkgname appears in Info.Uses. + CountUses func(pkgname *types.PkgName) int + path []ast.Node // path from call to root of file syntax tree enclosingFunc *ast.FuncDecl // top-level function/method enclosing the call, if any } @@ -432,27 +436,19 @@ func newImportState(logf func(string, ...any), caller *Caller, callee *gobCallee importMap: make(map[string][]string), } - // Build an index of used-once PkgNames. - type pkgNameUse struct { - count int - id *ast.Ident // an arbitrary use - } - pkgNameUses := make(map[*types.PkgName]pkgNameUse) - for id, obj := range caller.Info.Uses { - if pkgname, ok := obj.(*types.PkgName); ok { - u := pkgNameUses[pkgname] - u.id = id - u.count++ - pkgNameUses[pkgname] = u + // Provide an inefficient default implementation of CountUses. + // (Ideally clients amortize this for the entire package.) + countUses := caller.CountUses + if countUses == nil { + uses := make(map[*types.PkgName]int) + for _, obj := range caller.Info.Uses { + if pkgname, ok := obj.(*types.PkgName); ok { + uses[pkgname]++ + } } - } - // soleUse returns the ident that refers to pkgname, if there is exactly one. - soleUse := func(pkgname *types.PkgName) *ast.Ident { - u := pkgNameUses[pkgname] - if u.count == 1 { - return u.id + countUses = func(pkgname *types.PkgName) int { + return uses[pkgname] } - return nil } for _, imp := range caller.File.Imports { @@ -472,8 +468,10 @@ func newImportState(logf func(string, ...any), caller *Caller, callee *gobCallee // If that is the case, proactively check if any of the callee FreeObjs // need this import. Doing so eagerly simplifies the resulting logic. needed := true - sel, ok := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr) - if ok && soleUse(pkgName) == sel.X { + if sel, ok := ast.Unparen(caller.Call.Fun).(*ast.SelectorExpr); ok && + is[*ast.Ident](sel.X) && + caller.Info.Uses[sel.X.(*ast.Ident)] == pkgName && + countUses(pkgName) == 1 { needed = false // no longer needed by caller // Check to see if any of the inlined free objects need this package. for _, obj := range callee.FreeObjs { From 53f41002c059fbe900ade432e43559cb9a7135d0 Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Fri, 14 Nov 2025 12:41:09 -0500 Subject: [PATCH 07/60] gopls/doc: document Rename feature Adds documentation describing the behavior of renaming and moving a package. Fixes golang/go#69624 Change-Id: If3b6ae8459db84a7039c845cd15d9633ddca7d15 Reviewed-on: https://go-review.googlesource.com/c/tools/+/720560 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- gopls/doc/features/transformation.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/gopls/doc/features/transformation.md b/gopls/doc/features/transformation.md index b1a895bbf16..c5181a914bf 100644 --- a/gopls/doc/features/transformation.md +++ b/gopls/doc/features/transformation.md @@ -339,8 +339,19 @@ Special cases: Rename here to affect all methods ``` -- Renaming a package declaration additionally causes the package's - directory to be renamed. +Using Rename to move a package: +- When initiating a rename request on a package identifier, users can specify +a new package name which will result in the package's contents moving to a new +directory at the same directory level. Subpackages are not affected. +- When the setting "packageMove" is set to true, users are prompted with +the package path instead of the package name but can specify either an arbitrary +package path or a package name. Subpackages are not affected. + +Package moves are rejected if they would break the build. For example: +- Packages cannot move across a module boundary +- Packages cannot be moved into existing packages; gopls does not support package merging +- Packages cannot be moved to internal directories that would make them inaccessible to any of their current importers + Some tips for best results: From 72b42f2e6a1eb1d53e9f2dafd1080c4f7c8bc957 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 17 Nov 2025 13:49:18 -0500 Subject: [PATCH 08/60] go/analysis/passes/inline: add lazy-fix mode for gopls Computation of inliner fixes can be expensive due to its (unnecessary) strategy of formatting and parsing the file repeatedly. Rather than fix that right now, this CL makes the computation of fixes lazy when the analyzer is running within gopls. An undocumented analyzer flag, set by gopls' init, causes the analyzer to return an empty list of edits, which triggers gopls to compute the edits lazily based on the Diagnostic.Category string. As luck would have it, we already have a lazy fix in place for the refactor.inline.call Code Action, so we simply use that name again for the Category. The change broke an MCP test that assumed that the inline analyzer's fixes carry edits. There is a TODO in the MCP code not to rely on this assumption, but rather than do it, I rewrote the test to use the printf analyzer instead. For golang/go#75773 Change-Id: I5388e472e841cb23351fb9e90d8d7721dae3df07 Reviewed-on: https://go-review.googlesource.com/c/tools/+/721180 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/analysis/passes/inline/inline.go | 139 +++++++++++------- gopls/internal/golang/fix.go | 2 +- gopls/internal/settings/analysis.go | 9 +- .../testdata/mcptools/file_diagnostics.txt | 66 ++++----- internal/refactor/inline/inline.go | 14 +- 5 files changed, 132 insertions(+), 98 deletions(-) diff --git a/go/analysis/passes/inline/inline.go b/go/analysis/passes/inline/inline.go index 402d7e86bb2..c0b75202589 100644 --- a/go/analysis/passes/inline/inline.go +++ b/go/analysis/passes/inline/inline.go @@ -51,11 +51,16 @@ var Analyzer = &analysis.Analyzer{ }, } -var allowBindingDecl bool +var ( + allowBindingDecl bool + lazyEdits bool +) func init() { Analyzer.Flags.BoolVar(&allowBindingDecl, "allow_binding_decl", false, "permit inlinings that require a 'var params = args' declaration") + Analyzer.Flags.BoolVar(&lazyEdits, "lazy_edits", false, + "compute edits lazily (only meaningful to gopls driver)") } // analyzer holds the state for this analysis. @@ -177,66 +182,88 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur inspector.Cursor) { return // don't inline a function from within its own test } - // Inline the call. - content, err := a.readFile(call) - if err != nil { - a.pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err) - return - } - curFile := astutil.EnclosingFile(cur) - caller := &inline.Caller{ - Fset: a.pass.Fset, - Types: a.pass.Pkg, - Info: a.pass.TypesInfo, - File: curFile, - Call: call, - Content: content, - CountUses: func(pkgname *types.PkgName) int { - return moreiters.Len(a.index.Uses(pkgname)) - }, - } - res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard}) - if err != nil { - a.pass.Reportf(call.Lparen, "%v", err) - return - } + // Compute the edits. + // + // Ordinarily the analyzer reports a fix containing + // edits. However, the algorithm is somewhat expensive + // (unnecessarily so: see go.dev/issue/75773) so + // to reduce costs in gopls, we omit the edits, + // meaning that gopls must compute them on demand + // (based on the Diagnostic.Category) when they are + // requested via a code action. + // + // This does mean that the following categories of + // caller-dependent obstacles to inlining will be + // reported when the gopls user requests the fix, + // rather than by quietly suppressing the diagnostic: + // - shadowing problems + // - callee imports inaccessible "internal" packages + // - callee refers to nonexported symbols + // - callee uses too-new Go features + // - inlining call from a cgo file + var edits []analysis.TextEdit + if !lazyEdits { + // Inline the call. + content, err := a.readFile(call) + if err != nil { + a.pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err) + return + } + curFile := astutil.EnclosingFile(cur) + caller := &inline.Caller{ + Fset: a.pass.Fset, + Types: a.pass.Pkg, + Info: a.pass.TypesInfo, + File: curFile, + Call: call, + Content: content, + CountUses: func(pkgname *types.PkgName) int { + return moreiters.Len(a.index.Uses(pkgname)) + }, + } + res, err := inline.Inline(caller, callee, &inline.Options{Logf: discard}) + if err != nil { + a.pass.Reportf(call.Lparen, "%v", err) + return + } - if res.Literalized { - // Users are not fond of inlinings that literalize - // f(x) to func() { ... }(), so avoid them. - // - // (Unfortunately the inliner is very timid, - // and often literalizes when it cannot prove that - // reducing the call is safe; the user of this tool - // has no indication of what the problem is.) - return - } - if res.BindingDecl && !allowBindingDecl { - // When applying fix en masse, users are similarly - // unenthusiastic about inlinings that cannot - // entirely eliminate the parameters and - // insert a 'var params = args' declaration. - // The flag allows them to decline such fixes. - return - } - got := res.Content - - // Suggest the "fix". - var textEdits []analysis.TextEdit - for _, edit := range diff.Bytes(content, got) { - textEdits = append(textEdits, analysis.TextEdit{ - Pos: curFile.FileStart + token.Pos(edit.Start), - End: curFile.FileStart + token.Pos(edit.End), - NewText: []byte(edit.New), - }) + if res.Literalized { + // Users are not fond of inlinings that literalize + // f(x) to func() { ... }(), so avoid them. + // + // (Unfortunately the inliner is very timid, + // and often literalizes when it cannot prove that + // reducing the call is safe; the user of this tool + // has no indication of what the problem is.) + return + } + if res.BindingDecl && !allowBindingDecl { + // When applying fix en masse, users are similarly + // unenthusiastic about inlinings that cannot + // entirely eliminate the parameters and + // insert a 'var params = args' declaration. + // The flag allows them to decline such fixes. + return + } + got := res.Content + + for _, edit := range diff.Bytes(content, got) { + edits = append(edits, analysis.TextEdit{ + Pos: curFile.FileStart + token.Pos(edit.Start), + End: curFile.FileStart + token.Pos(edit.End), + NewText: []byte(edit.New), + }) + } } + a.pass.Report(analysis.Diagnostic{ - Pos: call.Pos(), - End: call.End(), - Message: fmt.Sprintf("Call of %v should be inlined", callee), + Pos: call.Pos(), + End: call.End(), + Message: fmt.Sprintf("Call of %v should be inlined", callee), + Category: "inline_call", // keep consistent with gopls/internal/golang.fixInlineCall SuggestedFixes: []analysis.SuggestedFix{{ Message: fmt.Sprintf("Inline call of %v", callee), - TextEdits: textEdits, + TextEdits: edits, // within gopls, this is nil => compute fix's edits lazily }}, }) } diff --git a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go index 84ff1a91906..74890318b5a 100644 --- a/gopls/internal/golang/fix.go +++ b/gopls/internal/golang/fix.go @@ -54,7 +54,7 @@ const ( fixExtractVariableAll = "extract_variable_all" fixExtractFunction = "extract_function" fixExtractMethod = "extract_method" - fixInlineCall = "inline_call" + fixInlineCall = "inline_call" // keep consistent with go/analysis/passes/inline Diagnostic.Category fixInlineVariable = "inline_variable" fixInvertIfCondition = "invert_if_condition" fixSplitLines = "split_lines" diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go index 4bb15f41c82..c74e458ee6c 100644 --- a/gopls/internal/settings/analysis.go +++ b/gopls/internal/settings/analysis.go @@ -5,6 +5,7 @@ package settings import ( + "log" "slices" "golang.org/x/tools/go/analysis" @@ -241,7 +242,7 @@ var DefaultAnalyzers = []*Analyzer{ severity: protocol.SeverityInformation, }, // other simplifiers - {analyzer: inline.Analyzer, severity: protocol.SeverityHint}, + {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}, @@ -281,3 +282,9 @@ var DefaultAnalyzers = []*Analyzer{ {analyzer: noresultvalues.Analyzer}, {analyzer: unusedvariable.Analyzer}, } + +func init() { + if err := inline.Analyzer.Flags.Set("lazy_edits", "true"); err != nil { + log.Fatalf("setting inline -lazy_edits flag: %v", err) + } +} diff --git a/gopls/internal/test/marker/testdata/mcptools/file_diagnostics.txt b/gopls/internal/test/marker/testdata/mcptools/file_diagnostics.txt index 00642f82753..918a948c555 100644 --- a/gopls/internal/test/marker/testdata/mcptools/file_diagnostics.txt +++ b/gopls/internal/test/marker/testdata/mcptools/file_diagnostics.txt @@ -1,7 +1,13 @@ This test exercises the "go_file_diagnostics" MCP tool. +Some diagnostics from the "printf" analyzer carry fixes, and those fixes have edits. +(Not all fixes have edits; some analyzers rely on gopls to compute them lazily.) + +The printf analyzer only reports a fix in files using >= go1.24. + -- flags -- -mcp +-min_go_command=go1.24 -- settings.json -- { @@ -12,6 +18,7 @@ This test exercises the "go_file_diagnostics" MCP tool. -- go.mod -- module example.com +go 1.24 -- a/main.go -- package main @@ -38,52 +45,33 @@ Fix: -- b/main.go -- package main -func _() { - _ = deprecated([]string{"a"}, "a") //@loc(inline, "deprecated") - - _ = deprecated([]string{"a"}, "a") //@loc(inline2, "deprecated") -} +import "fmt" -//go:fix inline -func deprecated(slice []string, s string) bool { - return proposed(slice, s, true) -} - -func proposed(_ []string, _ string, _ bool) bool { - return false // fake -} +// (diagnostic with fix) +var ( + msg string + _ = fmt.Sprintf(msg) //@ diag("msg", re"non-constant format string") +) -//@mcptool("go_file_diagnostics", `{"file":"$WORKDIR/b/main.go"}`, output=diagnoseInline) -//@diag(inline, re"inline") -//@diag(inline2, re"inline") --- @diagnoseInline -- -3:5-3:35: [Hint] Call of main.deprecated should be inlined -Fix: ---- $WORKDIR/b/main.go -+++ $WORKDIR/b/main.go -@@ -1,7 +1,7 @@ - package main - - func _() { -- _ = deprecated([]string{"a"}, "a") //@loc(inline, "deprecated") -+ _ = proposed([]string{"a"}, "a", true) //@loc(inline, "deprecated") - - _ = deprecated([]string{"a"}, "a") //@loc(inline2, "deprecated") - } +// (diagnostic without fix) +var _ = fmt.Sprintf("%d", "hello") //@ diag("%d", re"%d .* wrong type string") +//@ mcptool("go_file_diagnostics", `{"file":"$WORKDIR/b/main.go"}`, output=diagnosePrintf) -5:5-5:35: [Hint] Call of main.deprecated should be inlined +-- @diagnosePrintf -- +7:17-7:20: [Warning] non-constant format string in call to fmt.Sprintf Fix: --- $WORKDIR/b/main.go +++ $WORKDIR/b/main.go -@@ -3,7 +3,7 @@ - func _() { - _ = deprecated([]string{"a"}, "a") //@loc(inline, "deprecated") - -- _ = deprecated([]string{"a"}, "a") //@loc(inline2, "deprecated") -+ _ = proposed([]string{"a"}, "a", true) //@loc(inline2, "deprecated") - } +@@ -5,7 +5,7 @@ + // (diagnostic with fix) + var ( + msg string +- _ = fmt.Sprintf(msg) //@ diag("msg", re"non-constant format string") ++ _ = fmt.Sprintf("%s", msg) //@ diag("msg", re"non-constant format string") + ) - //go:fix inline + // (diagnostic without fix) +11:21-11:23: [Warning] fmt.Sprintf format %d has arg "hello" of wrong type string diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index 73c46e8a481..af1252cee86 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -40,7 +40,7 @@ type Caller struct { Info *types.Info File *ast.File Call *ast.CallExpr - Content []byte // source of file containing + Content []byte // source of file containing (TODO(adonovan): see comment at Result.Content) // CountUses is an optional optimized computation of // the number of times pkgname appears in Info.Uses. @@ -61,6 +61,18 @@ type Options struct { // Result holds the result of code transformation. type Result struct { + // TODO(adonovan): the only textual results that should be + // needed are (1) an edit in the vicinity of the call (either + // to the CallExpr or one of its ancestors), and optionally + // (2) an edit to the import declaration. + // Change the inliner API to return a list of edits, + // and not to accept a Caller.Content, as it is only + // temptation to use such algorithmically expensive + // operations as reformatting the entire file, which is + // a significant source of non-linear dynamic behavior; + // see https://go.dev/issue/75773. + // This will require a sequence of changes to the tests + // and the inliner algorithm itself. Content []byte // formatted, transformed content of caller file Literalized bool // chosen strategy replaced callee() with func(){...}() BindingDecl bool // transformation added "var params = args" declaration From e58dfd3b5bb8e29f01604d86c21772ec247b8ce4 Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Mon, 17 Nov 2025 12:43:25 -0500 Subject: [PATCH 09/60] gopls/internal/golang: package move: handle mixed build configs When there are files in a metadata.Package that are ignored because they are not in the package with the current build configuration, we should not allow moving that package. Change-Id: I2cc4f078113ce370cdafaee2333ffabc0befd673 Reviewed-on: https://go-review.googlesource.com/c/tools/+/721040 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/rename.go | 2 +- gopls/internal/golang/rename_check.go | 20 +++++++++++++++++++ .../marker/testdata/rename/packagedecl.txt | 7 +++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index 5c1d575c046..d8044824452 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -452,7 +452,7 @@ func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp pro var ( editMap map[protocol.DocumentURI][]diff.Edit - newPkgDir string // declared here so it can be used later to rename the package's enclosing directory + newPkgDir string // declared here so it can be used later to move the package's contents to the new directory ) if inPackageName { countRenamePackage.Inc() diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 2370c6e0a55..94ecc6d56d9 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -927,6 +927,14 @@ func checkPackageRename(opts *settings.Options, curPkg *cache.Package, f file.Ha if newName == curPkg.String() || newName == string(curPkg.Metadata().Name) { return f.URI().DirPath(), curPkg.Metadata().Name, curPkg.Metadata().PkgPath, nil } + + // If there are any Go files in the package that are not in the compiled package + // with the current build config, we should not allow a package move. + ignored := hasIgnoredGoFiles(curPkg) + if ignored { + return "", "", "", fmt.Errorf("moving a package with ignored files is not supported") + } + // TODO(mkalil): support relative paths if build.IsLocalImport(newName) { return "", "", "", fmt.Errorf("specifying relative paths in package rename not yet supported") @@ -981,6 +989,18 @@ func checkPackageRename(opts *settings.Options, curPkg *cache.Package, f file.Ha return newPkgDir, newPkgName, PackagePath(newName), nil } +// hasIgnoredGoFiles returns true if the input pkg contains any Go files that +// are not part of package pkg with the current build configuration. +func hasIgnoredGoFiles(pkg *cache.Package) bool { + // If any ignored files are Go files, don't allow a package move. + for _, f := range pkg.Metadata().IgnoredFiles { + if strings.HasSuffix(f.Path(), ".go") { + return true + } + } + return false +} + // isLocal reports whether obj is local to some function. // Precondition: not a struct field or interface method. func isLocal(obj types.Object) bool { diff --git a/gopls/internal/test/marker/testdata/rename/packagedecl.txt b/gopls/internal/test/marker/testdata/rename/packagedecl.txt index 389bb662544..be200f85393 100644 --- a/gopls/internal/test/marker/testdata/rename/packagedecl.txt +++ b/gopls/internal/test/marker/testdata/rename/packagedecl.txt @@ -102,6 +102,13 @@ import "golang.org/lsptests/rename/nine/nine/nine" -- othermoddir/go.mod -- module mod.com/other +-- foo/windows.go -- +//go:build windows +package foo + +-- foo/default.go -- +//go:build !windows +package foo //@ renameerr("foo", "golang.org/lsptests/rename/foo2", re"ignored files") -- @moveDownTwoDirs/nine/nine/nine/eight/eight.go -- @@ -0,0 +1 @@ From 84fe542995e1d19eebf9933562a5b130a7385ecb Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Tue, 18 Nov 2025 11:16:37 -0500 Subject: [PATCH 10/60] gopls/internal/golang: allow package move into empty directories Previously, we would prevent moving a package if the resulting package directory already exists. Instead, we should allow the move as long as the target directory is empty -- either it doesn't exist or it contains only directory entries. Change-Id: I6590148d9d0fc1a57a8a4b13d39412fc7965f33c Reviewed-on: https://go-review.googlesource.com/c/tools/+/721680 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/golang/rename_check.go | 40 ++++++++++++++---- .../test/integration/misc/rename_test.go | 41 +++++++++++-------- .../marker/testdata/rename/packagedecl.txt | 5 ++- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 94ecc6d56d9..4b19abbc58b 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -33,6 +33,7 @@ package golang // method, not the concrete method. import ( + "errors" "fmt" "go/ast" "go/build" @@ -948,10 +949,12 @@ func checkPackageRename(opts *settings.Options, curPkg *cache.Package, f file.Ha // path, in which case we should not allow renaming. root := filepath.Dir(f.URI().DirPath()) newPkgDir = filepath.Join(root, newName) - _, err := os.Stat(newPkgDir) - if err == nil { - // Directory already exists, return an error. - return "", "", "", fmt.Errorf("invalid package identifier: %q already exists", newName) + empty, err := dirIsEmpty(newPkgDir) + if err != nil { + return "", "", "", err + } + if !empty { + return "", "", "", fmt.Errorf("invalid package identifier: %q is not empty", newName) } parentPkgPath := strings.TrimSuffix(string(curPkg.Metadata().PkgPath), string(curPkg.Metadata().Name)) // leaves a trailing slash newPkgPath = PackagePath(parentPkgPath + newName) @@ -977,10 +980,12 @@ func checkPackageRename(opts *settings.Options, curPkg *cache.Package, f file.Ha return "", "", "", fmt.Errorf("invalid package path %q", newName) } newPkgName = PackageName(filepath.Base(newPkgDir)) - _, err = os.Stat(newPkgDir) - if err == nil { - // Directory or file already exists at this path; return an error. - return "", "", "", fmt.Errorf("invalid package path: %q already exists", newName) + empty, err := dirIsEmpty(newPkgDir) + if err != nil { + return "", "", "", err + } + if !empty { + return "", "", "", fmt.Errorf("invalid package path: %q is not empty", newName) } // Verify that the new package name is a valid identifier. if !isValidIdentifier(string(newPkgName)) { @@ -1001,6 +1006,25 @@ func hasIgnoredGoFiles(pkg *cache.Package) bool { return false } +// dirIsEmpty returns true if the directory does not exist or if the entries in +// dir consist of only directories. +func dirIsEmpty(dir string) (bool, error) { + files, err := os.ReadDir(dir) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return true, nil + } else { + return false, err + } + } + for _, f := range files { + if !f.IsDir() { + return false, nil + } + } + return true, nil +} + // isLocal reports whether obj is local to some function. // Precondition: not a struct field or interface method. func isLocal(obj types.Object) bool { diff --git a/gopls/internal/test/integration/misc/rename_test.go b/gopls/internal/test/integration/misc/rename_test.go index 32a1b4ca92b..44985323abe 100644 --- a/gopls/internal/test/integration/misc/rename_test.go +++ b/gopls/internal/test/integration/misc/rename_test.go @@ -848,7 +848,7 @@ const A = 1 + nested.B for _, badName := range []string{"$$$", "lib_test"} { if err := env.Editor.Rename(env.Ctx, loc, badName); err == nil { - t.Errorf("Rename(lib, libx) succeeded, want non-nil error") + t.Errorf("Rename(lib, libx) succeeded unexpectedly") } } }) @@ -926,17 +926,19 @@ package test2 WithOptions(Settings{"packageMove": true}).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("test1/test1.go") loc := env.RegexpSearch("test1/test1.go", "test1") - badNames := []string{"test2", "mod.com/test2", "differentmod.com/test2", - "mod.com/$$$", "mod*.$+", "mod.commmm"} - expectedErrMatches := []string{"invalid package identifier", "already exists", - "cannot move package across module boundary", "invalid package name", - "invalid package path", "cannot move package across module boundary"} - for i, badName := range badNames { - err := env.Editor.Rename(env.Ctx, loc, badName) + for _, test := range []struct{ badName, expectedErrMatch string }{ + {"test2", "invalid package identifier"}, + {"mod.com/test2", "not empty"}, + {"differentmod.com/test2", "cannot move package across module boundary"}, + {"mod.com/$$$", "invalid package name"}, + {"mod*.$+", "invalid package path"}, + {"mod.commmm", "cannot move package across module boundary"}, + } { + err := env.Editor.Rename(env.Ctx, loc, test.badName) if err == nil { - t.Errorf("Rename to %s succeeded, want non-nil error", badName) - } else if !strings.Contains(err.Error(), expectedErrMatches[i]) { - t.Errorf("Rename to %s produced incorrect error message, got %s, want %s", badName, err.Error(), expectedErrMatches[i]) + t.Errorf("Rename to %s succeeded unexpectedly", test.badName) + } else if !strings.Contains(err.Error(), test.expectedErrMatch) { + t.Errorf("Rename to %s produced incorrect error message, got %s, want %s", test.badName, err.Error(), test.expectedErrMatch) } } }) @@ -958,15 +960,18 @@ package test2 WithOptions(Settings{"packageMove": false}).Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("test1/test1.go") loc := env.RegexpSearch("test1/test1.go", "test1") - badNames := []string{"test2", "mod.com/test3"} - expectedErrMatches := []string{"invalid package identifier", "setting 'packageMove' is not enabled"} - for i, badName := range badNames { - err := env.Editor.Rename(env.Ctx, loc, badName) + + for _, test := range []struct{ badName, expectedErrMatch string }{ + {"test2", "invalid package identifier"}, + {"mod.com/test3", "setting 'packageMove' is not enabled"}, + } { + err := env.Editor.Rename(env.Ctx, loc, test.badName) if err == nil { - t.Errorf("Rename to %s succeeded, want non-nil error", badName) - } else if !strings.Contains(err.Error(), expectedErrMatches[i]) { - t.Errorf("Rename to %s produced incorrect error message, got %s, want %s", badName, err.Error(), expectedErrMatches[i]) + t.Errorf("Rename to %s succeeded unexpectedly", test.badName) + } else if !strings.Contains(err.Error(), test.expectedErrMatch) { + t.Errorf("Rename to %s produced incorrect error message, got %s, want %s", test.badName, err.Error(), test.expectedErrMatch) } + } }) } diff --git a/gopls/internal/test/marker/testdata/rename/packagedecl.txt b/gopls/internal/test/marker/testdata/rename/packagedecl.txt index be200f85393..cd52ebd2e2a 100644 --- a/gopls/internal/test/marker/testdata/rename/packagedecl.txt +++ b/gopls/internal/test/marker/testdata/rename/packagedecl.txt @@ -34,7 +34,7 @@ go 1.20 package one //@ rename("one", "golang.org/lsptests/rename/one", sameName), rename("one", "golang.org/lsptests/rename/two", newNameSameDir), renameerr("one", "golang.org/lsptests/otherdir/one", re"cannot move package across module boundary") -- three/three.go -- -package three //@ renameerr("three", "golang.org/lsptests/othermod/three", re"cannot move package across module boundary"), renameerr("three", "golang.org/lsptests/rename/one", re"already exists") +package three //@ renameerr("three", "golang.org/lsptests/othermod/three", re"cannot move package across module boundary"), renameerr("three", "golang.org/lsptests/rename/one", re"not empty") -- six/six.go -- @@ -91,6 +91,9 @@ import ( -- seven/eight/other/other.go -- package other //@ rename("other", "golang.org/lsptests/rename/seven/eight/other/newDir1/newDir2", createNewDir) +-- seven/eight/other/newDir1/newDir2/newDir3/test.go -- +package newDir3 // verifies that we can move the package above into a directory that already exists but is empty + -- fix/fix_test.go -- package fix_test -- othermoddir/othermod/other.go -- From cd01b4b156a2754f77ff66e492b8be205ff1d586 Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Tue, 18 Nov 2025 10:15:39 -0500 Subject: [PATCH 11/60] gopls/internal/settings: add renameMovesSubpackages setting Removes the packageMove setting - now we always prompt with the full package path during a package rename. The new renameMovesSubpackages setting, false by default, controls whether a package rename also results in moving its subpackages. Change-Id: I191f85cec09e244217fadf820aa7ee60a93063c7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/721660 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/doc/features/transformation.md | 22 ++-- gopls/doc/settings.md | 8 +- gopls/internal/doc/api.json | 4 +- gopls/internal/golang/rename.go | 44 +++----- gopls/internal/golang/rename_check.go | 40 ++++--- gopls/internal/mcp/rename_symbol.go | 2 +- gopls/internal/server/rename.go | 5 +- gopls/internal/settings/default.go | 3 +- gopls/internal/settings/settings.go | 10 +- .../test/integration/misc/rename_test.go | 103 ++++++++++++------ .../marker/testdata/rename/packagedecl.txt | 8 +- .../marker/testdata/rename/prepare_move.txt | 10 +- 12 files changed, 140 insertions(+), 119 deletions(-) diff --git a/gopls/doc/features/transformation.md b/gopls/doc/features/transformation.md index c5181a914bf..5d5a1b836fe 100644 --- a/gopls/doc/features/transformation.md +++ b/gopls/doc/features/transformation.md @@ -340,18 +340,22 @@ Special cases: ``` Using Rename to move a package: -- When initiating a rename request on a package identifier, users can specify -a new package name which will result in the package's contents moving to a new -directory at the same directory level. Subpackages are not affected. -- When the setting "packageMove" is set to true, users are prompted with -the package path instead of the package name but can specify either an arbitrary -package path or a package name. Subpackages are not affected. +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. +The Rename operation will move all the package's files to the resulting directory, +creating it if necessary. +By default, subpackages will remain where they are. To include subpackages +in the renaming, set [renameMovesSubpackages](../settings.md#renamemovessubpackages-bool) to true. +Existing imports of the package will be updated to reflect its new path. Package moves are rejected if they would break the build. For example: -- Packages cannot move across a module boundary -- Packages cannot be moved into existing packages; gopls does not support package merging -- Packages cannot be moved to internal directories that would make them inaccessible to any of their current importers +- Packages cannot move across a module boundary. +- Packages cannot be moved into existing packages; gopls does not support package merging. +- Packages cannot be moved to internal directories that would make them inaccessible to any of their current importers. +Renaming package main is not supported, because the main package has special meaning to the linker. +Renaming x_test packages is currently not supported. Some tips for best results: diff --git a/gopls/doc/settings.md b/gopls/doc/settings.md index a69197eca04..dd8a5d5cea8 100644 --- a/gopls/doc/settings.md +++ b/gopls/doc/settings.md @@ -269,13 +269,13 @@ and package declaration in a newly created Go file. Default: `true`. - -### `packageMove bool` + +### `renameMovesSubpackages bool` **This setting is experimental and may be deleted.** -packageMove enables PrepareRename to send the full package path -and allows users to move a package via renaming. +renameMovesSubpackages enables Rename operations on packages to +move subdirectories of the target package. Default: `false`. diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 66ab0ced9cd..7df2fef656e 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -2189,9 +2189,9 @@ "DeprecationMessage": "" }, { - "Name": "packageMove", + "Name": "renameMovesSubpackages", "Type": "bool", - "Doc": "packageMove enables PrepareRename to send the full package path\nand allows users to move a package via renaming.\n", + "Doc": "renameMovesSubpackages enables Rename operations on packages to\nmove subdirectories of the target package.\n", "EnumKeys": { "ValueType": "", "Keys": null diff --git a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go index d8044824452..c9ca2c5a666 100644 --- a/gopls/internal/golang/rename.go +++ b/gopls/internal/golang/rename.go @@ -53,7 +53,6 @@ import ( "go/types" "maps" "os" - "path" "path/filepath" "regexp" "slices" @@ -199,12 +198,14 @@ func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf if meta.Name == "main" { return nil, fmt.Errorf("can't rename package \"main\"") } + // TODO(mkalil): support renaming x_test packages. if strings.HasSuffix(string(meta.Name), "_test") { return nil, fmt.Errorf("can't rename x_test packages") } if meta.Module == nil { return nil, fmt.Errorf("can't rename package: missing module information for package %q", meta.PkgPath) } + // TODO(mkalil): why is renaming the root package of a module unsupported? if meta.Module.Path == string(meta.PkgPath) { return nil, fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path) } @@ -215,20 +216,9 @@ func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf return nil, err } - pkgName := string(meta.Name) - fullPath := string(meta.PkgPath) - text := pkgName - // Before displaying the full package path, verify that the PackageMove - // setting is enabled and that the package name matches its directory - // basename. Checking the value of meta.Module above ensures that the - // current view is either a GoMod or a GoWork view, which are the only views - // for which we should enable package move. - if snapshot.Options().PackageMove && path.Base(fullPath) == pkgName { - text = fullPath - } return &PrepareItem{ Range: rng, - Text: text, + Text: string(meta.PkgPath), }, nil } @@ -420,7 +410,7 @@ func editsToDocChanges(ctx context.Context, snapshot *cache.Snapshot, edits map[ // Rename returns a map of TextEdits for each file modified when renaming a // given identifier within a package. -func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, newName string, renameSubpkgs bool) ([]protocol.DocumentChange, error) { +func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, newName string) ([]protocol.DocumentChange, error) { ctx, done := event.Start(ctx, "golang.Rename") defer done() @@ -461,10 +451,11 @@ func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp pro newPkgPath PackagePath err error ) - if newPkgDir, newPkgName, newPkgPath, err = checkPackageRename(snapshot.Options(), pkg, f, newName); err != nil { + moveSubpackages := snapshot.Options().RenameMovesSubpackages + if newPkgDir, newPkgName, newPkgPath, err = checkPackageRename(pkg, f, newName, moveSubpackages); err != nil { return nil, err } - editMap, err = renamePackage(ctx, snapshot, f, newPkgName, newPkgPath, newPkgDir, renameSubpkgs) + editMap, err = renamePackage(ctx, snapshot, f, newPkgName, newPkgPath, newPkgDir, moveSubpackages) if err != nil { return nil, err } @@ -524,7 +515,7 @@ func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp pro } if inPackageName { oldDir := f.URI().DirPath() - if renameSubpkgs { + if snapshot.Options().RenameMovesSubpackages { // Update the last component of the file's enclosing directory. changes = append(changes, protocol.DocumentChangeRename( protocol.URIFromPath(oldDir), @@ -962,15 +953,15 @@ func renameExported(pkgs []*cache.Package, declPkgPath PackagePath, declObjPath // // f is the file originating the rename, and therefore f.URI().Dir() is the // current package directory. newName, newPath, and newDir describe the renaming. -func renamePackage(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName, newPath PackagePath, newDir string, renameSubpkgs bool) (map[protocol.DocumentURI][]diff.Edit, error) { +func renamePackage(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName, newPath PackagePath, newDir string, moveSubpackages bool) (map[protocol.DocumentURI][]diff.Edit, error) { // Rename the package decl and all imports. renamingEdits := make(map[protocol.DocumentURI][]diff.Edit) - err := updatePackageDeclsAndImports(ctx, s, f, newName, newPath, renamingEdits, renameSubpkgs) + err := updatePackageDeclsAndImports(ctx, s, f, newName, newPath, renamingEdits, moveSubpackages) if err != nil { return nil, err } - err = updateModFiles(ctx, s, f.URI().DirPath(), newDir, renamingEdits, renameSubpkgs) + err = updateModFiles(ctx, s, f.URI().DirPath(), newDir, renamingEdits, moveSubpackages) if err != nil { return nil, err } @@ -981,7 +972,7 @@ func renamePackage(ctx context.Context, s *cache.Snapshot, f file.Handle, newNam // Update any affected replace directives in go.mod files. // TODO(adonovan): should this operate on all go.mod files, // irrespective of whether they are included in the workspace? -func updateModFiles(ctx context.Context, s *cache.Snapshot, oldDir string, newPkgDir string, renamingEdits map[protocol.DocumentURI][]diff.Edit, renameSubpkgs bool) error { +func updateModFiles(ctx context.Context, s *cache.Snapshot, oldDir string, newPkgDir string, renamingEdits map[protocol.DocumentURI][]diff.Edit, moveSubpackages bool) error { modFiles := s.View().ModFiles() for _, m := range modFiles { fh, err := s.ReadFile(ctx, m) @@ -1008,8 +999,8 @@ func updateModFiles(ctx context.Context, s *cache.Snapshot, oldDir string, newPk // TODO: Is there a risk of converting a '\' delimited replacement to a '/' delimited replacement? - if renameSubpkgs && !strings.HasPrefix(filepath.ToSlash(replacedDir)+"/", filepath.ToSlash(oldDir)+"/") || - !renameSubpkgs && !(filepath.ToSlash(replacedDir) == filepath.ToSlash(oldDir)) { + if moveSubpackages && !strings.HasPrefix(filepath.ToSlash(replacedDir)+"/", filepath.ToSlash(oldDir)+"/") || + !moveSubpackages && !(filepath.ToSlash(replacedDir) == filepath.ToSlash(oldDir)) { continue //not affected by the package renanming } @@ -1068,7 +1059,7 @@ func updateModFiles(ctx context.Context, s *cache.Snapshot, oldDir string, newPk // It updates package clauses and import paths for the renamed package as well // as any other packages affected by the directory renaming among all packages // known to the snapshot. -func updatePackageDeclsAndImports(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName, newPkgPath PackagePath, renamingEdits map[protocol.DocumentURI][]diff.Edit, renameSubpkgs bool) error { +func updatePackageDeclsAndImports(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName, newPkgPath PackagePath, renamingEdits map[protocol.DocumentURI][]diff.Edit, moveSubpackages bool) error { if strings.HasSuffix(string(newName), "_test") { return fmt.Errorf("cannot rename to _test package") } @@ -1112,8 +1103,8 @@ func updatePackageDeclsAndImports(ctx context.Context, s *cache.Snapshot, f file // Subtle: check this condition before checking for valid module info // below, because we should not fail this operation if unrelated packages // lack module info. - if renameSubpkgs && !pathutil.InDir(string(oldPkgPath), string(mp.PkgPath)) || - !renameSubpkgs && mp.PkgPath != oldPkgPath { + if moveSubpackages && !pathutil.InDir(filepath.FromSlash(string(oldPkgPath)), filepath.FromSlash(string(mp.PkgPath))) || + !moveSubpackages && mp.PkgPath != oldPkgPath { continue // not affected by the package renaming } @@ -1138,7 +1129,6 @@ func updatePackageDeclsAndImports(ctx context.Context, s *cache.Snapshot, f file return err } } - imp := ImportPath(newImportPath) // TODO(adonovan): what if newImportPath has vendor/ prefix? if err := renameImports(ctx, s, mp, imp, pkgName, renamingEdits); err != nil { return err diff --git a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go index 4b19abbc58b..a4a42d3e747 100644 --- a/gopls/internal/golang/rename_check.go +++ b/gopls/internal/golang/rename_check.go @@ -51,7 +51,6 @@ import ( "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/file" - "golang.org/x/tools/gopls/internal/settings" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" @@ -923,7 +922,7 @@ func isValidIdentifier(id string) bool { // resulting from renaming the current package to newName, which may be an // identifier or a package path. An error is returned if the renaming is // invalid. -func checkPackageRename(opts *settings.Options, curPkg *cache.Package, f file.Handle, newName string) (newPkgDir string, newPkgName PackageName, newPkgPath PackagePath, err error) { +func checkPackageRename(curPkg *cache.Package, f file.Handle, newName string, moveSubpackages bool) (newPkgDir string, newPkgName PackageName, newPkgPath PackagePath, err error) { // Path unchanged if newName == curPkg.String() || newName == string(curPkg.Metadata().Name) { return f.URI().DirPath(), curPkg.Metadata().Name, curPkg.Metadata().PkgPath, nil @@ -949,21 +948,20 @@ func checkPackageRename(opts *settings.Options, curPkg *cache.Package, f file.Ha // path, in which case we should not allow renaming. root := filepath.Dir(f.URI().DirPath()) newPkgDir = filepath.Join(root, newName) - empty, err := dirIsEmpty(newPkgDir) + exists, empty, err := dirIsEmpty(newPkgDir) if err != nil { return "", "", "", err } - if !empty { - return "", "", "", fmt.Errorf("invalid package identifier: %q is not empty", newName) + // When rename moves subpackages, we merely change the directory name, which cannot + // happen if the target directory already exists. + if moveSubpackages && exists || !empty { + return "", "", "", fmt.Errorf("invalid package identifier: %q is not empty", newPkgDir) } parentPkgPath := strings.TrimSuffix(string(curPkg.Metadata().PkgPath), string(curPkg.Metadata().Name)) // leaves a trailing slash newPkgPath = PackagePath(parentPkgPath + newName) newPkgName = PackageName(newName) return newPkgDir, newPkgName, newPkgPath, nil } - if !opts.PackageMove { - return "", "", "", fmt.Errorf("a full package path is specified but internal setting 'packageMove' is not enabled") - } // Don't allow moving packages across module boundaries. curModPath := curPkg.Metadata().Module.Path if !strings.HasPrefix(newName+"/", curModPath+"/") { @@ -980,17 +978,22 @@ func checkPackageRename(opts *settings.Options, curPkg *cache.Package, f file.Ha return "", "", "", fmt.Errorf("invalid package path %q", newName) } newPkgName = PackageName(filepath.Base(newPkgDir)) - empty, err := dirIsEmpty(newPkgDir) + exists, empty, err := dirIsEmpty(newPkgDir) if err != nil { return "", "", "", err } - if !empty { - return "", "", "", fmt.Errorf("invalid package path: %q is not empty", newName) + // When rename moves subpackages, we merely change the directory name, which cannot + // happen if the target directory already exists. + if moveSubpackages && exists || !empty { + return "", "", "", fmt.Errorf("invalid package path: %q is not empty", newPkgDir) } // Verify that the new package name is a valid identifier. if !isValidIdentifier(string(newPkgName)) { return "", "", "", fmt.Errorf("invalid package name %q", newPkgName) } + if f.URI().Dir().Base() != string(curPkg.Metadata().Name) { + return "", "", "", fmt.Errorf("can't move package: package name %q does not match directory base name %q", string(curPkg.Metadata().Name), f.URI().Dir().Base()) + } return newPkgDir, newPkgName, PackagePath(newName), nil } @@ -1006,23 +1009,24 @@ func hasIgnoredGoFiles(pkg *cache.Package) bool { return false } -// dirIsEmpty returns true if the directory does not exist or if the entries in -// dir consist of only directories. -func dirIsEmpty(dir string) (bool, error) { +// dirIsEmpty returns two values: whether the directory exists and whether it is +// empty (contains only directory entries). If it does not exist, empty is +// always true. +func dirIsEmpty(dir string) (exists bool, empty bool, err error) { files, err := os.ReadDir(dir) if err != nil { if errors.Is(err, fs.ErrNotExist) { - return true, nil + return false, true, nil } else { - return false, err + return false, false, err } } for _, f := range files { if !f.IsDir() { - return false, nil + return true, false, nil } } - return true, nil + return true, true, nil } // isLocal reports whether obj is local to some function. diff --git a/gopls/internal/mcp/rename_symbol.go b/gopls/internal/mcp/rename_symbol.go index 56297818b0d..e2fce0bfe2d 100644 --- a/gopls/internal/mcp/rename_symbol.go +++ b/gopls/internal/mcp/rename_symbol.go @@ -38,7 +38,7 @@ func (h *handler) renameSymbolHandler(ctx context.Context, req *mcp.CallToolRequ if err != nil { return nil, nil, err } - changes, err := golang.Rename(ctx, snapshot, fh, loc.Range.Start, params.NewName, false) + changes, err := golang.Rename(ctx, snapshot, fh, loc.Range.Start, params.NewName) if err != nil { return nil, nil, err } diff --git a/gopls/internal/server/rename.go b/gopls/internal/server/rename.go index 304fa7f21b9..c7abc4fd77b 100644 --- a/gopls/internal/server/rename.go +++ b/gopls/internal/server/rename.go @@ -30,10 +30,7 @@ func (s *server) Rename(ctx context.Context, params *protocol.RenameParams) (*pr return nil, fmt.Errorf("cannot rename in file of type %s", kind) } - // TODO(mkalil): By default, we don't move subpackages on a package rename. - // When dialog support is enabled, the user will be able to specify whether - // they do want to move subpackages. - changes, err := golang.Rename(ctx, snapshot, fh, params.Position, params.NewName, false) + changes, err := golang.Rename(ctx, snapshot, fh, params.Position, params.NewName) if err != nil { return nil, err } diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index 812194e87b9..514d031e299 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -132,7 +132,8 @@ func DefaultOptions(overrides ...func(*Options)) *Options { CodeLensVendor: true, CodeLensRunGovulncheck: true, }, - NewGoFileHeader: true, + NewGoFileHeader: true, + RenameMovesSubpackages: false, }, }, InternalOptions: InternalOptions{ diff --git a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go index a78adb61193..17a3e5a2ffe 100644 --- a/gopls/internal/settings/settings.go +++ b/gopls/internal/settings/settings.go @@ -251,9 +251,9 @@ type UIOptions struct { // and package declaration in a newly created Go file. NewGoFileHeader bool - // PackageMove enables PrepareRename to send the full package path - // and allows users to move a package via renaming. - PackageMove bool `status:"experimental"` + // RenameMovesSubpackages enables Rename operations on packages to + // move subdirectories of the target package. + RenameMovesSubpackages bool `status:"experimental"` } // A CodeLensSource identifies an (algorithmic) source of code lenses. @@ -1368,8 +1368,8 @@ func (o *Options) setOne(name string, value any) (applied []CounterPath, _ error case "mcpTools": return setBoolMap(&o.MCPTools, value) - case "packageMove": - return setBool(&o.PackageMove, value) + case "renameMovesSubpackages": + return setBool(&o.RenameMovesSubpackages, value) // deprecated and renamed settings // diff --git a/gopls/internal/test/integration/misc/rename_test.go b/gopls/internal/test/integration/misc/rename_test.go index 44985323abe..6afa1839f5b 100644 --- a/gopls/internal/test/integration/misc/rename_test.go +++ b/gopls/internal/test/integration/misc/rename_test.go @@ -181,15 +181,26 @@ func main() { env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") // Check if the new package name exists. - /// Don't rename subpackages. - // TODO(mkalil): update the test framework to handle variable input - // for renameSubpkgs. + // Don't rename subpackages. env.RegexpSearch("nested/a.go", "package nested") env.RegexpSearch("main.go", `nested2 "mod.com/nested"`) env.RegexpSearch("main.go", "mod.com/nested") env.RegexpSearch("main.go", "mod.com/lib/nested") env.RegexpSearch("main.go", `nested1 "mod.com/lib/x"`) }) + + WithOptions(Settings{"renameMovesSubpackages": true}).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") + + // Check if the new package name exists. + // Rename subpackages. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", `nested2 "mod.com/nested"`) + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", "mod.com/nested/nested") + env.RegexpSearch("main.go", `nested1 "mod.com/nested/x"`) + }) } func TestRenamePackageWithAlias(t *testing.T) { @@ -269,6 +280,17 @@ func main() { // Don't rename subpackages. env.RegexpSearch("main.go", `foo "mod.com/lib/nested"`) }) + + WithOptions(Settings{"renameMovesSubpackages": true}).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + // Rename subpackages. + env.RegexpSearch("main.go", `foo "mod.com/nested/nested"`) + }) } func TestRenamePackage(t *testing.T) { @@ -570,6 +592,23 @@ func main() { env.RegexpSearch("go.mod", "./foo/bar") env.RegexpSearch("go.mod", "./foo/baz") }) + + WithOptions(Settings{"renameMovesSubpackages": true}).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("foo/foo.go") + env.Rename(env.RegexpSearch("foo/foo.go", "foo"), "foox") + + env.RegexpSearch("foox/foo.go", "package foox") + // Rename subpackages. + env.OpenFile("foox/bar/bar.go") + env.OpenFile("foox/bar/go.mod") + + env.RegexpSearch("main.go", "mod.com/foo/bar") + env.RegexpSearch("main.go", "mod.com/foox") + env.RegexpSearch("main.go", "foox.Bar()") + + env.RegexpSearch("go.mod", "./foox/bar") + env.RegexpSearch("go.mod", "./foox/baz") + }) } func TestRenamePackage_DuplicateImport(t *testing.T) { @@ -612,6 +651,18 @@ func main() { // Don't rename subpackages. env.RegexpSearch("main.go", `lib2 "mod.com/lib/nested"`) }) + + WithOptions(Settings{"renameMovesSubpackages": true}).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `lib1 "mod.com/nested"`) + // Rename subpackages. + env.RegexpSearch("main.go", `lib2 "mod.com/nested/nested"`) + }) } func TestRenamePackage_DuplicateBlankImport(t *testing.T) { @@ -654,6 +705,18 @@ func main() { // Don't rename subpackages. env.RegexpSearch("main.go", `lib1 "mod.com/lib/nested"`) }) + + WithOptions(Settings{"renameMovesSubpackages": true}).Run(t, files, func(t *testing.T, env *Env) { + env.OpenFile("lib/a.go") + env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") + + // Check if the new package name exists. + env.RegexpSearch("nested/a.go", "package nested") + env.RegexpSearch("main.go", "mod.com/nested") + env.RegexpSearch("main.go", `_ "mod.com/nested"`) + // Rename subpackages. + env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) + }) } func TestRenamePackage_TestVariant(t *testing.T) { @@ -923,7 +986,7 @@ package test1 package test2 ` - WithOptions(Settings{"packageMove": true}).Run(t, files, func(t *testing.T, env *Env) { + Run(t, files, func(t *testing.T, env *Env) { env.OpenFile("test1/test1.go") loc := env.RegexpSearch("test1/test1.go", "test1") for _, test := range []struct{ badName, expectedErrMatch string }{ @@ -944,38 +1007,6 @@ package test2 }) } -func TestRenamePackage_PackageMoveDisabled(t *testing.T) { - const files = ` --- go.mod -- -module mod.com - -go 1.21 --- test1/test1.go -- -package test1 - --- test2/test2.go -- -package test2 - -` - WithOptions(Settings{"packageMove": false}).Run(t, files, func(t *testing.T, env *Env) { - env.OpenFile("test1/test1.go") - loc := env.RegexpSearch("test1/test1.go", "test1") - - for _, test := range []struct{ badName, expectedErrMatch string }{ - {"test2", "invalid package identifier"}, - {"mod.com/test3", "setting 'packageMove' is not enabled"}, - } { - err := env.Editor.Rename(env.Ctx, loc, test.badName) - if err == nil { - t.Errorf("Rename to %s succeeded unexpectedly", test.badName) - } else if !strings.Contains(err.Error(), test.expectedErrMatch) { - t.Errorf("Rename to %s produced incorrect error message, got %s, want %s", test.badName, err.Error(), test.expectedErrMatch) - } - - } - }) -} - // checkTestdata checks that current buffer contents match their corresponding // expected content in the testdata directory. func checkTestdata(t *testing.T, env *Env) { diff --git a/gopls/internal/test/marker/testdata/rename/packagedecl.txt b/gopls/internal/test/marker/testdata/rename/packagedecl.txt index cd52ebd2e2a..cbf6a3708a9 100644 --- a/gopls/internal/test/marker/testdata/rename/packagedecl.txt +++ b/gopls/internal/test/marker/testdata/rename/packagedecl.txt @@ -12,11 +12,6 @@ It tests the following: - Impact of renaming on _test packages - Renaming with the exact same path - no edits produced --- settings.json -- -{ - "packageMove": true -} - -- flags -- -ignore_extra_diags @@ -113,6 +108,9 @@ package foo //go:build !windows package foo //@ renameerr("foo", "golang.org/lsptests/rename/foo2", re"ignored files") +-- bar/bar.go -- +package foobar //@ renameerr("foobar", "golang.org/lsptests/rename/otherpath", re"does not match directory base name") + -- @moveDownTwoDirs/nine/nine/nine/eight/eight.go -- @@ -0,0 +1 @@ +package eight //@ rename("eight", "golang.org/lsptests/rename/six/eight", differentDir), rename("eight", "golang.org/lsptests/rename/nine/nine/nine/eight", moveDownTwoDirs) diff --git a/gopls/internal/test/marker/testdata/rename/prepare_move.txt b/gopls/internal/test/marker/testdata/rename/prepare_move.txt index 283f754e8ea..c0a5b7f7126 100644 --- a/gopls/internal/test/marker/testdata/rename/prepare_move.txt +++ b/gopls/internal/test/marker/testdata/rename/prepare_move.txt @@ -1,9 +1,4 @@ -This test verifies the behavior of textDocument/prepareRename when the experimental package move setting is enabled. - --- settings.json -- -{ - "packageMove": true -} +This test verifies the behavior of textDocument/prepareRename - we prompt with the full package path. -- go.mod -- module golang.org/lsptests @@ -14,4 +9,5 @@ go 1.20 package b //@ preparerename("b", "golang.org/lsptests/b") -- a/other.go -- -package other //@ preparerename("other", "other") // package move disabled when the package name does not match its directory base name +// We prompt with the full package path, but changing a package directory is disabled when the package name does not match its directory base name. +package other //@ preparerename("other", "golang.org/lsptests/a") From ce5ff8a929ae11e8dc2215880c9a869b9a0ac0a8 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 30 Oct 2025 11:25:25 -0400 Subject: [PATCH 12/60] gopls/doc/release/v0.21.0.md: prepare Change-Id: Id84fa1a26bb228c634cb85dcaa70246c45bc1a48 Reviewed-on: https://go-review.googlesource.com/c/tools/+/716461 Reviewed-by: Robert Findley Reviewed-by: Ethan Lee Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Hongxiang Jiang --- gopls/doc/release/v0.21.0.md | 160 ++++++++++++++++++++++++++++++----- gopls/doc/release/v0.22.0.md | 20 +++++ 2 files changed, 161 insertions(+), 19 deletions(-) create mode 100644 gopls/doc/release/v0.22.0.md diff --git a/gopls/doc/release/v0.21.0.md b/gopls/doc/release/v0.21.0.md index 2403b9f1846..00c6de81a8a 100644 --- a/gopls/doc/release/v0.21.0.md +++ b/gopls/doc/release/v0.21.0.md @@ -1,5 +1,5 @@ --- -title: "Gopls release v0.21.0 (forthcoming)" +title: "Gopls release v0.21.0 (expected Nov 2025)" --- ## Configuration changes @@ -7,46 +7,92 @@ title: "Gopls release v0.21.0 (forthcoming)" - The new `newGoFileHeader` option allows toggling automatic insertion of the copyright comment and package declaration in a newly created Go file. -## Web-based features -## Editing features +- The default value of the `codelenses` setting now includes + `run_govulncheck: true`, causing gopls to offer a "Run govulncheck" + command associated with the `module` declaration in a go.mod file. + +- Also, the experimental `vulncheck` setting now supports the enum value + `"Prompt"`, meaning that vulncheck can be triggered via a prompt; + this is now the default. + +- The new, experimental `renameMovesSubpackages` setting determines + whether a rename package operation applies to subdirectories; see + below for details. + +## Navigational features: + +Hover can now report information about selected subexpressions (#69058). +For example, selecting the region indicated below: + + x[i].f() + ~~~~ + +will report information about the subexpression `x[i]`, such as its +type, constant value (if appropriate), and methods. +Previously, Hover would report only about `x`, `i`, or `f`. + +This feature requires the client to supply an extra field in the +`textDocumentPositionParams` part of its LSP `textDocument/hover` +request: the standard struct has a single `position` of type +`Position`, but gopls can also handle a `range` field of type +`Range` that specifies the extent of the text selection. +(If supplied, it must enclose the `position`.) + +The VS Code code extension already populates this field. We plan to +use it across various requests, and to lobby for its adoption by the +LSP standard. + +Other features in brief: + +- Definition now works for fields in doc links in comments (#75038). +- Hover adds navigation to doc links in hovers (golang/vscode-go#3827). +- Hover now reports information (size, etc) about embedded fields, not just their types (#75975). +- Folding Range now displays closing parens (#75229) and more ranges (#73735). + ## Analysis features ### `reflecttypefor` analyzer -The new `reflecttypefor` modernizer simplifies calls to -`reflect.TypeOf` to use `reflect.TypeFor` when the runtime type is -known at compile time. For example, `reflect.TypeOf(uint32(0))` -becomes `reflect.TypeFor[uint32]()`. +The new `reflecttypefor` analyzer modernizes calls to `reflect.TypeOf` +to use `reflect.TypeFor` when the runtime type is known at compile +time. For example: + + reflect.TypeOf(uint32(0)) + +becomes: + + reflect.TypeFor[uint32]() ### `newexpr` analyzer -The `newexpr` modernizer finds declarations of and calls to functions +The `newexpr` analyzer finds declarations of and calls to functions of this form: ```go func varOf(x int) *int { return &x } use(varOf(123)) ``` -so that they are transformed to: +modernizing them when appropriate to use the `new(expr)` feature of Go 1.26: ```go //go:fix inline func varOf(x int) *int { return new(x) } use(new(123)) ``` -(Such wrapper functions are widely used in serialization packages, + +Such wrapper functions are widely used in serialization packages, for instance the proto.{Int64,String,Bool} helpers used with -protobufs.) +protobufs. -### `iterators` analyzer +### `stditerators` analyzer -The `iterators` modernizer replaces loops of this form, +The `stditerators` analyzer replaces loops of this form, for i := 0; i < x.Len(); i++ { use(x.At(i)) @@ -58,19 +104,95 @@ or their "range x.Len()" equivalent, by use(x.At(i) } -for various types in the standard library that now offer an -iterator-based API. +for various types in the standard library that now offer a +modern iterator-based API. + +### `stringscut` analyzer + + + +The `stringscut` analyzer modernizes various patterns of use of +`strings.Index` such as: + + i := strings.Index(s, sep) + if i >= 0 { + use(s[i:]) + } + +by a call to `strings.Cut`, introduced in Go 1.25: + + before, after, ok := strings.Cut(s, sep) + if ok { + use(after) + } + +### `plusbuild` analyzer + + + +The `plusbuild` analyzer removes old-style `+build` build-tag comments +when they are redundant with the newer form: + + //go:build linux + // +build linux + +### `errorsastype` analyzer + + + +The `errorsastype` analyzer replaces calls to `errors.As`: + + var myErr *MyError + if errors.As(err, &myErr) { ... } + +by the more modern `errors.AsType`, when the type is known statically: + + if myErr, ok := errors.AsType[*MyError](err) { ... } + +### Miscellaneous + +- `minmax` now removes user-defined min/max functions when they are redundant; +- `stringscutprefix` now handles `strings.CutSuffix` too; +- the `yield` ad `nilness` analyzers handle control-flow booleans more precisely; +- `printf` now checks calls to local anonymous printf-wrapper functions too; +- the `staticcheck` suite has been updated to v0.7; ## Code transformation features +### Generalized package renaming + + + +The Rename operation, invoked on a `package p` declaration, now +prompts you to edit the entire package path. This allows you to choose +not just a new name for the package, but a new directory anywhere +within the same module. For example, you can rename `example.com/foo` +to `example.com/internal/bar`, and the tool will move files and update +imports as needed. + +By default, subpackages are no longer moved with the renamed package. +Enable the new (experimental) `renameMovesSubpackages` setting if you +want subpackages to move too. (In due course, we will add LSP support +for dialogs so that the server can query the user's intent +interactively.) + +### Renaming from a doc link + The Rename operation now treats [Doc Links](https://tip.golang.org/doc/comment#doclinks) like identifiers, so you can initiate a renaming from a Doc Link. - diff --git a/gopls/doc/release/v0.22.0.md b/gopls/doc/release/v0.22.0.md new file mode 100644 index 00000000000..5813c213253 --- /dev/null +++ b/gopls/doc/release/v0.22.0.md @@ -0,0 +1,20 @@ +--- +title: "Gopls release v0.22.0 (forthcoming)" +--- + +## Configuration changes +## Web-based features +## Editing features +## Analysis features + +### `foo` analyzer + + + +Description + +## Code transformation features + +### $feature + +https://go.dev/issue#xxxxx From 728dd6ec2fd637d86058347d3829a3d53b6f1b20 Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Wed, 19 Nov 2025 14:49:09 -0800 Subject: [PATCH 13/60] internal/typesinternal/typeindex: adjust test for new spec rule For golang/go#75885. Change-Id: Ic08077abb4bb3ecf91c932f4cf7b76cdb0cf4dfe Reviewed-on: https://go-review.googlesource.com/c/tools/+/722240 Reviewed-by: Robert Griesemer Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Robert Griesemer --- internal/typesinternal/typeindex/typeindex_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/typesinternal/typeindex/typeindex_test.go b/internal/typesinternal/typeindex/typeindex_test.go index 5d489295bfa..55255559a2b 100644 --- a/internal/typesinternal/typeindex/typeindex_test.go +++ b/internal/typesinternal/typeindex/typeindex_test.go @@ -167,12 +167,12 @@ func TestOrigin(t *testing.T) { for _, src := range []string{ ` package alias -type A[T any] = T +type A[T any] = *T var _ A[int]`, ` package localalias func _[T any]() { - type A[U any] = U + type A[U any] = *U var _ A[T] }`, ` From 488d49efc66fbc2db6bfbf28ca0bfab212f3dd28 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 19 Nov 2025 14:27:53 -0500 Subject: [PATCH 14/60] go/packages: populate types.Info maps even for package "unsafe" + test Change-Id: I6de09069aae08f2add50bd9c8f18e71c397e6e66 Reviewed-on: https://go-review.googlesource.com/c/tools/+/721980 LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Matloob Auto-Submit: Alan Donovan Reviewed-by: Michael Matloob --- go/packages/packages.go | 41 ++++++++++++++++++++++-------------- go/packages/packages_test.go | 19 +++++++++++++++++ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/go/packages/packages.go b/go/packages/packages.go index 060ab08efbc..ff607389dac 100644 --- a/go/packages/packages.go +++ b/go/packages/packages.go @@ -1027,11 +1027,15 @@ func (ld *loader) refine(response *DriverResponse) ([]*Package, error) { // Precondition: ld.Mode&(NeedSyntax|NeedTypes|NeedTypesInfo) != 0. func (ld *loader) loadPackage(lpkg *loaderPackage) { if lpkg.PkgPath == "unsafe" { - // Fill in the blanks to avoid surprises. + // To avoid surprises, fill in the blanks consistent + // with other packages. (For example, some analyzers + // assert that each needed types.Info map is non-nil + // even when there is no syntax that would cause them + // to consult the map.) lpkg.Types = types.Unsafe lpkg.Fset = ld.Fset lpkg.Syntax = []*ast.File{} - lpkg.TypesInfo = new(types.Info) + lpkg.TypesInfo = ld.newTypesInfo() lpkg.TypesSizes = ld.sizes return } @@ -1180,20 +1184,7 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { return } - // Populate TypesInfo only if needed, as it - // causes the type checker to work much harder. - if ld.Config.Mode&NeedTypesInfo != 0 { - lpkg.TypesInfo = &types.Info{ - Types: make(map[ast.Expr]types.TypeAndValue), - Defs: make(map[*ast.Ident]types.Object), - Uses: make(map[*ast.Ident]types.Object), - Implicits: make(map[ast.Node]types.Object), - Instances: make(map[*ast.Ident]types.Instance), - Scopes: make(map[ast.Node]*types.Scope), - Selections: make(map[*ast.SelectorExpr]*types.Selection), - FileVersions: make(map[*ast.File]string), - } - } + lpkg.TypesInfo = ld.newTypesInfo() lpkg.TypesSizes = ld.sizes importer := importerFunc(func(path string) (*types.Package, error) { @@ -1307,6 +1298,24 @@ func (ld *loader) loadPackage(lpkg *loaderPackage) { lpkg.IllTyped = illTyped } +func (ld *loader) newTypesInfo() *types.Info { + // Populate TypesInfo only if needed, as it + // causes the type checker to work much harder. + if ld.Config.Mode&NeedTypesInfo == 0 { + return nil + } + return &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Instances: make(map[*ast.Ident]types.Instance), + Scopes: make(map[ast.Node]*types.Scope), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + FileVersions: make(map[*ast.File]string), + } +} + // An importFunc is an implementation of the single-method // types.Importer interface based on a function value. type importerFunc func(path string) (*types.Package, error) diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 4767219b97d..94961cd4057 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -3405,3 +3405,22 @@ func TestLoadFileRelativePath(t *testing.T) { t.Fatalf(`pkgs[0].Id = %q; want "fake/pkg"`, pkgs[0].ID) } } + +func TestLoad_populatesInfoMapsForUnsafePackage(t *testing.T) { + pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadAllSyntax}, "unsafe") + if err != nil { + t.Fatal(err) + } + info := pkgs[0].TypesInfo + + if info.Defs == nil || + info.Uses == nil || + info.Selections == nil || + info.Types == nil || + info.FileVersions == nil || + info.Implicits == nil || + info.Instances == nil || + info.Scopes == nil { + t.Errorf("types.Info for package unsafe has one or more nil maps: %#v", *info) + } +} From e5e22fe7b3642596e635bd66880bd4ce0c2b0a73 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 17 Nov 2025 15:56:20 -0500 Subject: [PATCH 15/60] gopls/internal/debug: add /package debug view I notice that analysis diagnostics disappear sometimes and would like a way to inspect gopls' current internal state. (I had suspected that emacs' tendency to create files called foo.go.~suffix~, for which eglot sends DidOpens, introduces type errors, suppressing most analysis, but the /metadata view doesn't support that.) Change-Id: I9b0371c43d2e018cf4e04784b0a9065e15d3586a Reviewed-on: https://go-review.googlesource.com/c/tools/+/721281 LUCI-TryBot-Result: Go LUCI Reviewed-by: Peter Weinberger --- gopls/internal/debug/serve.go | 118 ++++++++++++++++++++++++-- gopls/internal/debug/template_test.go | 4 +- 2 files changed, 114 insertions(+), 8 deletions(-) diff --git a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go index 93c55c3f6be..cff3eca6be7 100644 --- a/gopls/internal/debug/serve.go +++ b/gopls/internal/debug/serve.go @@ -25,6 +25,7 @@ import ( "time" "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/debug/log" "golang.org/x/tools/gopls/internal/file" label1 "golang.org/x/tools/gopls/internal/label" @@ -327,7 +328,13 @@ func (i *Instance) getFile(r *http.Request) any { return nil } -// /metadata/{session}/{view}. Returns a [*metadata.Graph]. +type MetadataInfo struct { + SessionID string + ViewID string + Graph *metadata.Graph +} + +// /metadata/{session}/{view}. Returns a [*MetadataInfo]. func (i *Instance) getMetadata(r *http.Request) any { session := i.State.Session(r.PathValue("session")) if session == nil { @@ -346,7 +353,64 @@ func (i *Instance) getMetadata(r *http.Request) any { return nil } defer release() - return snapshot.MetadataGraph() + return &MetadataInfo{ + SessionID: session.ID(), + ViewID: v.ID(), + Graph: snapshot.MetadataGraph(), + } +} + +type PackageInfo struct { + SessionID string + ViewID string + Package *cache.Package + Diagnostics map[protocol.DocumentURI][]*cache.Diagnostic +} + +// /package/{session}/{view}/{id...}. Returns a [*PackageInfo]. +func (i *Instance) getPackage(r *http.Request) any { + // TODO(adonovan): shouldn't we report an HTTP error in all + // these early returns? Same for getMetadata. + session := i.State.Session(r.PathValue("session")) + if session == nil { + return nil // not found + } + + v, err := session.View(r.PathValue("view")) + if err != nil { + stdlog.Printf("/package: %v", err) + return nil // not found + } + + id := r.PathValue("id") + + snapshot, release, err := v.Snapshot() + if err != nil { + stdlog.Printf("/package: failed to get latest snapshot: %v", err) + return nil + } + defer release() + + pkgs, err := snapshot.TypeCheck(r.Context(), cache.PackageID(id)) + if err != nil { + stdlog.Printf("/package: failed to typecheck package %q: %v", id, err) + return nil + } + + // (PackageDiagnostics is redundant w.r.t. TypeCheck but it's + // the only way to access type errors in cache.Diagnostic form.) + diags, err := snapshot.PackageDiagnostics(r.Context(), cache.PackageID(id)) + if err != nil { + stdlog.Printf("/package: failed to typecheck package %q: %v", id, err) + return nil + } + + return &PackageInfo{ + SessionID: session.ID(), + ViewID: v.ID(), + Package: pkgs[0], + Diagnostics: diags, + } } func (i *Instance) getInfo(r *http.Request) any { @@ -489,6 +553,7 @@ func (i *Instance) Serve(ctx context.Context, addr string) (string, error) { mux.HandleFunc("/server/", render(ServerTmpl, i.getServer)) mux.HandleFunc("/file/{session}/{identifier}", render(FileTmpl, i.getFile)) mux.HandleFunc("/metadata/{session}/{view}/", render(MetadataTmpl, i.getMetadata)) + mux.HandleFunc("/package/{session}/{view}/{id...}", render(PackageTmpl, i.getPackage)) mux.HandleFunc("/info", render(InfoTmpl, i.getInfo)) mux.HandleFunc("/memory", render(MemoryTmpl, getMemory)) @@ -880,22 +945,23 @@ var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` {{end}} `)) -// For /metadata endpoint; operand is [*metadata.Graph]. +// For /metadata endpoint; operand is [*MetadataInfo]. var MetadataTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` {{define "title"}}Metadata graph{{end}} {{define "body"}}

↓ Index by file

-

Packages ({{len .Packages}})

+

Packages ({{len .Graph.Packages}})

    -{{range $id, $pkg := .Packages}} +{{range $id, $pkg := .Graph.Packages}}
  • {{$id}} {{with $pkg}}
    • Name: {{.Name}}
    • PkgPath: {{printf "%q" .PkgPath}}
    • {{if .Module}}
    • Module: {{printf "%#v" .Module}}
    • {{end}} +
    • Type information
    • {{if .ForTest}}
    • ForTest: {{.ForTest}}
    • {{end}} {{if .Standalone}}
    • Standalone
    • {{end}} {{if .Errors}}
    • Errors: {{.Errors}}
    • {{end}} @@ -921,8 +987,48 @@ var MetadataTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`

      Files

        -{{range $uri, $pkgs := .ForFile}}
      • {{$uri}} →{{range $pkgs}} {{.ID}}{{end}}
      • {{end}} +{{range $uri, $pkgs := .Graph.ForFile}}
      • {{$uri}} →{{range $pkgs}} {{.ID}}{{end}}
      • {{end}} +
      + +{{end}} +`)) + +// For /package endpoint; operand is [*PackageInfo]. +var PackageTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +{{define "title"}}Package {{.Package.Metadata.ID}}{{end}} +{{define "body"}} + + + +

      Diagnostics for syntax and type (but not analysis) errors

      +
        + {{range $url, $diags := .Diagnostics}} +
      • {{$url}} +
          + {{range $diag := $diags}} +
        • {{$diag.Range}}: [{{$diag.Severity}}] {{$diag.Message}}
          +
            +
          • code {{$diag.Code}}
          • +
          • code href {{$diag.CodeHref}}
          • +
          • source {{$diag.Source}}
          • +
          • tags {{$diag.Tags}}
          • +
          • related {{$diag.Related}}
          • {{/*TODO: improve*/}} +
          • bundled fixes {{$diag.BundledFixes}}
          • {{/*TODO: improve*/}} +
          • fixes {{$diag.SuggestedFixes}}
          • {{/*TODO: improve*/}} +
          + {{end}} +
        + {{end}}
      {{end}} +{{/* +TODO: + - link to godoc (tricky: in server package) + - show Object inventory of types.Package.Scope + - show index info (xrefs, methodsets, tests) + - call DiagnoseFile on each file? +*/}} `)) diff --git a/gopls/internal/debug/template_test.go b/gopls/internal/debug/template_test.go index 8cc9ff971fe..43bc9b61bc4 100644 --- a/gopls/internal/debug/template_test.go +++ b/gopls/internal/debug/template_test.go @@ -21,7 +21,6 @@ import ( "github.com/jba/templatecheck" "golang.org/x/tools/go/packages" "golang.org/x/tools/gopls/internal/cache" - "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/debug" "golang.org/x/tools/gopls/internal/util/moremaps" "golang.org/x/tools/internal/testenv" @@ -40,7 +39,8 @@ var templates = map[string]struct { "ClientTmpl": {debug.ClientTmpl, &debug.Client{}}, "ServerTmpl": {debug.ServerTmpl, &debug.Server{}}, "FileTmpl": {debug.FileTmpl, *new(debug.FileWithKind)}, - "MetadataTmpl": {debug.MetadataTmpl, &metadata.Graph{}}, + "MetadataTmpl": {debug.MetadataTmpl, new(debug.MetadataInfo)}, + "PackageTmpl": {debug.PackageTmpl, new(debug.PackageInfo)}, "InfoTmpl": {debug.InfoTmpl, "something"}, "MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}}, "AnalysisTmpl": {debug.AnalysisTmpl, new(debug.State).Analysis()}, From 855ec1bb55dfcced6e7ae8577697c2521aab9b1b Mon Sep 17 00:00:00 2001 From: cuishuang Date: Thu, 20 Nov 2025 17:04:45 +0800 Subject: [PATCH 16/60] all: minor improvement for docs Change-Id: I16c92337faab14917ec1045eafb7bd132df5e91a Reviewed-on: https://go-review.googlesource.com/c/tools/+/722340 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Commit-Queue: Alan Donovan --- go/analysis/passes/modernize/stringscut.go | 2 +- go/ssa/create.go | 2 +- internal/astutil/util.go | 2 +- internal/typesinternal/element_test.go | 2 +- refactor/eg/rewrite.go | 2 +- refactor/rename/rename.go | 2 +- refactor/rename/spec.go | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go/analysis/passes/modernize/stringscut.go b/go/analysis/passes/modernize/stringscut.go index 521c264c51e..bc4ad677cd0 100644 --- a/go/analysis/passes/modernize/stringscut.go +++ b/go/analysis/passes/modernize/stringscut.go @@ -489,7 +489,7 @@ func isNegativeConst(info *types.Info, expr ast.Expr) bool { return false } -// isNoneNegativeConst returns true if the expr is a const int with value >= zero. +// isNonNegativeConst returns true if the expr is a const int with value >= zero. func isNonNegativeConst(info *types.Info, expr ast.Expr) bool { if tv, ok := info.Types[expr]; ok && tv.Value != nil && tv.Value.Kind() == constant.Int { if v, ok := constant.Int64Val(tv.Value); ok { diff --git a/go/ssa/create.go b/go/ssa/create.go index a8778788abc..bbf88567cb8 100644 --- a/go/ssa/create.go +++ b/go/ssa/create.go @@ -314,7 +314,7 @@ func (prog *Program) ImportedPackage(path string) *Package { return prog.imported[path] } -// setNoReturns sets the predicate used by the SSA builder to decide +// setNoReturn sets the predicate used by the SSA builder to decide // whether a call to the specified named function cannot return, // allowing the builder to prune control-flow edges following the // call, thus improving the precision of downstream analysis. diff --git a/internal/astutil/util.go b/internal/astutil/util.go index 7a02fca21e2..6986a51875b 100644 --- a/internal/astutil/util.go +++ b/internal/astutil/util.go @@ -64,7 +64,7 @@ func NodeContains(n ast.Node, rng Range) bool { return NodeRange(n).Contains(rng) } -// NodeContainPos reports whether the Pos/End range of node n encloses +// NodeContainsPos reports whether the Pos/End range of node n encloses // the given pos. // // Like [NodeRange], it treats the range of an [ast.File] as the diff --git a/internal/typesinternal/element_test.go b/internal/typesinternal/element_test.go index 95f1ab33478..7d360db3d07 100644 --- a/internal/typesinternal/element_test.go +++ b/internal/typesinternal/element_test.go @@ -78,7 +78,7 @@ func TestForEachElement(t *testing.T) { // defined struct type with methods, incl. pointer methods. // Observe that it descends into the field type, but // the result does not include the struct type itself. - // (This follows the Go toolchain behavior , and finesses the need + // (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}"}}, diff --git a/refactor/eg/rewrite.go b/refactor/eg/rewrite.go index cf02b5dafe6..caed735d9df 100644 --- a/refactor/eg/rewrite.go +++ b/refactor/eg/rewrite.go @@ -250,7 +250,7 @@ func (tr *Transformer) apply(f func(reflect.Value) (reflect.Value, bool, map[str // subst returns a copy of (replacement) pattern with values from env // substituted in place of wildcards and pos used as the position of -// tokens from the pattern. if env == nil, subst returns a copy of +// tokens from the pattern. If env == nil, subst returns a copy of // pattern and doesn't change the line number information. func (tr *Transformer) subst(env map[string]ast.Expr, pattern, pos reflect.Value) reflect.Value { if !pattern.IsValid() { diff --git a/refactor/rename/rename.go b/refactor/rename/rename.go index a2c2241c37b..fb872e57e0b 100644 --- a/refactor/rename/rename.go +++ b/refactor/rename/rename.go @@ -335,7 +335,7 @@ func Main(ctxt *build.Context, offsetFlag, fromFlag, to string) error { // Only the initially imported packages (iprog.Imported) and // their external tests (iprog.Created) should be inspected or - // modified, as only they have type-checked functions bodies. + // modified, as only they have type-checked function bodies. // The rest are just dependencies, needed only for package-level // type information. for _, info := range iprog.Imported { diff --git a/refactor/rename/spec.go b/refactor/rename/spec.go index c1854d4a5ad..ac70c47e2a4 100644 --- a/refactor/rename/spec.go +++ b/refactor/rename/spec.go @@ -259,7 +259,7 @@ var wd = func() string { }() // For source trees built with 'go build', the -from or -offset -// spec identifies exactly one initial 'from' object to rename , +// spec identifies exactly one initial 'from' object to rename, // but certain proprietary build systems allow a single file to // appear in multiple packages (e.g. the test package contains a // copy of its library), so there may be multiple objects for From eb26c37358845ea415fc22d01a03114057d11c10 Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Fri, 14 Nov 2025 12:55:25 -0500 Subject: [PATCH 17/60] gopls/internal/mcp: add counter for go_rename_symbol mcp tool Updates golang/go#76303 Change-Id: Ic62e4528957ab23b9ccbdafe8ddec335d8cc17a0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/720580 LUCI-TryBot-Result: Go LUCI Reviewed-by: Peter Weinberger --- gopls/internal/mcp/counters.go | 1 + gopls/internal/mcp/rename_symbol.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/gopls/internal/mcp/counters.go b/gopls/internal/mcp/counters.go index 91e4996f83d..b998aee212a 100644 --- a/gopls/internal/mcp/counters.go +++ b/gopls/internal/mcp/counters.go @@ -16,6 +16,7 @@ var ( countGoFileMetadataMCP = counter.New("gopls/mcp-tool:go_file_metadata") countGoPackageAPIMCP = counter.New("gopls/mcp-tool:go_package_api") countGoReferencesMCP = counter.New("gopls/mcp-tool:go_references") + countGoRenameSymbolMCP = counter.New("gopls/mcp-tool:go_rename_symbol") countGoSearchMCP = counter.New("gopls/mcp-tool:go_search") countGoSymbolReferencesMCP = counter.New("gopls/mcp-tool:go_symbol_references") countGoWorkspaceMCP = counter.New("gopls/mcp-tool:go_workspace") diff --git a/gopls/internal/mcp/rename_symbol.go b/gopls/internal/mcp/rename_symbol.go index e2fce0bfe2d..d94021c4e52 100644 --- a/gopls/internal/mcp/rename_symbol.go +++ b/gopls/internal/mcp/rename_symbol.go @@ -24,7 +24,7 @@ type renameSymbolParams struct { } func (h *handler) renameSymbolHandler(ctx context.Context, req *mcp.CallToolRequest, params renameSymbolParams) (*mcp.CallToolResult, any, error) { - // TODO(mkalil): Add telemetry for this tool. + countGoRenameSymbolMCP.Inc() fh, snapshot, release, err := h.fileOf(ctx, params.File) if err != nil { return nil, nil, err From c07802bc677293f0548e2fbd015e5a0aa1ad7f18 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 20 Nov 2025 15:36:48 +0000 Subject: [PATCH 18/60] gopls/internal/golang: remove self-referential alias in TestUnify We're disallowing `type T[P any] = P`. Remove the failing test. For golang/go##75885 Change-Id: I03568a23de4c637b80bc8f829ab896c79ef0407d Reviewed-on: https://go-review.googlesource.com/c/tools/+/722380 Auto-Submit: Robert Findley Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/implementation_test.go | 22 -------------------- 1 file changed, 22 deletions(-) diff --git a/gopls/internal/golang/implementation_test.go b/gopls/internal/golang/implementation_test.go index b7253bb8bf7..a1375623faa 100644 --- a/gopls/internal/golang/implementation_test.go +++ b/gopls/internal/golang/implementation_test.go @@ -35,9 +35,6 @@ func (casefold) Equal(x, y string) bool type A[T any] = struct { x T } type AString = struct { x string } -// B matches anything! -type B[T any] = T - func C1[T any](int, T, ...string) T { panic(0) } func C2[U any](int, int, ...U) bool { panic(0) } func C3(int, bool, ...string) rune @@ -119,25 +116,6 @@ func F9[V any](V, *V, V) { panic(0) } wantParams: tmap{tparam("A", 0): stringType}, }, {x: "A", y: "Eq", want: false}, // completely unrelated - { - x: "B", - y: "String", - want: true, - wantParams: tmap{tparam("B", 0): stringType}, - }, - { - x: "B", - y: "Int", - want: true, - wantParams: tmap{tparam("B", 0): intType}, - }, - { - x: "B", - y: "A", - want: true, - // B's T is bound to A's struct { x T } - wantParams: tmap{tparam("B", 0): scope.Lookup("A").Type().Underlying()}, - }, { // C1's U unifies with C6's bool. x: "C1", From ffe077314185d82413a8476d411b8f72494c22fd Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Nov 2025 10:12:22 -0500 Subject: [PATCH 19/60] go/analysis/passes/modernize: mapsloop: avoid implicit key widening This CL does for map keys what CL 645876 did for map values: reject the transformation if there is an implicit widening, as it would otherwise break the build (or worse). Did I mention that modernizers are hard? :( For golang/go#71313 Fixes golang/go#76380 Change-Id: I2669efc6fa2ea062094ead3757b38742088965e5 Reviewed-on: https://go-review.googlesource.com/c/tools/+/722360 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Madeline Kalil --- go/analysis/passes/modernize/maps.go | 15 +++++++++------ .../modernize/testdata/src/mapsloop/mapsloop.go | 9 ++++++++- .../testdata/src/mapsloop/mapsloop.go.golden | 9 ++++++++- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/go/analysis/passes/modernize/maps.go b/go/analysis/passes/modernize/maps.go index 2352c8b6088..f97541d4b34 100644 --- a/go/analysis/passes/modernize/maps.go +++ b/go/analysis/passes/modernize/maps.go @@ -233,13 +233,16 @@ func mapsloop(pass *analysis.Pass) (any, error) { assign := rng.Body.List[0].(*ast.AssignStmt) if index, ok := assign.Lhs[0].(*ast.IndexExpr); ok && astutil.EqualSyntax(rng.Key, index.Index) && - astutil.EqualSyntax(rng.Value, assign.Rhs[0]) && - is[*types.Map](typeparams.CoreType(info.TypeOf(index.X))) && - types.Identical(info.TypeOf(index), info.TypeOf(rng.Value)) { // m[k], v + astutil.EqualSyntax(rng.Value, assign.Rhs[0]) { + if tmap, ok := typeparams.CoreType(info.TypeOf(index.X)).(*types.Map); ok && + types.Identical(info.TypeOf(index), info.TypeOf(rng.Value)) && // m[k], v + types.Identical(tmap.Key(), info.TypeOf(rng.Key)) { - // Have: for k, v := range x { m[k] = v } - // where there is no implicit conversion. - check(file, curRange, assign, index.X, rng.X) + // Have: for k, v := range x { m[k] = v } + // where there is no implicit conversion + // of either key or value. + check(file, curRange, assign, index.X, rng.X) + } } } } diff --git a/go/analysis/passes/modernize/testdata/src/mapsloop/mapsloop.go b/go/analysis/passes/modernize/testdata/src/mapsloop/mapsloop.go index 7d0f7d17e91..0c6300b018d 100644 --- a/go/analysis/passes/modernize/testdata/src/mapsloop/mapsloop.go +++ b/go/analysis/passes/modernize/testdata/src/mapsloop/mapsloop.go @@ -209,9 +209,16 @@ func nopeNotAMapGeneric[E any, M ~map[int]E, S ~[]E](src M) { } } -func nopeHasImplicitWidening(src map[string]int) { +func nopeHasImplicitValueWidening(src map[string]int) { dst := make(map[string]any) for k, v := range src { dst[k] = v } } + +func nopeHasImplicitKeyWidening(src map[string]string) { + dst := make(map[any]string) + for k, v := range src { + dst[k] = v + } +} diff --git a/go/analysis/passes/modernize/testdata/src/mapsloop/mapsloop.go.golden b/go/analysis/passes/modernize/testdata/src/mapsloop/mapsloop.go.golden index 9136105b908..4cdbb836c28 100644 --- a/go/analysis/passes/modernize/testdata/src/mapsloop/mapsloop.go.golden +++ b/go/analysis/passes/modernize/testdata/src/mapsloop/mapsloop.go.golden @@ -193,9 +193,16 @@ func nopeNotAMapGeneric[E any, M ~map[int]E, S ~[]E](src M) { } } -func nopeHasImplicitWidening(src map[string]int) { +func nopeHasImplicitValueWidening(src map[string]int) { dst := make(map[string]any) for k, v := range src { dst[k] = v } } + +func nopeHasImplicitKeyWidening(src map[string]string) { + dst := make(map[any]string) + for k, v := range src { + dst[k] = v + } +} From 11b3200c5d539db768ca40dec0b87a188481880e Mon Sep 17 00:00:00 2001 From: Robert Griesemer Date: Thu, 20 Nov 2025 09:25:58 -0800 Subject: [PATCH 20/60] gopls/internal/util/fingerprint: : remove self-referential alias in TestMatches We're disallowing `type T[P any] = P`. Remove the failing test. For golang/go#75885. Change-Id: I9cf2c335b8335152dae96abe727dfdfc4740977b Reviewed-on: https://go-review.googlesource.com/c/tools/+/722420 Auto-Submit: Robert Griesemer LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan Reviewed-by: Robert Griesemer --- gopls/internal/util/fingerprint/fingerprint_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/gopls/internal/util/fingerprint/fingerprint_test.go b/gopls/internal/util/fingerprint/fingerprint_test.go index 40ea2ede34e..ed94b2917b6 100644 --- a/gopls/internal/util/fingerprint/fingerprint_test.go +++ b/gopls/internal/util/fingerprint/fingerprint_test.go @@ -96,9 +96,6 @@ func (casefold) Equal(x, y string) bool type A[T any] = struct { x T } type AString = struct { x string } -// B matches anything! -type B[T any] = T - func C1[T any](int, T, ...string) T { panic(0) } func C2[U any](int, int, ...U) bool { panic(0) } func C3(int, bool, ...string) rune @@ -137,9 +134,6 @@ func F9[V any](V, *V, V) { panic(0) } {"Eq", "casefold", "Equal", true}, {"A", "AString", "", true}, {"A", "Eq", "", false}, // completely unrelated - {"B", "String", "", true}, - {"B", "Int", "", true}, - {"B", "A", "", true}, {"C1", "C2", "", false}, {"C1", "C3", "", false}, {"C1", "C4", "", false}, From a6e94ebe4b5613a142d1f02e1cc2d57a75c61774 Mon Sep 17 00:00:00 2001 From: Hongxiang Jiang Date: Thu, 20 Nov 2025 00:33:19 -0500 Subject: [PATCH 21/60] gopls/internal/test/integration: deduplicate CodeActionByKind util func For golang/go#76331 Change-Id: I110a2097a7b8f8658c5567576935b9d1a2d8572b Reviewed-on: https://go-review.googlesource.com/c/tools/+/722244 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/test/integration/env.go | 10 ++++++++++ .../test/integration/misc/addtest_test.go | 2 +- .../test/integration/misc/compileropt_test.go | 17 +++-------------- .../test/integration/web/freesymbols_test.go | 2 +- .../internal/test/integration/web/pkdoc_test.go | 4 ++-- .../internal/test/integration/web/util_test.go | 13 +------------ 6 files changed, 18 insertions(+), 30 deletions(-) diff --git a/gopls/internal/test/integration/env.go b/gopls/internal/test/integration/env.go index 537c2c12180..e1dc7b3838e 100644 --- a/gopls/internal/test/integration/env.go +++ b/gopls/internal/test/integration/env.go @@ -397,3 +397,13 @@ func CleanModCache(t *testing.T, modcache string) { t.Errorf("cleaning modcache: %v\noutput:\n%s", err, string(output)) } } + +// CodeActionByKind returns the first action of (exactly) the specified kind, or an error. +func CodeActionByKind(actions []protocol.CodeAction, kind protocol.CodeActionKind) (*protocol.CodeAction, error) { + for _, act := range actions { + if act.Kind == kind { + return &act, nil + } + } + return nil, fmt.Errorf("can't find action with kind %s, only %#v", kind, actions) +} diff --git a/gopls/internal/test/integration/misc/addtest_test.go b/gopls/internal/test/integration/misc/addtest_test.go index 9ad888f891b..10354072c8b 100644 --- a/gopls/internal/test/integration/misc/addtest_test.go +++ b/gopls/internal/test/integration/misc/addtest_test.go @@ -76,7 +76,7 @@ func TestFoo(t *testing.T) { if err != nil { t.Fatalf("CodeAction: %v", err) } - action, err := codeActionByKind(actions, settings.AddTest) + action, err := CodeActionByKind(actions, settings.AddTest) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/test/integration/misc/compileropt_test.go b/gopls/internal/test/integration/misc/compileropt_test.go index a02a5dddebd..6d31e5796c6 100644 --- a/gopls/internal/test/integration/misc/compileropt_test.go +++ b/gopls/internal/test/integration/misc/compileropt_test.go @@ -5,7 +5,6 @@ package misc import ( - "fmt" "runtime" "testing" @@ -41,7 +40,7 @@ func main() { actions := env.CodeActionForFile("main.go", nil) // Execute the "Show compiler optimization details" command. - docAction, err := codeActionByKind(actions, settings.GoToggleCompilerOptDetails) + docAction, err := CodeActionByKind(actions, settings.GoToggleCompilerOptDetails) if err != nil { t.Fatal(err) } @@ -118,7 +117,7 @@ func H(x int) any { return &x } env.OpenFile(filename) actions := env.CodeActionForFile(filename, nil) - docAction, err := codeActionByKind(actions, settings.GoToggleCompilerOptDetails) + docAction, err := CodeActionByKind(actions, settings.GoToggleCompilerOptDetails) if err != nil { t.Fatal(err) } @@ -195,7 +194,7 @@ func G() { defer func(){} () } // cannotInlineFunction(unhandled op DEF env.OpenFile("a/a.go") actions := env.CodeActionForFile("a/a.go", nil) - docAction, err := codeActionByKind(actions, settings.GoToggleCompilerOptDetails) + docAction, err := CodeActionByKind(actions, settings.GoToggleCompilerOptDetails) if err != nil { t.Fatal(err) } @@ -231,13 +230,3 @@ func cond[T any](cond bool, x, y T) T { return y } } - -// codeActionByKind returns the first action of (exactly) the specified kind, or an error. -func codeActionByKind(actions []protocol.CodeAction, kind protocol.CodeActionKind) (*protocol.CodeAction, error) { - for _, act := range actions { - if act.Kind == kind { - return &act, nil - } - } - return nil, fmt.Errorf("can't find action with kind %s, only %#v", kind, actions) -} diff --git a/gopls/internal/test/integration/web/freesymbols_test.go b/gopls/internal/test/integration/web/freesymbols_test.go index 7f44c29ec1f..8cd79907618 100644 --- a/gopls/internal/test/integration/web/freesymbols_test.go +++ b/gopls/internal/test/integration/web/freesymbols_test.go @@ -44,7 +44,7 @@ func f(buf bytes.Buffer, greeting string) { if err != nil { t.Fatalf("CodeAction: %v", err) } - action, err := codeActionByKind(actions, settings.GoFreeSymbols) + action, err := CodeActionByKind(actions, settings.GoFreeSymbols) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/test/integration/web/pkdoc_test.go b/gopls/internal/test/integration/web/pkdoc_test.go index b5001421f8e..0e8d171c6af 100644 --- a/gopls/internal/test/integration/web/pkdoc_test.go +++ b/gopls/internal/test/integration/web/pkdoc_test.go @@ -114,7 +114,7 @@ const A = 1 // Invoke the "Browse package documentation" code // action to start the server. actions := env.CodeAction(env.Sandbox.Workdir.EntireFile("a.go"), nil, 0) - docAction, err := codeActionByKind(actions, settings.GoDoc) + docAction, err := CodeActionByKind(actions, settings.GoDoc) if err != nil { t.Fatal(err) } @@ -459,7 +459,7 @@ func viewPkgDoc(t *testing.T, env *Env, loc protocol.Location) protocol.URI { // Invoke the "Browse package documentation" code // action to start the server. actions := env.CodeAction(loc, nil, 0) - docAction, err := codeActionByKind(actions, settings.GoDoc) + docAction, err := CodeActionByKind(actions, settings.GoDoc) if err != nil { t.Fatal(err) } diff --git a/gopls/internal/test/integration/web/util_test.go b/gopls/internal/test/integration/web/util_test.go index 445e7aee06e..5cc108a3a2c 100644 --- a/gopls/internal/test/integration/web/util_test.go +++ b/gopls/internal/test/integration/web/util_test.go @@ -7,7 +7,6 @@ package web_test // This file defines web server testing utilities. import ( - "fmt" "io" "net/http" "os" @@ -71,23 +70,13 @@ func checkMatch(t *testing.T, want bool, got []byte, pattern string) { } } -// codeActionByKind returns the first action of (exactly) the specified kind, or an error. -func codeActionByKind(actions []protocol.CodeAction, kind protocol.CodeActionKind) (*protocol.CodeAction, error) { - for _, act := range actions { - if act.Kind == kind { - return &act, nil - } - } - return nil, fmt.Errorf("can't find action with kind %s, only %#v", kind, actions) -} - // codeActionWebPage returns the URL and content of the page opened by the specified code action. func codeActionWebPage(t *testing.T, env *integration.Env, kind protocol.CodeActionKind, loc protocol.Location) (string, []byte) { actions, err := env.Editor.CodeAction(env.Ctx, loc, nil, protocol.CodeActionUnknownTrigger) if err != nil { t.Fatalf("CodeAction: %v", err) } - action, err := codeActionByKind(actions, kind) + action, err := integration.CodeActionByKind(actions, kind) if err != nil { t.Fatal(err) } From 4c22c6ef907001466d402d74d542f3a829d37185 Mon Sep 17 00:00:00 2001 From: Hongxiang Jiang Date: Thu, 20 Nov 2025 00:37:31 -0500 Subject: [PATCH 22/60] gopls/internal/test/integration: add codeAction/resolve in fake editor For golang/go#76331 Change-Id: I791caefcaadbee043917196e7e761d8c7c51409f Reviewed-on: https://go-review.googlesource.com/c/tools/+/722245 Reviewed-by: Alan Donovan Auto-Submit: Hongxiang Jiang LUCI-TryBot-Result: Go LUCI --- gopls/internal/test/integration/fake/editor.go | 8 ++++++++ gopls/internal/test/integration/misc/addtest_test.go | 4 ++++ gopls/internal/test/integration/web/util_test.go | 4 ++++ 3 files changed, 16 insertions(+) diff --git a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go index 313e5cdbbec..ad78e29c55b 100644 --- a/gopls/internal/test/integration/fake/editor.go +++ b/gopls/internal/test/integration/fake/editor.go @@ -1685,6 +1685,14 @@ func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) e return e.Server.DidChangeWorkspaceFolders(ctx, ¶ms) } +// ResolveCodeAction executes a codeAction/resolve request on the server. +func (e *Editor) ResolveCodeAction(ctx context.Context, action *protocol.CodeAction) (*protocol.CodeAction, error) { + if e.Server == nil { + return nil, nil + } + return e.Server.ResolveCodeAction(ctx, action) +} + // CodeAction executes a codeAction request on the server. // If loc.Range is zero, the whole file is implied. // To reduce distraction, the trigger action (unknown, automatic, invoked) diff --git a/gopls/internal/test/integration/misc/addtest_test.go b/gopls/internal/test/integration/misc/addtest_test.go index 10354072c8b..7e159131559 100644 --- a/gopls/internal/test/integration/misc/addtest_test.go +++ b/gopls/internal/test/integration/misc/addtest_test.go @@ -80,6 +80,10 @@ func TestFoo(t *testing.T) { if err != nil { t.Fatal(err) } + action, err = env.Editor.ResolveCodeAction(env.Ctx, action) + if err != nil { + t.Fatal(err) + } // Execute the command. // Its side effect should be a single showDocument request. diff --git a/gopls/internal/test/integration/web/util_test.go b/gopls/internal/test/integration/web/util_test.go index 5cc108a3a2c..66cc70489ef 100644 --- a/gopls/internal/test/integration/web/util_test.go +++ b/gopls/internal/test/integration/web/util_test.go @@ -80,6 +80,10 @@ func codeActionWebPage(t *testing.T, env *integration.Env, kind protocol.CodeAct if err != nil { t.Fatal(err) } + action, err = env.Editor.ResolveCodeAction(env.Ctx, action) + if err != nil { + t.Fatal(err) + } // Execute the command. // Its side effect should be a single showDocument request. From 68724afed20950ce7cb7cc5d86618686c7b93e75 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 19 Nov 2025 14:28:21 -0500 Subject: [PATCH 23/60] go/analysis/passes/printf: use induction on interface methods This CL causes the printf analyzer to use the presence of an assignment from an implementation to an interface type type I interface { Printf(...) } type T ... var _ I = T(...) as evidence that: if a method T.F is found to be a printf wrapper, then the corresponding abstract method I.F is also to be considered a printf wrapper. This allows induction through interfaces without the need for annotations. Only assigments within the same package as I.Printf are considered. This feature is only enabled for interface methods declared in files using at least go1.26, to avoid surprises due to go test's vet suite getting stricter. + docs, tests Fixes golang/go#76368 For golang/go#58340 Change-Id: I124aa383d9b4278ffe8d982ed630813a836bd0cf Reviewed-on: https://go-review.googlesource.com/c/tools/+/721984 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/printf/doc.go | 30 ++ go/analysis/passes/printf/printf.go | 277 ++++++++++++------ .../passes/printf/testdata/src/a/a2.go | 36 +++ 3 files changed, 253 insertions(+), 90 deletions(-) create mode 100644 go/analysis/passes/printf/testdata/src/a/a2.go diff --git a/go/analysis/passes/printf/doc.go b/go/analysis/passes/printf/doc.go index f04e4414341..a09bfd1c6c8 100644 --- a/go/analysis/passes/printf/doc.go +++ b/go/analysis/passes/printf/doc.go @@ -92,6 +92,36 @@ // } // logf("%s", 123) // logf format %s has arg 123 of wrong type int // +// Interface methods may also be analyzed as printf wrappers, if +// within the interface's package there is an assignment from a +// implementation type whose corresponding method is a printf wrapper. +// +// For example, the var declaration below causes a *myLoggerImpl value +// to be assigned to a Logger variable: +// +// type Logger interface { +// Logf(format string, args ...any) +// } +// +// type myLoggerImpl struct{ ... } +// +// var _ Logger = (*myLoggerImpl)(nil) +// +// func (*myLoggerImpl) Logf(format string, args ...any) { +// println(fmt.Sprintf(format, args...)) +// } +// +// Since myLoggerImpl's Logf method is a printf wrapper, this +// establishes that Logger.Logf is a printf wrapper too, causing +// dynamic calls through the interface to be checked: +// +// func f(log Logger) { +// log.Logf("%s", 123) // Logger.Logf format %s has arg 123 of wrong type int +// } +// +// This feature applies only to interface methods declared in files +// using at least Go 1.26. +// // # Specifying printf wrappers by flag // // The -funcs flag specifies a comma-separated list of names of diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index 1eac2589bfa..fd9fe164723 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -27,6 +27,7 @@ import ( "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" + "golang.org/x/tools/refactor/satisfy" ) func init() { @@ -65,7 +66,7 @@ func (kind Kind) String() string { case KindErrorf: return "errorf" } - return "" + return "(none)" } // Result is the printf analyzer's result type. Clients may query the result @@ -138,7 +139,7 @@ type wrapper struct { type printfCaller struct { w *wrapper - call *ast.CallExpr + call *ast.CallExpr // forwarding call (nil for implicit interface method -> impl calls) } // formatArgsParams returns the "format string" and "args ...any" @@ -183,21 +184,42 @@ func findPrintLike(pass *analysis.Pass, res *Result) { wrappers []*wrapper byObj = make(map[types.Object]*wrapper) ) - for cur := range inspect.Root().Preorder((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil)) { - var ( - curBody inspector.Cursor // for *ast.BlockStmt - sig *types.Signature - obj types.Object - ) + for cur := range inspect.Root().Preorder((*ast.FuncDecl)(nil), (*ast.FuncLit)(nil), (*ast.InterfaceType)(nil)) { + + // addWrapper records that a func (or var representing + // a FuncLit) is a potential print{,f} wrapper. + // curBody is its *ast.BlockStmt, if any. + addWrapper := func(obj types.Object, sig *types.Signature, curBody inspector.Cursor) *wrapper { + format, args := formatArgsParams(sig) + if args != nil { + // obj (the symbol for a function/method, or variable + // assigned to an anonymous function) is a potential + // print or printf wrapper. + // + // Later processing will analyze the graph of potential + // wrappers and their function bodies to pick out the + // ones that are true wrappers. + w := &wrapper{ + obj: obj, + curBody: curBody, + format: format, // non-nil => printf + args: args, + } + byObj[w.obj] = w + wrappers = append(wrappers, w) + return w + } + return nil + } + switch f := cur.Node().(type) { case *ast.FuncDecl: // named function or method: // // func wrapf(format string, args ...any) {...} if f.Body != nil { - curBody = cur.ChildAt(edge.FuncDecl_Body, -1) - obj = info.Defs[f.Name] - sig = obj.Type().(*types.Signature) + fn := info.Defs[f.Name].(*types.Func) + addWrapper(fn, fn.Signature(), cur.ChildAt(edge.FuncDecl_Body, -1)) } case *ast.FuncLit: @@ -210,8 +232,6 @@ func findPrintLike(pass *analysis.Pass, res *Result) { // The LHS may also be a struct field x.wrapf or // an imported var pkg.Wrapf. // - sig = info.TypeOf(f).(*types.Signature) - curBody = cur.ChildAt(edge.FuncLit_Body, -1) var lhs ast.Expr switch ek, idx := cur.ParentEdge(); ek { case edge.ValueSpec_Values: @@ -222,49 +242,89 @@ func findPrintLike(pass *analysis.Pass, res *Result) { lhs = curLhs.Node().(ast.Expr) } + var v *types.Var switch lhs := lhs.(type) { case *ast.Ident: // variable: wrapf = func(...) - obj = info.ObjectOf(lhs).(*types.Var) + v = info.ObjectOf(lhs).(*types.Var) case *ast.SelectorExpr: if sel, ok := info.Selections[lhs]; ok { // struct field: x.wrapf = func(...) - obj = sel.Obj().(*types.Var) + v = sel.Obj().(*types.Var) } else { // imported var: pkg.Wrapf = func(...) - obj = info.Uses[lhs.Sel].(*types.Var) + v = info.Uses[lhs.Sel].(*types.Var) } } - } - if obj != nil { - format, args := formatArgsParams(sig) - if args != nil { - // obj (the symbol for a function/method, or variable - // assigned to an anonymous function) is a potential - // print or printf wrapper. - // - // Later processing will analyze the graph of potential - // wrappers and their function bodies to pick out the - // ones that are true wrappers. - w := &wrapper{ - obj: obj, - curBody: curBody, - format: format, // non-nil => printf - args: args, + if v != nil { + sig := info.TypeOf(f).(*types.Signature) + curBody := cur.ChildAt(edge.FuncLit_Body, -1) + addWrapper(v, sig, curBody) + } + + case *ast.InterfaceType: + // Induction through interface methods is gated as + // if it were a go1.26 language feature, to avoid + // surprises when go test's vet suite gets stricter. + if analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(cur), versions.Go1_26) { + for imeth := range info.TypeOf(f).(*types.Interface).Methods() { + addWrapper(imeth, imeth.Signature(), inspector.Cursor{}) } - byObj[w.obj] = w - wrappers = append(wrappers, w) } } } + // impls maps abstract methods to implementations. + // + // Interface methods are modelled as if they have a body + // that calls each implementing method. + // + // In the code below, impls maps Logger.Logf to + // [myLogger.Logf], and if myLogger.Logf is discovered to be + // printf-like, then so will be Logger.Logf. + // + // type Logger interface { + // Logf(format string, args ...any) + // } + // type myLogger struct{ ... } + // func (myLogger) Logf(format string, args ...any) {...} + // var _ Logger = myLogger{} + impls := methodImplementations(pass) + // Pass 2: scan the body of each wrapper function // for calls to other printf-like functions. - // - // Also, reject tricky cases where the parameters - // are potentially mutated by AssignStmt or UnaryExpr. - // TODO: Relax these checks; issue 26555. for _, w := range wrappers { + + // doCall records a call from one wrapper to another. + doCall := func(callee types.Object, call *ast.CallExpr) { + // Call from one wrapper candidate to another? + // Record the edge so that if callee is found to be + // a true wrapper, w will be too. + if w2, ok := byObj[callee]; ok { + w2.callers = append(w2.callers, printfCaller{w, call}) + } + + // Is the candidate a true wrapper, because it calls + // a known print{,f}-like function from the allowlist + // or an imported fact, or another wrapper found + // to be a true wrapper? + // If so, convert all w's callers to kind. + kind := callKind(pass, callee, res) + if kind != KindNone { + checkForward(pass, w, call, kind, res) + } + } + + // An interface method has no body, but acts + // like an implicit call to each implementing method. + if w.curBody.Inspector() == nil { + for impl := range impls[w.obj.(*types.Func)] { + doCall(impl, nil) + } + continue // (no body) + } + + // Process all calls in the wrapper function's body. scan: for cur := range w.curBody.Preorder( (*ast.AssignStmt)(nil), @@ -272,6 +332,12 @@ func findPrintLike(pass *analysis.Pass, res *Result) { (*ast.CallExpr)(nil), ) { switch n := cur.Node().(type) { + + // Reject tricky cases where the parameters + // are potentially mutated by AssignStmt or UnaryExpr. + // (This logic checks for mutation only before the call.) + // TODO: Relax these checks; issue 26555. + case *ast.AssignStmt: // If the wrapper updates format or args // it is not a simple wrapper. @@ -294,23 +360,7 @@ func findPrintLike(pass *analysis.Pass, res *Result) { case *ast.CallExpr: if len(n.Args) > 0 && match(info, n.Args[len(n.Args)-1], w.args) { if callee := typeutil.Callee(pass.TypesInfo, n); callee != nil { - - // Call from one wrapper candidate to another? - // Record the edge so that if callee is found to be - // a true wrapper, w will be too. - if w2, ok := byObj[callee]; ok { - w2.callers = append(w2.callers, printfCaller{w, n}) - } - - // Is the candidate a true wrapper, because it calls - // a known print{,f}-like function from the allowlist - // or an imported fact, or another wrapper found - // to be a true wrapper? - // If so, convert all w's callers to kind. - kind := callKind(pass, callee, res) - if kind != KindNone { - checkForward(pass, w, n, kind, res) - } + doCall(callee, n) } } } @@ -318,41 +368,90 @@ func findPrintLike(pass *analysis.Pass, res *Result) { } } +// methodImplementations returns the mapping from interface methods +// declared in this package to their corresponding implementing +// methods (which may also be interface methods), according to the set +// of assignments to interface types that appear within this package. +func methodImplementations(pass *analysis.Pass) map[*types.Func]map[*types.Func]bool { + impls := make(map[*types.Func]map[*types.Func]bool) + + // To find interface/implementation relations, + // we use the 'satisfy' pass, but proposal #70638 + // provides a better way. + // + // This pass over the syntax could be factored out as + // a separate analysis pass if it is needed by other + // analyzers. + var f satisfy.Finder + f.Find(pass.TypesInfo, pass.Files) + for assign := range f.Result { + // Have: LHS = RHS, where LHS is an interface type. + for imeth := range assign.LHS.Underlying().(*types.Interface).Methods() { + // Limit to interface methods of current package. + if imeth.Pkg() != pass.Pkg { + continue + } + + if _, args := formatArgsParams(imeth.Signature()); args == nil { + continue // not print{,f}-like + } + + // Add implementing method to the set. + impl, _, _ := types.LookupFieldOrMethod(assign.RHS, false, pass.Pkg, imeth.Name()) // can't fail + set, ok := impls[imeth] + if !ok { + set = make(map[*types.Func]bool) + impls[imeth] = set + } + set[impl.(*types.Func)] = true + } + } + return impls +} + func match(info *types.Info, arg ast.Expr, param *types.Var) bool { id, ok := arg.(*ast.Ident) return ok && info.ObjectOf(id) == param } -// checkForward checks that a forwarding wrapper is forwarding correctly. -// It diagnoses writing fmt.Printf(format, args) instead of fmt.Printf(format, args...). +// checkForward checks whether a forwarding wrapper is forwarding correctly. +// If so, it propagates changes in wrapper kind information backwards +// through through the wrapper.callers graph of forwarding calls. +// +// If not, it reports a diagnostic that the user wrote +// fmt.Printf(format, args) instead of fmt.Printf(format, args...). func checkForward(pass *analysis.Pass, w *wrapper, call *ast.CallExpr, kind Kind, res *Result) { - matched := kind == KindPrint || - kind != KindNone && len(call.Args) >= 2 && match(pass.TypesInfo, call.Args[len(call.Args)-2], w.format) - if !matched { - return - } - - if !call.Ellipsis.IsValid() { - typ, ok := pass.TypesInfo.Types[call.Fun].Type.(*types.Signature) - if !ok { + // Check correct call forwarding. + // (Interface methods forward correctly by construction.) + if call != nil { + matched := kind == KindPrint || + kind != KindNone && len(call.Args) >= 2 && match(pass.TypesInfo, call.Args[len(call.Args)-2], w.format) + if !matched { return } - if len(call.Args) > typ.Params().Len() { - // If we're passing more arguments than what the - // print/printf function can take, adding an ellipsis - // would break the program. For example: - // - // func foo(arg1 string, arg2 ...interface{}) { - // fmt.Printf("%s %v", arg1, arg2) - // } + + if !call.Ellipsis.IsValid() { + typ, ok := pass.TypesInfo.Types[call.Fun].Type.(*types.Signature) + if !ok { + return + } + if len(call.Args) > typ.Params().Len() { + // If we're passing more arguments than what the + // print/printf function can take, adding an ellipsis + // would break the program. For example: + // + // func foo(arg1 string, arg2 ...interface{}) { + // fmt.Printf("%s %v", arg1, arg2) + // } + return + } + desc := "printf" + if kind == KindPrint { + desc = "print" + } + pass.ReportRangef(call, "missing ... in args forwarded to %s-like function", desc) return } - desc := "printf" - if kind == KindPrint { - desc = "print" - } - pass.ReportRangef(call, "missing ... in args forwarded to %s-like function", desc) - return } // If the candidate's print{,f} status becomes known, @@ -444,16 +543,14 @@ var isPrint = stringSet{ "(*testing.common).Logf": true, "(*testing.common).Skip": true, "(*testing.common).Skipf": true, - // *testing.T and B are detected by induction, but testing.TB is - // an interface and the inference can't follow dynamic calls. - "(testing.TB).Error": true, - "(testing.TB).Errorf": true, - "(testing.TB).Fatal": true, - "(testing.TB).Fatalf": true, - "(testing.TB).Log": true, - "(testing.TB).Logf": true, - "(testing.TB).Skip": true, - "(testing.TB).Skipf": true, + "(testing.TB).Error": true, + "(testing.TB).Errorf": true, + "(testing.TB).Fatal": true, + "(testing.TB).Fatalf": true, + "(testing.TB).Log": true, + "(testing.TB).Logf": true, + "(testing.TB).Skip": true, + "(testing.TB).Skipf": true, } // formatStringIndex returns the index of the format string (the last diff --git a/go/analysis/passes/printf/testdata/src/a/a2.go b/go/analysis/passes/printf/testdata/src/a/a2.go new file mode 100644 index 00000000000..8913846d503 --- /dev/null +++ b/go/analysis/passes/printf/testdata/src/a/a2.go @@ -0,0 +1,36 @@ +//go:build go1.26 + +package a + +// Test of induction through interface assignments. (Applies only to +// interface methods declared in files that use at least Go 1.26.) + +import "fmt" + +type myLogger int + +func (myLogger) Logf(format string, args ...any) { // want Logf:"printfWrapper" + print(fmt.Sprintf(format, args...)) +} + +// Logger is assigned from myLogger. + +type Logger interface { + Logf(format string, args ...any) // want Logf:"printfWrapper" +} + +var _ Logger = myLogger(0) // establishes that Logger wraps myLogger + +func _(log Logger) { + log.Logf("%s", 123) // want `\(a.Logger\).Logf format %s has arg 123 of wrong type int` +} + +// Logger2 is not assigned from myLogger. + +type Logger2 interface { + Logf(format string, args ...any) +} + +func _(log Logger2) { + log.Logf("%s", 123) // nope +} From af205c0a298022be9e62adee1bb5bca906ed8541 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Nov 2025 13:56:11 -0500 Subject: [PATCH 24/60] gopls/doc/release/v0.21.0.md: tweaks Change-Id: I34f0f91e6c3fcd8359d1f170373e60301813a528 Reviewed-on: https://go-review.googlesource.com/c/tools/+/722461 Reviewed-by: Madeline Kalil LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/doc/release/v0.21.0.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/gopls/doc/release/v0.21.0.md b/gopls/doc/release/v0.21.0.md index 00c6de81a8a..c84d795f382 100644 --- a/gopls/doc/release/v0.21.0.md +++ b/gopls/doc/release/v0.21.0.md @@ -11,7 +11,7 @@ title: "Gopls release v0.21.0 (expected Nov 2025)" `run_govulncheck: true`, causing gopls to offer a "Run govulncheck" command associated with the `module` declaration in a go.mod file. -- Also, the experimental `vulncheck` setting now supports the enum value +- The experimental `vulncheck` setting now supports the enum value `"Prompt"`, meaning that vulncheck can be triggered via a prompt; this is now the default. @@ -101,7 +101,7 @@ The `stditerators` analyzer replaces loops of this form, or their "range x.Len()" equivalent, by for elem := range x.All() { - use(x.At(i) + use(elem) } for various types in the standard library that now offer a @@ -151,11 +151,11 @@ by the more modern `errors.AsType`, when the type is known statically: ### Miscellaneous -- `minmax` now removes user-defined min/max functions when they are redundant; -- `stringscutprefix` now handles `strings.CutSuffix` too; -- the `yield` ad `nilness` analyzers handle control-flow booleans more precisely; -- `printf` now checks calls to local anonymous printf-wrapper functions too; -- the `staticcheck` suite has been updated to v0.7; +- The `minmax` modernizer now removes user-defined min/max functions when they are redundant. +- The `stringscutprefix` modernizer now handles `strings.CutSuffix` too. +- The `yield` and `nilness` analyzers handle control-flow booleans more precisely. +- The `printf` analyzer now checks calls to local anonymous printf-wrapper functions too. +- The `staticcheck` suite has been updated to v0.7. ## Code transformation features @@ -176,21 +176,24 @@ want subpackages to move too. (In due course, we will add LSP support for dialogs so that the server can query the user's intent interactively.) +This feature requires client support for the LSP +[`textDocument/prepareRename`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_prepareRename) operation. + ### Renaming from a doc link -The Rename operation now treats [Doc Links](https://tip.golang.org/doc/comment#doclinks) -like identifiers, so you can initiate a renaming from a Doc Link. +The Rename operation now treats [doc links](https://tip.golang.org/doc/comment#doclinks) +like identifiers, so you can initiate a renaming from a doc link. -## MCP features +## Model context protocol (MCP) features The MCP server now supports the `go_rename_symbol` tool, which permits an agent to rename a symbol based on its name (e.g. `fmt.Println` or `bytes.Buffer.WriteString`). The tool uses the symbol index to resolve the name to a declaration and then initiates a renaming. -It also supports the `gopls.vulncheck` tool, allowing agents to scan -packages for security vulnerabilities. +The server also supports the `gopls.vulncheck` tool, allowing agents +to scan packages for security vulnerabilities. ## Closed issues From c7a1a29f93f04ddc3b2c5d8c8f3985cea719d737 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Nov 2025 17:15:34 -0500 Subject: [PATCH 25/60] internal/pkgbits: fix printf mistake in test For golang/go#72850 Change-Id: I1624ae74c5ece7580e2f45d796968ed5f70f221d Reviewed-on: https://go-review.googlesource.com/c/tools/+/722541 LUCI-TryBot-Result: Go LUCI Reviewed-by: Madeline Kalil Auto-Submit: Alan Donovan --- internal/pkgbits/pkgbits_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/pkgbits/pkgbits_test.go b/internal/pkgbits/pkgbits_test.go index b8f946a0a4f..f1f2b4a74af 100644 --- a/internal/pkgbits/pkgbits_test.go +++ b/internal/pkgbits/pkgbits_test.go @@ -29,7 +29,7 @@ func TestRoundTrip(t *testing.T) { r := pr.NewDecoder(pkgbits.RelocMeta, pkgbits.PublicRootIdx, pkgbits.SyncPublic) if r.Version() != w.Version() { - t.Errorf("Expected reader version %q to be the writer version %q", r.Version(), w.Version()) + t.Errorf("Expected reader version %d to be the writer version %d", r.Version(), w.Version()) } } } From a7d12506a0429c004dff5783b6da7c8456b8b0a4 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 21 Nov 2025 08:44:31 -0500 Subject: [PATCH 26/60] go/analysis/passes/printf: clarify checkForward Belated follow-up to suggestion in CL 721984: - rename checkForward to propagate, since that's it's main job; - split its forwarding check into a helper function, itself called checkForward; - clarify commentary; - hoist doCall out of loop to avoid an alloc; - use Kind.String() in forwarding error message; this is simpler and causes it to accurately distinguish printf from errorf. For golang/go#76368 Change-Id: I00777c7ab4e6dbb0c1407a680973de33b3031c42 Reviewed-on: https://go-review.googlesource.com/c/tools/+/722800 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/analysis/passes/printf/printf.go | 131 +++++++++++++++------------- 1 file changed, 71 insertions(+), 60 deletions(-) diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index fd9fe164723..e6088b33024 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -137,6 +137,7 @@ type wrapper struct { callers []printfCaller } +// printfCaller is a candidate print{,f} forwarding call from candidate wrapper w. type printfCaller struct { w *wrapper call *ast.CallExpr // forwarding call (nil for implicit interface method -> impl calls) @@ -291,35 +292,35 @@ func findPrintLike(pass *analysis.Pass, res *Result) { // var _ Logger = myLogger{} impls := methodImplementations(pass) + // doCall records a call from one wrapper to another. + doCall := func(w *wrapper, callee types.Object, call *ast.CallExpr) { + // Call from one wrapper candidate to another? + // Record the edge so that if callee is found to be + // a true wrapper, w will be too. + if w2, ok := byObj[callee]; ok { + w2.callers = append(w2.callers, printfCaller{w, call}) + } + + // Is the candidate a true wrapper, because it calls + // a known print{,f}-like function from the allowlist + // or an imported fact, or another wrapper found + // to be a true wrapper? + // If so, convert all w's callers to kind. + kind := callKind(pass, callee, res) + if kind != KindNone { + propagate(pass, w, call, kind, res) + } + } + // Pass 2: scan the body of each wrapper function // for calls to other printf-like functions. for _, w := range wrappers { - // doCall records a call from one wrapper to another. - doCall := func(callee types.Object, call *ast.CallExpr) { - // Call from one wrapper candidate to another? - // Record the edge so that if callee is found to be - // a true wrapper, w will be too. - if w2, ok := byObj[callee]; ok { - w2.callers = append(w2.callers, printfCaller{w, call}) - } - - // Is the candidate a true wrapper, because it calls - // a known print{,f}-like function from the allowlist - // or an imported fact, or another wrapper found - // to be a true wrapper? - // If so, convert all w's callers to kind. - kind := callKind(pass, callee, res) - if kind != KindNone { - checkForward(pass, w, call, kind, res) - } - } - // An interface method has no body, but acts // like an implicit call to each implementing method. if w.curBody.Inspector() == nil { for impl := range impls[w.obj.(*types.Func)] { - doCall(impl, nil) + doCall(w, impl, nil) } continue // (no body) } @@ -360,7 +361,7 @@ func findPrintLike(pass *analysis.Pass, res *Result) { case *ast.CallExpr: if len(n.Args) > 0 && match(info, n.Args[len(n.Args)-1], w.args) { if callee := typeutil.Callee(pass.TypesInfo, n); callee != nil { - doCall(callee, n) + doCall(w, callee, n) } } } @@ -414,44 +415,15 @@ func match(info *types.Info, arg ast.Expr, param *types.Var) bool { return ok && info.ObjectOf(id) == param } -// checkForward checks whether a forwarding wrapper is forwarding correctly. -// If so, it propagates changes in wrapper kind information backwards -// through through the wrapper.callers graph of forwarding calls. -// -// If not, it reports a diagnostic that the user wrote -// fmt.Printf(format, args) instead of fmt.Printf(format, args...). -func checkForward(pass *analysis.Pass, w *wrapper, call *ast.CallExpr, kind Kind, res *Result) { +// propagate propagates changes in wrapper (non-None) kind information backwards +// through 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. - // (Interface methods forward correctly by construction.) - if call != nil { - matched := kind == KindPrint || - kind != KindNone && len(call.Args) >= 2 && match(pass.TypesInfo, call.Args[len(call.Args)-2], w.format) - if !matched { - return - } - - if !call.Ellipsis.IsValid() { - typ, ok := pass.TypesInfo.Types[call.Fun].Type.(*types.Signature) - if !ok { - return - } - if len(call.Args) > typ.Params().Len() { - // If we're passing more arguments than what the - // print/printf function can take, adding an ellipsis - // would break the program. For example: - // - // func foo(arg1 string, arg2 ...interface{}) { - // fmt.Printf("%s %v", arg1, arg2) - // } - return - } - desc := "printf" - if kind == KindPrint { - desc = "print" - } - pass.ReportRangef(call, "missing ... in args forwarded to %s-like function", desc) - return - } + // + // Interface methods (call==nil) forward + // correctly by construction. + if call != nil && !checkForward(pass, w, call, kind) { + return } // If the candidate's print{,f} status becomes known, @@ -471,9 +443,48 @@ func checkForward(pass *analysis.Pass, w *wrapper, call *ast.CallExpr, kind Kind // Propagate kind back to known callers. for _, caller := range w.callers { - checkForward(pass, caller.w, caller.call, kind, res) + propagate(pass, caller.w, caller.call, kind, res) + } + } +} + +// checkForward checks whether a call from wrapper w is a well-formed +// forwarding call of the specified (non-None) kind. +// +// If not, it reports a diagnostic that the user wrote +// fmt.Printf(format, args) instead of fmt.Printf(format, args...). +func checkForward(pass *analysis.Pass, w *wrapper, call *ast.CallExpr, kind Kind) bool { + // Printf/Errorf calls must delegate the format string. + switch kind { + case KindPrintf, KindErrorf: + if len(call.Args) < 2 || !match(pass.TypesInfo, call.Args[len(call.Args)-2], w.format) { + return false } } + + // The args... delegation must be variadic. + // (That args is actually delegated was + // established before the root call to doCall.) + if !call.Ellipsis.IsValid() { + typ, ok := pass.TypesInfo.Types[call.Fun].Type.(*types.Signature) + if !ok { + return false + } + if len(call.Args) > typ.Params().Len() { + // If we're passing more arguments than what the + // print/printf function can take, adding an ellipsis + // would break the program. For example: + // + // func foo(arg1 string, arg2 ...interface{}) { + // fmt.Printf("%s %v", arg1, arg2) + // } + return false + } + pass.ReportRangef(call, "missing ... in args forwarded to %s-like function", kind) + return false + } + + return true } func origin(obj types.Object) types.Object { From ba5189b063c24d50532bdedee31bf9f33128226e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Nov 2025 17:27:41 -0500 Subject: [PATCH 27/60] gopls/internal/template: fix printf mistake in test For golang/go#72850 Change-Id: I1be070edcc82a13ee0f7ad1b20485c14afbdf7e0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/722601 Reviewed-by: Madeline Kalil LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/internal/template/parse_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gopls/internal/template/parse_test.go b/gopls/internal/template/parse_test.go index 1995e4a3fa7..6ba674e9dfc 100644 --- a/gopls/internal/template/parse_test.go +++ b/gopls/internal/template/parse_test.go @@ -95,13 +95,13 @@ func TestQuotes(t *testing.T) { } { p := parseBuffer("", []byte(s.tmpl)) if len(p.tokens) != s.tokCnt { - t.Errorf("%q: got %d tokens, expected %d", s, len(p.tokens), s.tokCnt) + t.Errorf("%#v: got %d tokens, expected %d", s, len(p.tokens), s.tokCnt) } if p.parseErr != nil { t.Errorf("%q: %v", string(p.buf), p.parseErr) } if len(p.elided) != int(s.elidedCnt) { - t.Errorf("%q: elided %d, expected %d", s, len(p.elided), s.elidedCnt) + t.Errorf("%#v: elided %d, expected %d", s, len(p.elided), s.elidedCnt) } } } From 4bf3169c8ac4c885e61f616071d674bb17b40d27 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 22 Nov 2025 14:23:13 -0500 Subject: [PATCH 28/60] go/analysis/passes/modernize: waitgroup: highlight "go func" part This CL moves the portion of text that is flagged by the diagnostic to just "go func", not "wg.Add(1); go func() { ... }()" as before, to avoid annoying multiline squigglies. Change-Id: I5f38713c950253674382cd74ce501f6885fe1b01 Reviewed-on: https://go-review.googlesource.com/c/tools/+/723380 Reviewed-by: Madeline Kalil Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/modernize/forvar.go | 2 +- .../testdata/src/waitgroup/waitgroup.go | 48 +++++++++---------- .../src/waitgroup/waitgroup.go.golden | 24 +++++----- .../testdata/src/waitgroup/waitgroup_alias.go | 8 ++-- .../src/waitgroup/waitgroup_alias.go.golden | 4 +- .../testdata/src/waitgroup/waitgroup_dot.go | 8 ++-- .../src/waitgroup/waitgroup_dot.go.golden | 4 +- go/analysis/passes/modernize/waitgroup.go | 6 ++- 8 files changed, 53 insertions(+), 51 deletions(-) diff --git a/go/analysis/passes/modernize/forvar.go b/go/analysis/passes/modernize/forvar.go index 67f60acaaf3..ba54daebbfc 100644 --- a/go/analysis/passes/modernize/forvar.go +++ b/go/analysis/passes/modernize/forvar.go @@ -35,7 +35,7 @@ var ForVarAnalyzer = &analysis.Analyzer{ // where the two idents are the same, // and the ident is defined (:=) as a variable in the for statement. // (Note that this 'fix' does not work for three clause loops -// because the Go specfilesUsingGoVersionsays "The variable used by each subsequent iteration +// because the Go spec says "The variable used by each subsequent iteration // is declared implicitly before executing the post statement and initialized to the // value of the previous iteration's variable at that moment.") // diff --git a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup.go b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup.go index 51d93dec2be..9625e3c2144 100644 --- a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup.go +++ b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup.go @@ -8,20 +8,20 @@ import ( // supported case for pattern 1. func _() { var wg sync.WaitGroup - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" defer wg.Done() fmt.Println() }() - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" defer wg.Done() }() for range 10 { - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" defer wg.Done() fmt.Println() }() @@ -31,20 +31,20 @@ func _() { // supported case for pattern 2. func _() { var wg sync.WaitGroup - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() wg.Done() }() - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" wg.Done() }() for range 10 { - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() wg.Done() }() @@ -54,22 +54,22 @@ func _() { // this function puts some wrong usages but waitgroup modernizer will still offer fixes. func _() { var wg sync.WaitGroup - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" defer wg.Done() defer wg.Done() fmt.Println() }() - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" defer wg.Done() fmt.Println() wg.Done() }() - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() wg.Done() wg.Done() @@ -161,23 +161,23 @@ type ServerContainer struct { func _() { var s Server - s.wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + s.wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" print() s.wg.Done() }() var sc ServerContainer - sc.serv.wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + sc.serv.wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" print() sc.serv.wg.Done() }() var wg sync.WaitGroup arr := [1]*sync.WaitGroup{&wg} - arr[0].Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + arr[0].Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" print() arr[0].Done() }() diff --git a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup.go.golden b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup.go.golden index b90446fd514..1a08fe45ac8 100644 --- a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup.go.golden +++ b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup.go.golden @@ -8,15 +8,15 @@ import ( // supported case for pattern 1. func _() { var wg sync.WaitGroup - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() }) - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" }) for range 10 { - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() }) } @@ -25,15 +25,15 @@ func _() { // supported case for pattern 2. func _() { var wg sync.WaitGroup - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() }) - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" }) for range 10 { - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() }) } @@ -42,17 +42,17 @@ func _() { // this function puts some wrong usages but waitgroup modernizer will still offer fixes. func _() { var wg sync.WaitGroup - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" defer wg.Done() fmt.Println() }) - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() wg.Done() }) - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() wg.Done() }) @@ -143,18 +143,18 @@ type ServerContainer struct { func _() { var s Server - s.wg.Go(func() { + s.wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" print() }) var sc ServerContainer - sc.serv.wg.Go(func() { + sc.serv.wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" print() }) var wg sync.WaitGroup arr := [1]*sync.WaitGroup{&wg} - arr[0].Go(func() { + arr[0].Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" print() }) } diff --git a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_alias.go b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_alias.go index 087edba27be..6632160de06 100644 --- a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_alias.go +++ b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_alias.go @@ -7,14 +7,14 @@ import ( func _() { var wg sync1.WaitGroup - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" defer wg.Done() fmt.Println() }() - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() wg.Done() }() diff --git a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_alias.go.golden b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_alias.go.golden index f6f5653f46d..f54e3e64808 100644 --- a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_alias.go.golden +++ b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_alias.go.golden @@ -7,11 +7,11 @@ import ( func _() { var wg sync1.WaitGroup - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() }) - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() }) } \ No newline at end of file diff --git a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_dot.go b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_dot.go index b4d1e150dbc..091143605db 100644 --- a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_dot.go +++ b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_dot.go @@ -8,14 +8,14 @@ import ( // supported case for pattern 1. func _() { var wg WaitGroup - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" defer wg.Done() fmt.Println() }() - wg.Add(1) // want "Goroutine creation can be simplified using WaitGroup.Go" - go func() { + wg.Add(1) + go func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() wg.Done() }() diff --git a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_dot.go.golden b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_dot.go.golden index f3af8bd66b0..2cc6e923b84 100644 --- a/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_dot.go.golden +++ b/go/analysis/passes/modernize/testdata/src/waitgroup/waitgroup_dot.go.golden @@ -8,11 +8,11 @@ import ( // supported case for pattern 1. func _() { var wg WaitGroup - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() }) - wg.Go(func() { + wg.Go(func() { // want "Goroutine creation can be simplified using WaitGroup.Go" fmt.Println() }) } \ No newline at end of file diff --git a/go/analysis/passes/modernize/waitgroup.go b/go/analysis/passes/modernize/waitgroup.go index 19564c69b60..abf5885cee2 100644 --- a/go/analysis/passes/modernize/waitgroup.go +++ b/go/analysis/passes/modernize/waitgroup.go @@ -137,8 +137,10 @@ func waitgroup(pass *analysis.Pass) (any, error) { } pass.Report(analysis.Diagnostic{ - Pos: addCall.Pos(), - End: goStmt.End(), + // go func() { + // ~~~~~~~~~ + Pos: goStmt.Pos(), + End: lit.Type.End(), Message: "Goroutine creation can be simplified using WaitGroup.Go", SuggestedFixes: []analysis.SuggestedFix{{ Message: "Simplify by using WaitGroup.Go", From c2c902c441193065b90564c0396231f1319a0fc2 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Mon, 24 Nov 2025 10:51:40 -0500 Subject: [PATCH 29/60] gopls/completion: avoid nil dereference Telemetry reported a nil dereference which this CL would detect and avoid. Fixes golang/go#76438 Change-Id: I257d733b1ed12a7395391ec331f14102e42f42ca Reviewed-on: https://go-review.googlesource.com/c/tools/+/723801 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/golang/completion/unimported.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gopls/internal/golang/completion/unimported.go b/gopls/internal/golang/completion/unimported.go index 564cb74e901..53350ef4b85 100644 --- a/gopls/internal/golang/completion/unimported.go +++ b/gopls/internal/golang/completion/unimported.go @@ -82,6 +82,10 @@ func (c *completer) unimported(ctx context.Context, pkgname metadata.PackageName // prefer completion items that are referenced in the go.mod file func (c *completer) filterGoMod(ctx context.Context, items []CompletionItem) []CompletionItem { + if c.pkg.Metadata().Module == nil { + // for std or GOROOT mode + return items + } gomod := c.pkg.Metadata().Module.GoMod uri := protocol.URIFromPath(gomod) fh, err := c.snapshot.ReadFile(ctx, uri) From 4c5faddb0f44811649729679206d8481cf214a25 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Sun, 23 Nov 2025 08:50:24 -0500 Subject: [PATCH 30/60] internal/modindex: unescape import paths In the module cache import paths containing upper case letters are escaped to avoid troubles with case-insensitive file systems. This CL makes sure readers of the module index get the unescaped paths. Part of fixing golang/go#76310 Change-Id: If9a06b29f4f0f471c44839f4f842ee8a40df92c3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/723460 LUCI-TryBot-Result: Go LUCI Reviewed-by: Madeline Kalil --- internal/modindex/lookup.go | 8 +++++++- internal/modindex/lookup_test.go | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/modindex/lookup.go b/internal/modindex/lookup.go index 0c011a99b33..83bd49cd4bf 100644 --- a/internal/modindex/lookup.go +++ b/internal/modindex/lookup.go @@ -8,6 +8,8 @@ import ( "slices" "strconv" "strings" + + "golang.org/x/mod/module" ) type Candidate struct { @@ -104,11 +106,15 @@ func (ix *Index) Lookup(pkgName, name string, prefix bool) []Candidate { if len(flds) < 2 { continue // should never happen } + impPath, err := module.UnescapePath(e.ImportPath) + if err != nil { + continue + } px := Candidate{ PkgName: pkgName, Name: flds[0], Dir: string(e.Dir), - ImportPath: e.ImportPath, + ImportPath: impPath, Type: asLexType(flds[1][0]), Deprecated: len(flds[1]) > 1 && flds[1][1] == 'D', } diff --git a/internal/modindex/lookup_test.go b/internal/modindex/lookup_test.go index a0876ec3c6c..346695093a4 100644 --- a/internal/modindex/lookup_test.go +++ b/internal/modindex/lookup_test.go @@ -27,7 +27,7 @@ type titem struct { } var thedata = tdata{ - fname: "cloud.google.com/go/longrunning@v0.4.1/foo.go", + fname: "cloud.google.com/go/!longrunning@v0.4.1/foo.go", pkg: "foo", items: []titem{ // these need to be in alphabetical order @@ -84,6 +84,9 @@ func TestLookup(t *testing.T) { } // get all the symbols p := ix.Lookup("foo", "", true) + if len(p) > 0 && !strings.Contains(p[0].ImportPath, "Longrunning") { + t.Errorf("Failed to convert escaped path: %s", p[0].ImportPath) + } if len(p) != len(thedata.items) { t.Errorf("got %d possibilities for pkg foo, expected %d", len(p), len(thedata.items)) } From 98aa9a7d0bbb11ce97c8bd9e9112db87b19ecda5 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Sun, 23 Nov 2025 14:37:50 -0500 Subject: [PATCH 31/60] gopls/internal/cache: make unimported completions deterministic Completions found in the module cache are accumulated in a map, so that, without extra processing, the result can be nondeterministic. This CL fixes that, and also ensures that neither vendored nor internal packages will be recommended. This was tested by using elaborated versions of the example from the bug report. Completes the fix of golang/go#76310 Change-Id: I6bbc1f5737c4c5b70d1b6f50b250f76becb7ef95 Reviewed-on: https://go-review.googlesource.com/c/tools/+/723540 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/cache/source.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/gopls/internal/cache/source.go b/gopls/internal/cache/source.go index d0e57a797de..807f316b11e 100644 --- a/gopls/internal/cache/source.go +++ b/gopls/internal/cache/source.go @@ -71,9 +71,13 @@ func (s *goplsSource) ResolveReferences(ctx context.Context, filename string, mi if err != nil { return nil, err } - // trim cans to one per missing package. + // trim candidates to one per missing package. byPkgNm := make(map[string][]*result) for _, c := range fromCache { + // avoid internal and vendor + if !imports.CanUse(filename, c.res.Import.ImportPath) { + continue + } byPkgNm[c.res.Package.Name] = append(byPkgNm[c.res.Package.Name], c) } for k, v := range byPkgNm { @@ -166,7 +170,12 @@ func (s *goplsSource) resolveCacheReferences(missing imports.References) ([]*res } } - return moremaps.ValueSlice(found), nil + // return results in some deterministic order + got := moremaps.ValueSlice(found) + slices.SortFunc(got, func(a, b *result) int { + return strings.Compare(a.res.Import.ImportPath, b.res.Import.ImportPath) + }) + return got, nil } type found struct { From 7839abf5e8fc3af7864901871f0fee44c36e5938 Mon Sep 17 00:00:00 2001 From: Peter Weinberger Date: Tue, 25 Nov 2025 10:32:45 -0500 Subject: [PATCH 32/60] gopls/internal/metadata: document when Module can be nil Change-Id: Ibf9e94836ce808c9d29e057fc2d3de4767e0ddbc Reviewed-on: https://go-review.googlesource.com/c/tools/+/724300 LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/cache/metadata/metadata.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/cache/metadata/metadata.go b/gopls/internal/cache/metadata/metadata.go index ae847d579be..d68fed4aca6 100644 --- a/gopls/internal/cache/metadata/metadata.go +++ b/gopls/internal/cache/metadata/metadata.go @@ -56,7 +56,7 @@ type Package struct { Errors []packages.Error // must be set for packages in import cycles DepsByImpPath map[ImportPath]PackageID // may contain dups; empty ID => missing DepsByPkgPath map[PackagePath]PackageID // values are unique and non-empty - Module *packages.Module + Module *packages.Module // may be missing for std and cmd; see Go issue #65816. DepsErrors []*packagesinternal.PackageError LoadDir string // directory from which go/packages was run Standalone bool // package synthesized for a standalone file (e.g. ignore-tagged) From 6f1f89817d0dde814f436ccac9a21eb9e54fb56d Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 17 Nov 2025 13:03:14 -0500 Subject: [PATCH 33/60] internal/analysis/driverutil: include end positions in -json output + test For golang/go#73605 Change-Id: I4fcd7f81046075477aeb685cdfdbca8b258ed61c Reviewed-on: https://go-review.googlesource.com/c/tools/+/721060 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: Cherry Mui Reviewed-by: Michael Matloob --- go/analysis/internal/checker/testdata/json.txt | 5 +++-- internal/analysis/driverutil/print.go | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/go/analysis/internal/checker/testdata/json.txt b/go/analysis/internal/checker/testdata/json.txt index 8e6091aebbc..30a02c9fda3 100644 --- a/go/analysis/internal/checker/testdata/json.txt +++ b/go/analysis/internal/checker/testdata/json.txt @@ -1,8 +1,8 @@ # Test basic JSON output. -# -# File slashes assume non-Windows. +# File slashes assume non-Windows. skip GOOS=windows + checker -rename -json example.com/p exit 0 @@ -21,6 +21,7 @@ func f(bar int) {} "rename": [ { "posn": "/TMP/p/p.go:3:8", + "end": "/TMP/p/p.go:3:11", "message": "renaming \"bar\" to \"baz\"", "suggested_fixes": [ { diff --git a/internal/analysis/driverutil/print.go b/internal/analysis/driverutil/print.go index 7fc42a5ef7b..5458846857d 100644 --- a/internal/analysis/driverutil/print.go +++ b/internal/analysis/driverutil/print.go @@ -7,6 +7,7 @@ package driverutil // This file defined output helpers common to all drivers. import ( + "cmp" "encoding/json" "fmt" "go/token" @@ -76,11 +77,10 @@ type JSONSuggestedFix struct { } // A JSONDiagnostic describes the JSON schema of an analysis.Diagnostic. -// -// TODO(matloob): include End position if present. type JSONDiagnostic struct { Category string `json:"category,omitempty"` Posn string `json:"posn"` // e.g. "file.go:line:column" + End string `json:"end"` // (ditto) Message string `json:"message"` SuggestedFixes []JSONSuggestedFix `json:"suggested_fixes,omitempty"` Related []JSONRelatedInformation `json:"related,omitempty"` @@ -88,10 +88,9 @@ type JSONDiagnostic struct { // A JSONRelated describes a secondary position and message related to // a primary diagnostic. -// -// TODO(adonovan): include End position if present. type JSONRelatedInformation struct { Posn string `json:"posn"` // e.g. "file.go:line:column" + End string `json:"end"` // (ditto) Message string `json:"message"` } @@ -127,12 +126,14 @@ func (tree JSONTree) Add(fset *token.FileSet, id, name string, diags []analysis. for _, r := range f.Related { related = append(related, JSONRelatedInformation{ Posn: fset.Position(r.Pos).String(), + End: fset.Position(cmp.Or(r.End, r.Pos)).String(), Message: r.Message, }) } jdiag := JSONDiagnostic{ Category: f.Category, Posn: fset.Position(f.Pos).String(), + End: fset.Position(cmp.Or(f.End, f.Pos)).String(), Message: f.Message, SuggestedFixes: fixes, Related: related, From e31ed53b51641c5e73ffe1dde8faae4ba66f33f2 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 25 Nov 2025 12:24:00 -0500 Subject: [PATCH 34/60] internal/stdlib: regenerate Also, include about 500 interface methods, which we somehow overlooked. Notably, this includes the new iterators from CL 707356. For golang/go#66631 For golang/go#75693 Change-Id: I946bf4586c070128fe08bab75e4427141a0d516c Reviewed-on: https://go-review.googlesource.com/c/tools/+/724340 Reviewed-by: Madeline Kalil LUCI-TryBot-Result: Go LUCI --- internal/stdlib/deps.go | 626 +++++++++++++------------- internal/stdlib/generate.go | 26 +- internal/stdlib/manifest.go | 549 +++++++++++++++++++++- internal/stdlib/stdlib.go | 2 +- internal/stdlib/testdata/nethttp.deps | 4 +- 5 files changed, 877 insertions(+), 330 deletions(-) diff --git a/internal/stdlib/deps.go b/internal/stdlib/deps.go index 581784da435..f7b9c12865a 100644 --- a/internal/stdlib/deps.go +++ b/internal/stdlib/deps.go @@ -12,360 +12,364 @@ type pkginfo struct { } var deps = [...]pkginfo{ - {"archive/tar", "\x03n\x03E<\x01\n\x01$\x01\x01\x02\x05\b\x02\x01\x02\x02\f"}, - {"archive/zip", "\x02\x04d\a\x03\x12\x021<\x01+\x05\x01\x0f\x03\x02\x0e\x04"}, - {"bufio", "\x03n\x84\x01D\x14"}, - {"bytes", "q*Z\x03\fG\x02\x02"}, + {"archive/tar", "\x03p\x03F=\x01\n\x01$\x01\x01\x02\x05\b\x02\x01\x02\x02\f"}, + {"archive/zip", "\x02\x04f\a\x03\x13\x021=\x01+\x05\x01\x0f\x03\x02\x0e\x04"}, + {"bufio", "\x03p\x86\x01D\x14"}, + {"bytes", "s+[\x03\fG\x02\x02"}, {"cmp", ""}, - {"compress/bzip2", "\x02\x02\xf1\x01A"}, - {"compress/flate", "\x02o\x03\x81\x01\f\x033\x01\x03"}, - {"compress/gzip", "\x02\x04d\a\x03\x14mT"}, - {"compress/lzw", "\x02o\x03\x81\x01"}, - {"compress/zlib", "\x02\x04d\a\x03\x12\x01n"}, - {"container/heap", "\xb7\x02"}, + {"compress/bzip2", "\x02\x02\xf5\x01A"}, + {"compress/flate", "\x02q\x03\x83\x01\f\x033\x01\x03"}, + {"compress/gzip", "\x02\x04f\a\x03\x15nT"}, + {"compress/lzw", "\x02q\x03\x83\x01"}, + {"compress/zlib", "\x02\x04f\a\x03\x13\x01o"}, + {"container/heap", "\xbb\x02"}, {"container/list", ""}, {"container/ring", ""}, - {"context", "q[o\x01\r"}, - {"crypto", "\x86\x01oC"}, - {"crypto/aes", "\x10\n\t\x95\x02"}, - {"crypto/cipher", "\x03 \x01\x01\x1f\x11\x1c+Y"}, - {"crypto/des", "\x10\x15\x1f-+\x9c\x01\x03"}, - {"crypto/dsa", "D\x04)\x84\x01\r"}, - {"crypto/ecdh", "\x03\v\f\x10\x04\x16\x04\r\x1c\x84\x01"}, - {"crypto/ecdsa", "\x0e\x05\x03\x04\x01\x10\a\v\x06\x01\x04\f\x01\x1c\x84\x01\r\x05K\x01"}, - {"crypto/ed25519", "\x0e\x1e\x11\a\n\a\x1c\x84\x01C"}, - {"crypto/elliptic", "2?\x84\x01\r9"}, + {"context", "s\\p\x01\r"}, + {"crypto", "\x89\x01pC"}, + {"crypto/aes", "\x10\n\t\x99\x02"}, + {"crypto/cipher", "\x03 \x01\x01 \x12\x1c,Z"}, + {"crypto/des", "\x10\x15 .,\x9d\x01\x03"}, + {"crypto/dsa", "E\x04*\x86\x01\r"}, + {"crypto/ecdh", "\x03\v\f\x10\x04\x17\x04\x0e\x1c\x86\x01"}, + {"crypto/ecdsa", "\x0e\x05\x03\x04\x01\x10\b\v\x06\x01\x04\r\x01\x1c\x86\x01\r\x05K\x01"}, + {"crypto/ed25519", "\x0e\x1e\x12\a\v\a\x1c\x86\x01C"}, + {"crypto/elliptic", "3@\x86\x01\r9"}, {"crypto/fips140", "\"\x05"}, - {"crypto/hkdf", "/\x14\x01-\x15"}, - {"crypto/hmac", "\x1a\x16\x13\x01\x111"}, - {"crypto/internal/boring", "\x0e\x02\ri"}, - {"crypto/internal/boring/bbig", "\x1a\xe8\x01M"}, - {"crypto/internal/boring/bcache", "\xbc\x02\x13"}, + {"crypto/hkdf", "/\x15\x01.\x16"}, + {"crypto/hmac", "\x1a\x16\x14\x01\x122"}, + {"crypto/internal/boring", "\x0e\x02\rl"}, + {"crypto/internal/boring/bbig", "\x1a\xec\x01M"}, + {"crypto/internal/boring/bcache", "\xc0\x02\x13"}, {"crypto/internal/boring/sig", ""}, {"crypto/internal/constanttime", ""}, - {"crypto/internal/cryptotest", "\x03\r\n\b%\x0e\x19\x06\x12\x12 \x04\x06\t\x18\x01\x11\x11\x1b\x01\a\x05\b\x03\x05\v"}, - {"crypto/internal/entropy", "I"}, - {"crypto/internal/entropy/v1.0.0", "B/\x93\x018\x13"}, - {"crypto/internal/fips140", "A0\xbd\x01\v\x16"}, - {"crypto/internal/fips140/aes", "\x03\x1f\x03\x02\x13\x05\x01\x01\x06*\x93\x014"}, - {"crypto/internal/fips140/aes/gcm", "\"\x01\x02\x02\x02\x11\x05\x01\a*\x90\x01"}, - {"crypto/internal/fips140/alias", "\xcf\x02"}, - {"crypto/internal/fips140/bigmod", "'\x18\x01\a*\x93\x01"}, - {"crypto/internal/fips140/check", "\"\x0e\x06\t\x02\xb4\x01Z"}, - {"crypto/internal/fips140/check/checktest", "'\x87\x02!"}, - {"crypto/internal/fips140/drbg", "\x03\x1e\x01\x01\x04\x13\x05\t\x01(\x84\x01\x0f7\x01"}, - {"crypto/internal/fips140/ecdh", "\x03\x1f\x05\x02\t\r2\x84\x01\x0f7"}, - {"crypto/internal/fips140/ecdsa", "\x03\x1f\x04\x01\x02\a\x02\x069\x15oF"}, - {"crypto/internal/fips140/ed25519", "\x03\x1f\x05\x02\x04\v9\xc7\x01\x03"}, - {"crypto/internal/fips140/edwards25519", "\x1e\t\a\x112\x93\x017"}, - {"crypto/internal/fips140/edwards25519/field", "'\x13\x052\x93\x01"}, - {"crypto/internal/fips140/hkdf", "\x03\x1f\x05\t\x06;\x15"}, - {"crypto/internal/fips140/hmac", "\x03\x1f\x14\x01\x019\x15"}, - {"crypto/internal/fips140/mlkem", "\x03\x1f\x05\x02\x0e\x03\x052\xca\x01"}, - {"crypto/internal/fips140/nistec", "\x1e\t\f\f2\x93\x01*\r\x14"}, - {"crypto/internal/fips140/nistec/fiat", "'\x137\x93\x01"}, - {"crypto/internal/fips140/pbkdf2", "\x03\x1f\x05\t\x06;\x15"}, - {"crypto/internal/fips140/rsa", "\x03\x1b\x04\x04\x01\x02\r\x01\x01\x027\x15oF"}, - {"crypto/internal/fips140/sha256", "\x03\x1f\x1d\x01\a*\x15~"}, - {"crypto/internal/fips140/sha3", "\x03\x1f\x18\x05\x011\x93\x01K"}, - {"crypto/internal/fips140/sha512", "\x03\x1f\x1d\x01\a*\x15~"}, - {"crypto/internal/fips140/ssh", "'_"}, - {"crypto/internal/fips140/subtle", "\x1e\a\x1a\xc5\x01"}, - {"crypto/internal/fips140/tls12", "\x03\x1f\x05\t\x06\x029\x15"}, - {"crypto/internal/fips140/tls13", "\x03\x1f\x05\b\a\t2\x15"}, - {"crypto/internal/fips140cache", "\xae\x02\r&"}, + {"crypto/internal/cryptotest", "\x03\r\n\b&\x0f\x19\x06\x13\x12 \x04\x06\t\x19\x01\x11\x11\x1b\x01\a\x05\b\x03\x05\v"}, + {"crypto/internal/entropy", "J"}, + {"crypto/internal/entropy/v1.0.0", "C0\x95\x018\x13"}, + {"crypto/internal/fips140", "B1\xbf\x01\v\x16"}, + {"crypto/internal/fips140/aes", "\x03\x1f\x03\x02\x14\x05\x01\x01\x06+\x95\x014"}, + {"crypto/internal/fips140/aes/gcm", "\"\x01\x02\x02\x02\x12\x05\x01\a+\x92\x01"}, + {"crypto/internal/fips140/alias", "\xd3\x02"}, + {"crypto/internal/fips140/bigmod", "'\x19\x01\a+\x95\x01"}, + {"crypto/internal/fips140/check", "\"\x0e\a\t\x02\xb7\x01Z"}, + {"crypto/internal/fips140/check/checktest", "'\x8b\x02!"}, + {"crypto/internal/fips140/drbg", "\x03\x1e\x01\x01\x04\x14\x05\t\x01)\x86\x01\x0f7\x01"}, + {"crypto/internal/fips140/ecdh", "\x03\x1f\x05\x02\n\r3\x86\x01\x0f7"}, + {"crypto/internal/fips140/ecdsa", "\x03\x1f\x04\x01\x02\a\x03\x06:\x16pF"}, + {"crypto/internal/fips140/ed25519", "\x03\x1f\x05\x02\x04\f:\xc9\x01\x03"}, + {"crypto/internal/fips140/edwards25519", "\x1e\t\a\x123\x95\x017"}, + {"crypto/internal/fips140/edwards25519/field", "'\x14\x053\x95\x01"}, + {"crypto/internal/fips140/hkdf", "\x03\x1f\x05\t\a<\x16"}, + {"crypto/internal/fips140/hmac", "\x03\x1f\x15\x01\x01:\x16"}, + {"crypto/internal/fips140/mldsa", "\x03\x1b\x04\x05\x02\x0e\x01\x03\x053\x95\x017"}, + {"crypto/internal/fips140/mlkem", "\x03\x1f\x05\x02\x0f\x03\x053\xcc\x01"}, + {"crypto/internal/fips140/nistec", "\x1e\t\r\f3\x95\x01*\r\x14"}, + {"crypto/internal/fips140/nistec/fiat", "'\x148\x95\x01"}, + {"crypto/internal/fips140/pbkdf2", "\x03\x1f\x05\t\a<\x16"}, + {"crypto/internal/fips140/rsa", "\x03\x1b\x04\x04\x01\x02\x0e\x01\x01\x028\x16pF"}, + {"crypto/internal/fips140/sha256", "\x03\x1f\x1e\x01\a+\x16\x7f"}, + {"crypto/internal/fips140/sha3", "\x03\x1f\x19\x05\x012\x95\x01K"}, + {"crypto/internal/fips140/sha512", "\x03\x1f\x1e\x01\a+\x16\x7f"}, + {"crypto/internal/fips140/ssh", "'b"}, + {"crypto/internal/fips140/subtle", "\x1e\a\x1b\xc8\x01"}, + {"crypto/internal/fips140/tls12", "\x03\x1f\x05\t\a\x02:\x16"}, + {"crypto/internal/fips140/tls13", "\x03\x1f\x05\b\b\t3\x16"}, + {"crypto/internal/fips140cache", "\xb2\x02\r&"}, {"crypto/internal/fips140deps", ""}, - {"crypto/internal/fips140deps/byteorder", "\x9c\x01"}, - {"crypto/internal/fips140deps/cpu", "\xb1\x01\a"}, - {"crypto/internal/fips140deps/godebug", "\xb9\x01"}, - {"crypto/internal/fips140deps/time", "\xc9\x02"}, - {"crypto/internal/fips140hash", "7\x1c3\xc9\x01"}, - {"crypto/internal/fips140only", ")\r\x01\x01N3<"}, + {"crypto/internal/fips140deps/byteorder", "\x9f\x01"}, + {"crypto/internal/fips140deps/cpu", "\xb4\x01\a"}, + {"crypto/internal/fips140deps/godebug", "\xbc\x01"}, + {"crypto/internal/fips140deps/time", "\xcd\x02"}, + {"crypto/internal/fips140hash", "8\x1d4\xca\x01"}, + {"crypto/internal/fips140only", ")\x0e\x01\x01P3="}, {"crypto/internal/fips140test", ""}, - {"crypto/internal/hpke", "\x0e\x01\x01\x03\x056#+hM"}, - {"crypto/internal/impl", "\xb9\x02"}, - {"crypto/internal/randutil", "\xf5\x01\x12"}, - {"crypto/internal/sysrand", "qo! \r\r\x01\x01\f\x06"}, - {"crypto/internal/sysrand/internal/seccomp", "q"}, - {"crypto/md5", "\x0e6-\x15\x16h"}, - {"crypto/mlkem", "1"}, - {"crypto/pbkdf2", "4\x0f\x01-\x15"}, - {"crypto/rand", "\x1a\b\a\x1b\x04\x01(\x84\x01\rM"}, - {"crypto/rc4", "%\x1f-\xc7\x01"}, - {"crypto/rsa", "\x0e\f\x01\v\x0f\x0e\x01\x04\x06\a\x1c\x03\x123<\f\x01"}, - {"crypto/sha1", "\x0e\f*\x03*\x15\x16\x15S"}, - {"crypto/sha256", "\x0e\f\x1cP"}, - {"crypto/sha3", "\x0e)O\xc9\x01"}, - {"crypto/sha512", "\x0e\f\x1eN"}, - {"crypto/subtle", "\x1e\x1c\x9c\x01X"}, - {"crypto/tls", "\x03\b\x02\x01\x01\x01\x01\x02\x01\x01\x01\x02\x01\x01\t\x01\r\n\x01\n\x05\x03\x01\x01\x01\x01\x02\x01\x02\x01\x17\x02\x03\x12\x16\x15\b<\x16\x16\r\b\x01\x01\x01\x02\x01\r\x06\x02\x01\x0f"}, - {"crypto/tls/internal/fips140tls", "\x17\xa5\x02"}, - {"crypto/x509", "\x03\v\x01\x01\x01\x01\x01\x01\x01\x015\x05\x01\x01\x02\x05\x0e\x06\x02\x02\x03E\x039\x01\x02\b\x01\x01\x02\a\x10\x05\x01\x06\x02\x05\b\x02\x01\x02\x0e\x02\x01\x01\x02\x03\x01"}, - {"crypto/x509/pkix", "g\x06\a\x8e\x01G"}, - {"database/sql", "\x03\nN\x16\x03\x81\x01\v\a\"\x05\b\x02\x03\x01\r\x02\x02\x02"}, - {"database/sql/driver", "\rd\x03\xb5\x01\x0f\x11"}, - {"debug/buildinfo", "\x03[\x02\x01\x01\b\a\x03e\x1a\x02\x01+\x0f\x1f"}, - {"debug/dwarf", "\x03g\a\x03\x81\x011\x11\x01\x01"}, - {"debug/elf", "\x03\x06T\r\a\x03e\x1b\x01\f \x17\x01\x16"}, - {"debug/gosym", "\x03g\n\xc3\x01\x01\x01\x02"}, - {"debug/macho", "\x03\x06T\r\ne\x1c,\x17\x01"}, - {"debug/pe", "\x03\x06T\r\a\x03e\x1c,\x17\x01\x16"}, - {"debug/plan9obj", "j\a\x03e\x1c,"}, - {"embed", "q*A\x19\x01S"}, + {"crypto/internal/hpke", "\x03\v\x01\x01\x03\x055\x03\x04\x01\x01\x16\a\x03\x13\xcc\x01"}, + {"crypto/internal/impl", "\xbd\x02"}, + {"crypto/internal/randutil", "\xf9\x01\x12"}, + {"crypto/internal/sysrand", "sq! \r\r\x01\x01\f\x06"}, + {"crypto/internal/sysrand/internal/seccomp", "s"}, + {"crypto/md5", "\x0e7.\x16\x16i"}, + {"crypto/mlkem", "\x0e$"}, + {"crypto/mlkem/mlkemtest", "2\x1b&"}, + {"crypto/pbkdf2", "5\x0f\x01.\x16"}, + {"crypto/rand", "\x1a\b\a\x1c\x04\x01)\x86\x01\rM"}, + {"crypto/rc4", "% .\xc9\x01"}, + {"crypto/rsa", "\x0e\f\x01\v\x10\x0e\x01\x04\a\a\x1c\x03\x133=\f\x01"}, + {"crypto/sha1", "\x0e\f+\x03+\x16\x16\x15T"}, + {"crypto/sha256", "\x0e\f\x1dR"}, + {"crypto/sha3", "\x0e*Q\xca\x01"}, + {"crypto/sha512", "\x0e\f\x1fP"}, + {"crypto/subtle", "\x1e\x1d\x9f\x01X"}, + {"crypto/tls", "\x03\b\x02\x01\x01\x01\x01\x02\x01\x01\x01\x02\x01\x01\t\x01\x0e\n\x01\n\x05\x04\x01\x01\x01\x01\x02\x01\x02\x01\x17\x02\x03\x13\x16\x15\b=\x16\x16\r\b\x01\x01\x01\x02\x01\r\x06\x02\x01\x0f"}, + {"crypto/tls/internal/fips140tls", "\x17\xa9\x02"}, + {"crypto/x509", "\x03\v\x01\x01\x01\x01\x01\x01\x01\x016\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\x0e\x02\x01\x01\x02\x03\x01"}, + {"crypto/x509/pkix", "i\x06\a\x90\x01G"}, + {"database/sql", "\x03\nP\x16\x03\x83\x01\v\a\"\x05\b\x02\x03\x01\r\x02\x02\x02"}, + {"database/sql/driver", "\rf\x03\xb7\x01\x0f\x11"}, + {"debug/buildinfo", "\x03]\x02\x01\x01\b\a\x03g\x1a\x02\x01+\x0f\x1f"}, + {"debug/dwarf", "\x03i\a\x03\x83\x011\x11\x01\x01"}, + {"debug/elf", "\x03\x06V\r\a\x03g\x1b\x01\f \x17\x01\x16"}, + {"debug/gosym", "\x03i\n\xc5\x01\x01\x01\x02"}, + {"debug/macho", "\x03\x06V\r\ng\x1c,\x17\x01"}, + {"debug/pe", "\x03\x06V\r\a\x03g\x1c,\x17\x01\x16"}, + {"debug/plan9obj", "l\a\x03g\x1c,"}, + {"embed", "s+B\x19\x01S"}, {"embed/internal/embedtest", ""}, {"encoding", ""}, - {"encoding/ascii85", "\xf5\x01C"}, - {"encoding/asn1", "\x03n\x03e(\x01'\r\x02\x01\x10\x03\x01"}, - {"encoding/base32", "\xf5\x01A\x02"}, - {"encoding/base64", "\x9c\x01YA\x02"}, - {"encoding/binary", "q\x84\x01\f(\r\x05"}, - {"encoding/csv", "\x02\x01n\x03\x81\x01D\x12\x02"}, - {"encoding/gob", "\x02c\x05\a\x03e\x1c\v\x01\x03\x1d\b\x12\x01\x0f\x02"}, - {"encoding/hex", "q\x03\x81\x01A\x03"}, - {"encoding/json", "\x03\x01a\x04\b\x03\x81\x01\f(\r\x02\x01\x02\x10\x01\x01\x02"}, - {"encoding/pem", "\x03f\b\x84\x01A\x03"}, - {"encoding/xml", "\x02\x01b\f\x03\x81\x014\x05\n\x01\x02\x10\x02"}, - {"errors", "\xcc\x01\x83\x01"}, - {"expvar", "nK@\b\v\x15\r\b\x02\x03\x01\x11"}, - {"flag", "e\f\x03\x81\x01,\b\x05\b\x02\x01\x10"}, - {"fmt", "qE&\x19\f \b\r\x02\x03\x12"}, - {"go/ast", "\x03\x01p\x0e\x01r\x03)\b\r\x02\x01\x12\x02"}, - {"go/build", "\x02\x01n\x03\x01\x02\x02\a\x02\x01\x17\x1f\x04\x02\b\x1b\x13\x01+\x01\x04\x01\a\b\x02\x01\x12\x02\x02"}, - {"go/build/constraint", "q\xc7\x01\x01\x12\x02"}, - {"go/constant", "t\x0f~\x01\x024\x01\x02\x12"}, - {"go/doc", "\x04p\x01\x05\t=51\x10\x02\x01\x12\x02"}, - {"go/doc/comment", "\x03q\xc2\x01\x01\x01\x01\x12\x02"}, - {"go/format", "\x03q\x01\v\x01\x02rD"}, - {"go/importer", "v\a\x01\x01\x04\x01q9"}, - {"go/internal/gccgoimporter", "\x02\x01[\x13\x03\x04\v\x01o\x02,\x01\x05\x11\x01\f\b"}, - {"go/internal/gcimporter", "\x02r\x0f\x010\x05\r/,\x15\x03\x02"}, - {"go/internal/srcimporter", "t\x01\x01\n\x03\x01q,\x01\x05\x12\x02\x14"}, - {"go/parser", "\x03n\x03\x01\x02\v\x01r\x01+\x06\x12"}, - {"go/printer", "t\x01\x02\x03\tr\f \x15\x02\x01\x02\v\x05\x02"}, - {"go/scanner", "\x03q\x0fr2\x10\x01\x13\x02"}, - {"go/token", "\x04p\x84\x01>\x02\x03\x01\x0f\x02"}, - {"go/types", "\x03\x01\x06g\x03\x01\x03\b\x03\x024\x062\x04\x03\t \x06\a\b\x01\x01\x01\x02\x01\x0f\x02\x02"}, - {"go/version", "\xbe\x01{"}, - {"hash", "\xf5\x01"}, - {"hash/adler32", "q\x15\x16"}, - {"hash/crc32", "q\x15\x16\x15\x8a\x01\x01\x13"}, - {"hash/crc64", "q\x15\x16\x9f\x01"}, - {"hash/fnv", "q\x15\x16h"}, - {"hash/maphash", "\x86\x01\x11<|"}, - {"html", "\xb9\x02\x02\x12"}, - {"html/template", "\x03k\x06\x18-<\x01\n!\x05\x01\x02\x03\f\x01\x02\f\x01\x03\x02"}, - {"image", "\x02o\x1ef\x0f4\x03\x01"}, + {"encoding/ascii85", "\xf9\x01C"}, + {"encoding/asn1", "\x03p\x03g(\x01'\r\x02\x01\x10\x03\x01"}, + {"encoding/base32", "\xf9\x01A\x02"}, + {"encoding/base64", "\x9f\x01ZA\x02"}, + {"encoding/binary", "s\x86\x01\f(\r\x05"}, + {"encoding/csv", "\x02\x01p\x03\x83\x01D\x12\x02"}, + {"encoding/gob", "\x02e\x05\a\x03g\x1c\v\x01\x03\x1d\b\x12\x01\x0f\x02"}, + {"encoding/hex", "s\x03\x83\x01A\x03"}, + {"encoding/json", "\x03\x01c\x04\b\x03\x83\x01\f(\r\x02\x01\x02\x10\x01\x01\x02"}, + {"encoding/pem", "\x03h\b\x86\x01A\x03"}, + {"encoding/xml", "\x02\x01d\f\x03\x83\x014\x05\n\x01\x02\x10\x02"}, + {"errors", "\xcf\x01\x84\x01"}, + {"expvar", "pLA\b\v\x15\r\b\x02\x03\x01\x11"}, + {"flag", "g\f\x03\x83\x01,\b\x05\b\x02\x01\x10"}, + {"fmt", "sF'\x19\f \b\r\x02\x03\x12"}, + {"go/ast", "\x03\x01r\x0f\x01s\x03)\b\r\x02\x01\x12\x02"}, + {"go/build", "\x02\x01p\x03\x01\x02\x02\b\x02\x01\x17\x1f\x04\x02\b\x1c\x13\x01+\x01\x04\x01\a\b\x02\x01\x12\x02\x02"}, + {"go/build/constraint", "s\xc9\x01\x01\x12\x02"}, + {"go/constant", "v\x10\x7f\x01\x024\x01\x02\x12"}, + {"go/doc", "\x04r\x01\x05\n=61\x10\x02\x01\x12\x02"}, + {"go/doc/comment", "\x03s\xc4\x01\x01\x01\x01\x12\x02"}, + {"go/format", "\x03s\x01\f\x01\x02sD"}, + {"go/importer", "x\a\x01\x02\x04\x01r9"}, + {"go/internal/gccgoimporter", "\x02\x01]\x13\x03\x04\f\x01p\x02,\x01\x05\x11\x01\f\b"}, + {"go/internal/gcimporter", "\x02t\x10\x010\x05\r0,\x15\x03\x02"}, + {"go/internal/scannerhooks", "\x86\x01"}, + {"go/internal/srcimporter", "v\x01\x01\v\x03\x01r,\x01\x05\x12\x02\x14"}, + {"go/parser", "\x03p\x03\x01\x02\b\x04\x01s\x01+\x06\x12"}, + {"go/printer", "v\x01\x02\x03\ns\f \x15\x02\x01\x02\v\x05\x02"}, + {"go/scanner", "\x03s\v\x05s2\x10\x01\x13\x02"}, + {"go/token", "\x04r\x86\x01>\x02\x03\x01\x0f\x02"}, + {"go/types", "\x03\x01\x06i\x03\x01\x03\t\x03\x024\x063\x04\x03\t \x06\a\b\x01\x01\x01\x02\x01\x0f\x02\x02"}, + {"go/version", "\xc1\x01|"}, + {"hash", "\xf9\x01"}, + {"hash/adler32", "s\x16\x16"}, + {"hash/crc32", "s\x16\x16\x15\x8b\x01\x01\x13"}, + {"hash/crc64", "s\x16\x16\xa0\x01"}, + {"hash/fnv", "s\x16\x16i"}, + {"hash/maphash", "\x89\x01\x11<}"}, + {"html", "\xbd\x02\x02\x12"}, + {"html/template", "\x03m\x06\x19-=\x01\n!\x05\x01\x02\x03\f\x01\x02\f\x01\x03\x02"}, + {"image", "\x02q\x1fg\x0f4\x03\x01"}, {"image/color", ""}, - {"image/color/palette", "\x8f\x01"}, - {"image/draw", "\x8e\x01\x01\x04"}, - {"image/gif", "\x02\x01\x05i\x03\x1a\x01\x01\x01\vY"}, - {"image/internal/imageutil", "\x8e\x01"}, - {"image/jpeg", "\x02o\x1d\x01\x04b"}, - {"image/png", "\x02\aa\n\x12\x02\x06\x01fC"}, - {"index/suffixarray", "\x03g\a\x84\x01\f+\n\x01"}, - {"internal/abi", "\xb8\x01\x97\x01"}, - {"internal/asan", "\xcf\x02"}, - {"internal/bisect", "\xae\x02\r\x01"}, - {"internal/buildcfg", "tGf\x06\x02\x05\n\x01"}, - {"internal/bytealg", "\xb1\x01\x9e\x01"}, + {"image/color/palette", "\x92\x01"}, + {"image/draw", "\x91\x01\x01\x04"}, + {"image/gif", "\x02\x01\x05k\x03\x1b\x01\x01\x01\vZ\x0f"}, + {"image/internal/imageutil", "\x91\x01"}, + {"image/jpeg", "\x02q\x1e\x01\x04c"}, + {"image/png", "\x02\ac\n\x13\x02\x06\x01gC"}, + {"index/suffixarray", "\x03i\a\x86\x01\f+\n\x01"}, + {"internal/abi", "\xbb\x01\x98\x01"}, + {"internal/asan", "\xd3\x02"}, + {"internal/bisect", "\xb2\x02\r\x01"}, + {"internal/buildcfg", "vHg\x06\x02\x05\n\x01"}, + {"internal/bytealg", "\xb4\x01\x9f\x01"}, {"internal/byteorder", ""}, {"internal/cfg", ""}, - {"internal/cgrouptest", "tZS\x06\x0f\x02\x01\x04\x01"}, - {"internal/chacha8rand", "\x9c\x01\x15\a\x97\x01"}, + {"internal/cgrouptest", "v[T\x06\x0f\x02\x01\x04\x01"}, + {"internal/chacha8rand", "\x9f\x01\x15\a\x98\x01"}, {"internal/copyright", ""}, {"internal/coverage", ""}, {"internal/coverage/calloc", ""}, - {"internal/coverage/cfile", "n\x06\x16\x17\x01\x02\x01\x01\x01\x01\x01\x01\x01\"\x02&,\x06\a\n\x01\x03\r\x06"}, - {"internal/coverage/cformat", "\x04p-\x04P\v6\x01\x02\r"}, - {"internal/coverage/cmerge", "t-`"}, - {"internal/coverage/decodecounter", "j\n-\v\x02G,\x17\x17"}, - {"internal/coverage/decodemeta", "\x02h\n\x16\x17\v\x02G,"}, - {"internal/coverage/encodecounter", "\x02h\n-\f\x01\x02E\v!\x15"}, - {"internal/coverage/encodemeta", "\x02\x01g\n\x12\x04\x17\r\x02E,."}, - {"internal/coverage/pods", "\x04p-\x80\x01\x06\x05\n\x02\x01"}, - {"internal/coverage/rtcov", "\xcf\x02"}, - {"internal/coverage/slicereader", "j\n\x81\x01Z"}, - {"internal/coverage/slicewriter", "t\x81\x01"}, - {"internal/coverage/stringtab", "t8\x04E"}, + {"internal/coverage/cfile", "p\x06\x17\x17\x01\x02\x01\x01\x01\x01\x01\x01\x01\"\x02',\x06\a\n\x01\x03\r\x06"}, + {"internal/coverage/cformat", "\x04r.\x04Q\v6\x01\x02\r"}, + {"internal/coverage/cmerge", "v.a"}, + {"internal/coverage/decodecounter", "l\n.\v\x02H,\x17\x17"}, + {"internal/coverage/decodemeta", "\x02j\n\x17\x17\v\x02H,"}, + {"internal/coverage/encodecounter", "\x02j\n.\f\x01\x02F\v!\x15"}, + {"internal/coverage/encodemeta", "\x02\x01i\n\x13\x04\x17\r\x02F,."}, + {"internal/coverage/pods", "\x04r.\x81\x01\x06\x05\n\x02\x01"}, + {"internal/coverage/rtcov", "\xd3\x02"}, + {"internal/coverage/slicereader", "l\n\x83\x01Z"}, + {"internal/coverage/slicewriter", "v\x83\x01"}, + {"internal/coverage/stringtab", "v9\x04F"}, {"internal/coverage/test", ""}, {"internal/coverage/uleb128", ""}, - {"internal/cpu", "\xcf\x02"}, - {"internal/dag", "\x04p\xc2\x01\x03"}, - {"internal/diff", "\x03q\xc3\x01\x02"}, - {"internal/exportdata", "\x02\x01n\x03\x02c\x1c,\x01\x05\x11\x01\x02"}, - {"internal/filepathlite", "q*A\x1a@"}, - {"internal/fmtsort", "\x04\xa5\x02\r"}, - {"internal/fuzz", "\x03\nE\x18\x04\x03\x03\x01\v\x036<\f\x03\x1d\x01\x05\x02\x05\n\x01\x02\x01\x01\f\x04\x02"}, + {"internal/cpu", "\xd3\x02"}, + {"internal/dag", "\x04r\xc4\x01\x03"}, + {"internal/diff", "\x03s\xc5\x01\x02"}, + {"internal/exportdata", "\x02\x01p\x03\x02e\x1c,\x01\x05\x11\x01\x02"}, + {"internal/filepathlite", "s+B\x1a@"}, + {"internal/fmtsort", "\x04\xa9\x02\r"}, + {"internal/fuzz", "\x03\nG\x18\x04\x03\x03\x01\f\x036=\f\x03\x1d\x01\x05\x02\x05\n\x01\x02\x01\x01\f\x04\x02"}, {"internal/goarch", ""}, - {"internal/godebug", "\x99\x01!\x81\x01\x01\x13"}, + {"internal/godebug", "\x9c\x01!\x82\x01\x01\x13"}, {"internal/godebugs", ""}, {"internal/goexperiment", ""}, {"internal/goos", ""}, - {"internal/goroot", "\xa1\x02\x01\x05\x12\x02"}, + {"internal/goroot", "\xa5\x02\x01\x05\x12\x02"}, {"internal/gover", "\x04"}, {"internal/goversion", ""}, - {"internal/lazyregexp", "\xa1\x02\v\r\x02"}, - {"internal/lazytemplate", "\xf5\x01,\x18\x02\f"}, - {"internal/msan", "\xcf\x02"}, + {"internal/lazyregexp", "\xa5\x02\v\r\x02"}, + {"internal/lazytemplate", "\xf9\x01,\x18\x02\f"}, + {"internal/msan", "\xd3\x02"}, {"internal/nettrace", ""}, - {"internal/obscuretestdata", "i\x8c\x01,"}, - {"internal/oserror", "q"}, - {"internal/pkgbits", "\x03O\x18\a\x03\x04\vr\r\x1f\r\n\x01"}, + {"internal/obscuretestdata", "k\x8e\x01,"}, + {"internal/oserror", "s"}, + {"internal/pkgbits", "\x03Q\x18\a\x03\x04\fs\r\x1f\r\n\x01"}, {"internal/platform", ""}, - {"internal/poll", "qj\x05\x159\r\x01\x01\f\x06"}, - {"internal/profile", "\x03\x04j\x03\x81\x017\n\x01\x01\x01\x10"}, + {"internal/poll", "sl\x05\x159\r\x01\x01\f\x06"}, + {"internal/profile", "\x03\x04l\x03\x83\x017\n\x01\x01\x01\x10"}, {"internal/profilerecord", ""}, - {"internal/race", "\x97\x01\xb8\x01"}, - {"internal/reflectlite", "\x97\x01!:\x16"}, - {"vendor/golang.org/x/text/unicode/norm", "j\n\x81\x01F\x12\x11"}, - {"weak", "\x97\x01\x97\x01!"}, + {"vendor/golang.org/x/crypto/internal/alias", "\xd3\x02"}, + {"vendor/golang.org/x/crypto/internal/poly1305", "W\x15\x9c\x01"}, + {"vendor/golang.org/x/net/dns/dnsmessage", "s\xc7\x01"}, + {"vendor/golang.org/x/net/http/httpguts", "\x8f\x02\x14\x1a\x14\r"}, + {"vendor/golang.org/x/net/http/httpproxy", "s\x03\x99\x01\x10\x05\x01\x18\x14\r"}, + {"vendor/golang.org/x/net/http2/hpack", "\x03p\x03\x83\x01F"}, + {"vendor/golang.org/x/net/idna", "v\x8f\x018\x14\x10\x02\x01"}, + {"vendor/golang.org/x/net/nettest", "\x03i\a\x03\x83\x01\x11\x05\x16\x01\f\n\x01\x02\x02\x01\v"}, + {"vendor/golang.org/x/sys/cpu", "\xa5\x02\r\n\x01\x16"}, + {"vendor/golang.org/x/text/secure/bidirule", "s\xde\x01\x11\x01"}, + {"vendor/golang.org/x/text/transform", "\x03p\x86\x01X"}, + {"vendor/golang.org/x/text/unicode/bidi", "\x03\bk\x87\x01>\x16"}, + {"vendor/golang.org/x/text/unicode/norm", "l\n\x83\x01F\x12\x11"}, + {"weak", "\x9a\x01\x98\x01!"}, } // bootstrap is the list of bootstrap packages extracted from cmd/dist. @@ -385,6 +389,7 @@ var bootstrap = map[string]bool{ "cmd/compile/internal/arm64": true, "cmd/compile/internal/base": true, "cmd/compile/internal/bitvec": true, + "cmd/compile/internal/bloop": true, "cmd/compile/internal/compare": true, "cmd/compile/internal/coverage": true, "cmd/compile/internal/deadlocals": true, @@ -413,6 +418,7 @@ var bootstrap = map[string]bool{ "cmd/compile/internal/riscv64": true, "cmd/compile/internal/rttype": true, "cmd/compile/internal/s390x": true, + "cmd/compile/internal/slice": true, "cmd/compile/internal/ssa": true, "cmd/compile/internal/ssagen": true, "cmd/compile/internal/staticdata": true, diff --git a/internal/stdlib/generate.go b/internal/stdlib/generate.go index e54ebf2c028..0ecf92b9870 100644 --- a/internal/stdlib/generate.go +++ b/internal/stdlib/generate.go @@ -119,13 +119,32 @@ func manifest(apidir string) { if _, recv, ok := strings.Cut(kind, "method "); ok { // e.g. "method (*Func) Pos() token.Pos" - kind = "method" + kind = "method" // (concrete) recv := removeTypeParam(recv) // (*Foo[T]) -> (*Foo) sym = recv + "." + sym // (*T).m - } else if _, field, ok := strings.Cut(rest, " struct, "); ok && kind == "type" { + } else if method, ok := strings.CutPrefix(rest, " interface, "); ok && kind == "type" { + // e.g. "pkg reflect, type Type interface, Comparable() bool" + // or "pkg net, type Error interface, Temporary //deprecated" + + kind = "method" // (abstract) + + if strings.HasPrefix(method, "unexported methods") { + continue + } + if strings.Contains(method, " //deprecated") { + continue + } + name, _, ok := strings.Cut(method, "(") + if !ok { + log.Printf("unexpected: %s", line) + continue + } + sym = fmt.Sprintf("(%s).%s", sym, name) // (T).m + + } else if field, ok := strings.CutPrefix(rest, " struct, "); ok && kind == "type" { // e.g. "type ParenExpr struct, Lparen token.Pos" kind = "field" name, typ, _ := strings.Cut(field, " ") @@ -156,6 +175,9 @@ func manifest(apidir string) { // enums are redeclared in later versions // as their encoding changes; // deprecations count as updates too. + // TODO(adonovan): it would be better to mark + // deprecated as a boolean without changing the + // version. if _, ok := symbols[sym]; !ok { var sig string if kind == "func" { diff --git a/internal/stdlib/manifest.go b/internal/stdlib/manifest.go index 362f23c436c..f1e24625a7a 100644 --- a/internal/stdlib/manifest.go +++ b/internal/stdlib/manifest.go @@ -16,6 +16,14 @@ var PackageSymbols = map[string][]Symbol{ {"(*Writer).Flush", Method, 0, ""}, {"(*Writer).Write", Method, 0, ""}, {"(*Writer).WriteHeader", Method, 0, ""}, + {"(FileInfoNames).Gname", Method, 23, ""}, + {"(FileInfoNames).IsDir", Method, 23, ""}, + {"(FileInfoNames).ModTime", Method, 23, ""}, + {"(FileInfoNames).Mode", Method, 23, ""}, + {"(FileInfoNames).Name", Method, 23, ""}, + {"(FileInfoNames).Size", Method, 23, ""}, + {"(FileInfoNames).Sys", Method, 23, ""}, + {"(FileInfoNames).Uname", Method, 23, ""}, {"(Format).String", Method, 10, ""}, {"ErrFieldTooLong", Var, 0, ""}, {"ErrHeader", Var, 0, ""}, @@ -338,6 +346,9 @@ var PackageSymbols = map[string][]Symbol{ {"(*Writer).Write", Method, 0, ""}, {"(CorruptInputError).Error", Method, 0, ""}, {"(InternalError).Error", Method, 0, ""}, + {"(Reader).Read", Method, 0, ""}, + {"(Reader).ReadByte", Method, 0, ""}, + {"(Resetter).Reset", Method, 4, ""}, {"BestCompression", Const, 0, ""}, {"BestSpeed", Const, 0, ""}, {"CorruptInputError", Type, 0, ""}, @@ -409,6 +420,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Writer).Flush", Method, 0, ""}, {"(*Writer).Reset", Method, 2, ""}, {"(*Writer).Write", Method, 0, ""}, + {"(Resetter).Reset", Method, 4, ""}, {"BestCompression", Const, 0, ""}, {"BestSpeed", Const, 0, ""}, {"DefaultCompression", Const, 0, ""}, @@ -426,6 +438,11 @@ var PackageSymbols = map[string][]Symbol{ {"Writer", Type, 0, ""}, }, "container/heap": { + {"(Interface).Len", Method, 0, ""}, + {"(Interface).Less", Method, 0, ""}, + {"(Interface).Pop", Method, 0, ""}, + {"(Interface).Push", Method, 0, ""}, + {"(Interface).Swap", Method, 0, ""}, {"Fix", Func, 2, "func(h Interface, i int)"}, {"Init", Func, 0, "func(h Interface)"}, {"Interface", Type, 0, ""}, @@ -469,6 +486,10 @@ var PackageSymbols = map[string][]Symbol{ {"Ring.Value", Field, 0, ""}, }, "context": { + {"(Context).Deadline", Method, 7, ""}, + {"(Context).Done", Method, 7, ""}, + {"(Context).Err", Method, 7, ""}, + {"(Context).Value", Method, 7, ""}, {"AfterFunc", Func, 21, "func(ctx Context, f func()) (stop func() bool)"}, {"Background", Func, 7, "func() Context"}, {"CancelCauseFunc", Type, 20, ""}, @@ -488,17 +509,31 @@ var PackageSymbols = map[string][]Symbol{ {"WithoutCancel", Func, 21, "func(parent Context) Context"}, }, "crypto": { + {"(Decapsulator).Decapsulate", Method, 26, ""}, + {"(Decapsulator).Encapsulator", Method, 26, ""}, + {"(Decrypter).Decrypt", Method, 5, ""}, + {"(Decrypter).Public", Method, 5, ""}, + {"(Encapsulator).Bytes", Method, 26, ""}, + {"(Encapsulator).Encapsulate", Method, 26, ""}, {"(Hash).Available", Method, 0, ""}, {"(Hash).HashFunc", Method, 4, ""}, {"(Hash).New", Method, 0, ""}, {"(Hash).Size", Method, 0, ""}, {"(Hash).String", Method, 15, ""}, + {"(MessageSigner).Public", Method, 25, ""}, + {"(MessageSigner).Sign", Method, 25, ""}, + {"(MessageSigner).SignMessage", Method, 25, ""}, + {"(Signer).Public", Method, 4, ""}, + {"(Signer).Sign", Method, 4, ""}, + {"(SignerOpts).HashFunc", Method, 4, ""}, {"BLAKE2b_256", Const, 9, ""}, {"BLAKE2b_384", Const, 9, ""}, {"BLAKE2b_512", Const, 9, ""}, {"BLAKE2s_256", Const, 9, ""}, + {"Decapsulator", Type, 26, ""}, {"Decrypter", Type, 5, ""}, {"DecrypterOpts", Type, 5, ""}, + {"Encapsulator", Type, 26, ""}, {"Hash", Type, 0, ""}, {"MD4", Const, 0, ""}, {"MD5", Const, 0, ""}, @@ -530,6 +565,16 @@ var PackageSymbols = map[string][]Symbol{ {"NewCipher", Func, 0, "func(key []byte) (cipher.Block, error)"}, }, "crypto/cipher": { + {"(AEAD).NonceSize", Method, 2, ""}, + {"(AEAD).Open", Method, 2, ""}, + {"(AEAD).Overhead", Method, 2, ""}, + {"(AEAD).Seal", Method, 2, ""}, + {"(Block).BlockSize", Method, 0, ""}, + {"(Block).Decrypt", Method, 0, ""}, + {"(Block).Encrypt", Method, 0, ""}, + {"(BlockMode).BlockSize", Method, 0, ""}, + {"(BlockMode).CryptBlocks", Method, 0, ""}, + {"(Stream).XORKeyStream", Method, 0, ""}, {"(StreamReader).Read", Method, 0, ""}, {"(StreamWriter).Close", Method, 0, ""}, {"(StreamWriter).Write", Method, 0, ""}, @@ -594,7 +639,13 @@ var PackageSymbols = map[string][]Symbol{ {"(*PublicKey).Bytes", Method, 20, ""}, {"(*PublicKey).Curve", Method, 20, ""}, {"(*PublicKey).Equal", Method, 20, ""}, - {"Curve", Type, 20, ""}, + {"(Curve).GenerateKey", Method, 20, ""}, + {"(Curve).NewPrivateKey", Method, 20, ""}, + {"(Curve).NewPublicKey", Method, 20, ""}, + {"(KeyExchanger).Curve", Method, 26, ""}, + {"(KeyExchanger).ECDH", Method, 26, ""}, + {"(KeyExchanger).PublicKey", Method, 26, ""}, + {"KeyExchanger", Type, 26, ""}, {"P256", Func, 20, "func() Curve"}, {"P384", Func, 20, "func() Curve"}, {"P521", Func, 20, "func() Curve"}, @@ -667,6 +718,12 @@ var PackageSymbols = map[string][]Symbol{ {"(*CurveParams).Params", Method, 0, ""}, {"(*CurveParams).ScalarBaseMult", Method, 0, ""}, {"(*CurveParams).ScalarMult", Method, 0, ""}, + {"(Curve).Add", Method, 0, ""}, + {"(Curve).Double", Method, 0, ""}, + {"(Curve).IsOnCurve", Method, 0, ""}, + {"(Curve).Params", Method, 0, ""}, + {"(Curve).ScalarBaseMult", Method, 0, ""}, + {"(Curve).ScalarMult", Method, 0, ""}, {"Curve", Type, 0, ""}, {"CurveParams", Type, 0, ""}, {"CurveParams.B", Field, 0, ""}, @@ -688,6 +745,7 @@ var PackageSymbols = map[string][]Symbol{ }, "crypto/fips140": { {"Enabled", Func, 24, "func() bool"}, + {"Version", Func, 26, "func() string"}, }, "crypto/hkdf": { {"Expand", Func, 24, "func[H hash.Hash](h func() H, pseudorandomKey []byte, info string, keyLength int) ([]byte, error)"}, @@ -708,9 +766,11 @@ var PackageSymbols = map[string][]Symbol{ {"(*DecapsulationKey1024).Bytes", Method, 24, ""}, {"(*DecapsulationKey1024).Decapsulate", Method, 24, ""}, {"(*DecapsulationKey1024).EncapsulationKey", Method, 24, ""}, + {"(*DecapsulationKey1024).Encapsulator", Method, 26, ""}, {"(*DecapsulationKey768).Bytes", Method, 24, ""}, {"(*DecapsulationKey768).Decapsulate", Method, 24, ""}, {"(*DecapsulationKey768).EncapsulationKey", Method, 24, ""}, + {"(*DecapsulationKey768).Encapsulator", Method, 26, ""}, {"(*EncapsulationKey1024).Bytes", Method, 24, ""}, {"(*EncapsulationKey1024).Encapsulate", Method, 24, ""}, {"(*EncapsulationKey768).Bytes", Method, 24, ""}, @@ -732,6 +792,10 @@ var PackageSymbols = map[string][]Symbol{ {"SeedSize", Const, 24, ""}, {"SharedKeySize", Const, 24, ""}, }, + "crypto/mlkem/mlkemtest": { + {"Encapsulate1024", Func, 26, "func(ek *mlkem.EncapsulationKey1024, random []byte) (sharedKey []byte, ciphertext []byte, err error)"}, + {"Encapsulate768", Func, 26, "func(ek *mlkem.EncapsulationKey768, random []byte) (sharedKey []byte, ciphertext []byte, err error)"}, + }, "crypto/pbkdf2": { {"Key", Func, 24, "func[Hash hash.Hash](h func() Hash, password string, salt []byte, iter int, keyLength int) ([]byte, error)"}, }, @@ -769,6 +833,7 @@ var PackageSymbols = map[string][]Symbol{ {"DecryptPKCS1v15", Func, 0, "func(random io.Reader, priv *PrivateKey, ciphertext []byte) ([]byte, error)"}, {"DecryptPKCS1v15SessionKey", Func, 0, "func(random io.Reader, priv *PrivateKey, ciphertext []byte, key []byte) error"}, {"EncryptOAEP", Func, 0, "func(hash hash.Hash, random io.Reader, pub *PublicKey, msg []byte, label []byte) ([]byte, error)"}, + {"EncryptOAEPWithOptions", Func, 26, "func(random io.Reader, pub *PublicKey, msg []byte, opts *OAEPOptions) ([]byte, error)"}, {"EncryptPKCS1v15", Func, 0, "func(random io.Reader, pub *PublicKey, msg []byte) ([]byte, error)"}, {"ErrDecryption", Var, 0, ""}, {"ErrMessageTooLong", Var, 0, ""}, @@ -921,6 +986,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*SessionState).Bytes", Method, 21, ""}, {"(AlertError).Error", Method, 21, ""}, {"(ClientAuthType).String", Method, 15, ""}, + {"(ClientSessionCache).Get", Method, 3, ""}, + {"(ClientSessionCache).Put", Method, 3, ""}, {"(CurveID).String", Method, 15, ""}, {"(QUICEncryptionLevel).String", Method, 21, ""}, {"(RecordHeaderError).Error", Method, 6, ""}, @@ -953,6 +1020,7 @@ var PackageSymbols = map[string][]Symbol{ {"ClientHelloInfo.CipherSuites", Field, 4, ""}, {"ClientHelloInfo.Conn", Field, 8, ""}, {"ClientHelloInfo.Extensions", Field, 24, ""}, + {"ClientHelloInfo.HelloRetryRequest", Field, 26, ""}, {"ClientHelloInfo.ServerName", Field, 4, ""}, {"ClientHelloInfo.SignatureSchemes", Field, 8, ""}, {"ClientHelloInfo.SupportedCurves", Field, 4, ""}, @@ -1001,6 +1069,7 @@ var PackageSymbols = map[string][]Symbol{ {"ConnectionState.DidResume", Field, 1, ""}, {"ConnectionState.ECHAccepted", Field, 23, ""}, {"ConnectionState.HandshakeComplete", Field, 0, ""}, + {"ConnectionState.HelloRetryRequest", Field, 26, ""}, {"ConnectionState.NegotiatedProtocol", Field, 0, ""}, {"ConnectionState.NegotiatedProtocolIsMutual", Field, 0, ""}, {"ConnectionState.OCSPResponse", Field, 5, ""}, @@ -1055,8 +1124,10 @@ var PackageSymbols = map[string][]Symbol{ {"QUICEncryptionLevelEarly", Const, 21, ""}, {"QUICEncryptionLevelHandshake", Const, 21, ""}, {"QUICEncryptionLevelInitial", Const, 21, ""}, + {"QUICErrorEvent", Const, 26, ""}, {"QUICEvent", Type, 21, ""}, {"QUICEvent.Data", Field, 21, ""}, + {"QUICEvent.Err", Field, 26, ""}, {"QUICEvent.Kind", Field, 21, ""}, {"QUICEvent.Level", Field, 21, ""}, {"QUICEvent.SessionState", Field, 23, ""}, @@ -1151,8 +1222,10 @@ var PackageSymbols = map[string][]Symbol{ {"(*RevocationList).CheckSignatureFrom", Method, 19, ""}, {"(CertificateInvalidError).Error", Method, 0, ""}, {"(ConstraintViolationError).Error", Method, 0, ""}, + {"(ExtKeyUsage).String", Method, 26, ""}, {"(HostnameError).Error", Method, 0, ""}, {"(InsecureAlgorithmError).Error", Method, 6, ""}, + {"(KeyUsage).String", Method, 26, ""}, {"(OID).AppendBinary", Method, 24, ""}, {"(OID).AppendText", Method, 24, ""}, {"(OID).Equal", Method, 22, ""}, @@ -1516,6 +1589,9 @@ var PackageSymbols = map[string][]Symbol{ {"(NullInt64).Value", Method, 0, ""}, {"(NullString).Value", Method, 0, ""}, {"(NullTime).Value", Method, 13, ""}, + {"(Result).LastInsertId", Method, 0, ""}, + {"(Result).RowsAffected", Method, 0, ""}, + {"(Scanner).Scan", Method, 0, ""}, {"ColumnType", Type, 8, ""}, {"Conn", Type, 9, ""}, {"DB", Type, 0, ""}, @@ -1547,8 +1623,6 @@ var PackageSymbols = map[string][]Symbol{ {"NamedArg.Name", Field, 8, ""}, {"NamedArg.Value", Field, 8, ""}, {"Null", Type, 22, ""}, - {"Null.V", Field, 22, ""}, - {"Null.Valid", Field, 22, ""}, {"NullBool", Type, 0, ""}, {"NullBool.Bool", Field, 0, ""}, {"NullBool.Valid", Field, 0, ""}, @@ -1591,10 +1665,72 @@ var PackageSymbols = map[string][]Symbol{ {"TxOptions.ReadOnly", Field, 8, ""}, }, "database/sql/driver": { + {"(ColumnConverter).ColumnConverter", Method, 0, ""}, + {"(Conn).Begin", Method, 0, ""}, + {"(Conn).Close", Method, 0, ""}, + {"(Conn).Prepare", Method, 0, ""}, + {"(ConnBeginTx).BeginTx", Method, 8, ""}, + {"(ConnPrepareContext).PrepareContext", Method, 8, ""}, + {"(Connector).Connect", Method, 10, ""}, + {"(Connector).Driver", Method, 10, ""}, + {"(Driver).Open", Method, 0, ""}, + {"(DriverContext).OpenConnector", Method, 10, ""}, + {"(Execer).Exec", Method, 0, ""}, + {"(ExecerContext).ExecContext", Method, 8, ""}, + {"(NamedValueChecker).CheckNamedValue", Method, 9, ""}, {"(NotNull).ConvertValue", Method, 0, ""}, {"(Null).ConvertValue", Method, 0, ""}, + {"(Pinger).Ping", Method, 8, ""}, + {"(Queryer).Query", Method, 1, ""}, + {"(QueryerContext).QueryContext", Method, 8, ""}, + {"(Result).LastInsertId", Method, 0, ""}, + {"(Result).RowsAffected", Method, 0, ""}, + {"(Rows).Close", Method, 0, ""}, + {"(Rows).Columns", Method, 0, ""}, + {"(Rows).Next", Method, 0, ""}, {"(RowsAffected).LastInsertId", Method, 0, ""}, {"(RowsAffected).RowsAffected", Method, 0, ""}, + {"(RowsColumnScanner).Close", Method, 26, ""}, + {"(RowsColumnScanner).Columns", Method, 26, ""}, + {"(RowsColumnScanner).Next", Method, 26, ""}, + {"(RowsColumnScanner).ScanColumn", Method, 26, ""}, + {"(RowsColumnTypeDatabaseTypeName).Close", Method, 8, ""}, + {"(RowsColumnTypeDatabaseTypeName).ColumnTypeDatabaseTypeName", Method, 8, ""}, + {"(RowsColumnTypeDatabaseTypeName).Columns", Method, 8, ""}, + {"(RowsColumnTypeDatabaseTypeName).Next", Method, 8, ""}, + {"(RowsColumnTypeLength).Close", Method, 8, ""}, + {"(RowsColumnTypeLength).ColumnTypeLength", Method, 8, ""}, + {"(RowsColumnTypeLength).Columns", Method, 8, ""}, + {"(RowsColumnTypeLength).Next", Method, 8, ""}, + {"(RowsColumnTypeNullable).Close", Method, 8, ""}, + {"(RowsColumnTypeNullable).ColumnTypeNullable", Method, 8, ""}, + {"(RowsColumnTypeNullable).Columns", Method, 8, ""}, + {"(RowsColumnTypeNullable).Next", Method, 8, ""}, + {"(RowsColumnTypePrecisionScale).Close", Method, 8, ""}, + {"(RowsColumnTypePrecisionScale).ColumnTypePrecisionScale", Method, 8, ""}, + {"(RowsColumnTypePrecisionScale).Columns", Method, 8, ""}, + {"(RowsColumnTypePrecisionScale).Next", Method, 8, ""}, + {"(RowsColumnTypeScanType).Close", Method, 8, ""}, + {"(RowsColumnTypeScanType).ColumnTypeScanType", Method, 8, ""}, + {"(RowsColumnTypeScanType).Columns", Method, 8, ""}, + {"(RowsColumnTypeScanType).Next", Method, 8, ""}, + {"(RowsNextResultSet).Close", Method, 8, ""}, + {"(RowsNextResultSet).Columns", Method, 8, ""}, + {"(RowsNextResultSet).HasNextResultSet", Method, 8, ""}, + {"(RowsNextResultSet).Next", Method, 8, ""}, + {"(RowsNextResultSet).NextResultSet", Method, 8, ""}, + {"(SessionResetter).ResetSession", Method, 10, ""}, + {"(Stmt).Close", Method, 0, ""}, + {"(Stmt).Exec", Method, 0, ""}, + {"(Stmt).NumInput", Method, 0, ""}, + {"(Stmt).Query", Method, 0, ""}, + {"(StmtExecContext).ExecContext", Method, 8, ""}, + {"(StmtQueryContext).QueryContext", Method, 8, ""}, + {"(Tx).Commit", Method, 0, ""}, + {"(Tx).Rollback", Method, 0, ""}, + {"(Validator).IsValid", Method, 15, ""}, + {"(ValueConverter).ConvertValue", Method, 0, ""}, + {"(Valuer).Value", Method, 0, ""}, {"Bool", Var, 0, ""}, {"ColumnConverter", Type, 0, ""}, {"Conn", Type, 0, ""}, @@ -1756,6 +1892,9 @@ var PackageSymbols = map[string][]Symbol{ {"(DecodeError).Error", Method, 0, ""}, {"(Tag).GoString", Method, 0, ""}, {"(Tag).String", Method, 0, ""}, + {"(Type).Common", Method, 0, ""}, + {"(Type).Size", Method, 0, ""}, + {"(Type).String", Method, 0, ""}, {"AddrType", Type, 0, ""}, {"AddrType.BasicType", Field, 0, ""}, {"ArrayType", Type, 0, ""}, @@ -3163,6 +3302,7 @@ var PackageSymbols = map[string][]Symbol{ {"R_LARCH_B16", Const, 20, ""}, {"R_LARCH_B21", Const, 20, ""}, {"R_LARCH_B26", Const, 20, ""}, + {"R_LARCH_CALL36", Const, 26, ""}, {"R_LARCH_CFA", Const, 22, ""}, {"R_LARCH_COPY", Const, 19, ""}, {"R_LARCH_DELETE", Const, 22, ""}, @@ -3220,11 +3360,25 @@ var PackageSymbols = map[string][]Symbol{ {"R_LARCH_SUB64", Const, 19, ""}, {"R_LARCH_SUB8", Const, 19, ""}, {"R_LARCH_SUB_ULEB128", Const, 22, ""}, + {"R_LARCH_TLS_DESC32", Const, 26, ""}, + {"R_LARCH_TLS_DESC64", Const, 26, ""}, + {"R_LARCH_TLS_DESC64_HI12", Const, 26, ""}, + {"R_LARCH_TLS_DESC64_LO20", Const, 26, ""}, + {"R_LARCH_TLS_DESC64_PC_HI12", Const, 26, ""}, + {"R_LARCH_TLS_DESC64_PC_LO20", Const, 26, ""}, + {"R_LARCH_TLS_DESC_CALL", Const, 26, ""}, + {"R_LARCH_TLS_DESC_HI20", Const, 26, ""}, + {"R_LARCH_TLS_DESC_LD", Const, 26, ""}, + {"R_LARCH_TLS_DESC_LO12", Const, 26, ""}, + {"R_LARCH_TLS_DESC_PCREL20_S2", Const, 26, ""}, + {"R_LARCH_TLS_DESC_PC_HI20", Const, 26, ""}, + {"R_LARCH_TLS_DESC_PC_LO12", Const, 26, ""}, {"R_LARCH_TLS_DTPMOD32", Const, 19, ""}, {"R_LARCH_TLS_DTPMOD64", Const, 19, ""}, {"R_LARCH_TLS_DTPREL32", Const, 19, ""}, {"R_LARCH_TLS_DTPREL64", Const, 19, ""}, {"R_LARCH_TLS_GD_HI20", Const, 20, ""}, + {"R_LARCH_TLS_GD_PCREL20_S2", Const, 26, ""}, {"R_LARCH_TLS_GD_PC_HI20", Const, 20, ""}, {"R_LARCH_TLS_IE64_HI12", Const, 20, ""}, {"R_LARCH_TLS_IE64_LO20", Const, 20, ""}, @@ -3235,11 +3389,15 @@ var PackageSymbols = map[string][]Symbol{ {"R_LARCH_TLS_IE_PC_HI20", Const, 20, ""}, {"R_LARCH_TLS_IE_PC_LO12", Const, 20, ""}, {"R_LARCH_TLS_LD_HI20", Const, 20, ""}, + {"R_LARCH_TLS_LD_PCREL20_S2", Const, 26, ""}, {"R_LARCH_TLS_LD_PC_HI20", Const, 20, ""}, {"R_LARCH_TLS_LE64_HI12", Const, 20, ""}, {"R_LARCH_TLS_LE64_LO20", Const, 20, ""}, + {"R_LARCH_TLS_LE_ADD_R", Const, 26, ""}, {"R_LARCH_TLS_LE_HI20", Const, 20, ""}, + {"R_LARCH_TLS_LE_HI20_R", Const, 26, ""}, {"R_LARCH_TLS_LE_LO12", Const, 20, ""}, + {"R_LARCH_TLS_LE_LO12_R", Const, 26, ""}, {"R_LARCH_TLS_TPREL32", Const, 19, ""}, {"R_LARCH_TLS_TPREL64", Const, 19, ""}, {"R_MIPS", Type, 6, ""}, @@ -3944,6 +4102,7 @@ var PackageSymbols = map[string][]Symbol{ {"(FatArch).ImportedSymbols", Method, 3, ""}, {"(FatArch).Section", Method, 3, ""}, {"(FatArch).Segment", Method, 3, ""}, + {"(Load).Raw", Method, 0, ""}, {"(LoadBytes).Raw", Method, 0, ""}, {"(LoadCmd).GoString", Method, 0, ""}, {"(LoadCmd).String", Method, 0, ""}, @@ -4590,6 +4749,12 @@ var PackageSymbols = map[string][]Symbol{ {"FS", Type, 16, ""}, }, "encoding": { + {"(BinaryAppender).AppendBinary", Method, 24, ""}, + {"(BinaryMarshaler).MarshalBinary", Method, 2, ""}, + {"(BinaryUnmarshaler).UnmarshalBinary", Method, 2, ""}, + {"(TextAppender).AppendText", Method, 24, ""}, + {"(TextMarshaler).MarshalText", Method, 2, ""}, + {"(TextUnmarshaler).UnmarshalText", Method, 2, ""}, {"BinaryAppender", Type, 24, ""}, {"BinaryMarshaler", Type, 2, ""}, {"BinaryUnmarshaler", Type, 2, ""}, @@ -4705,6 +4870,17 @@ var PackageSymbols = map[string][]Symbol{ {"URLEncoding", Var, 0, ""}, }, "encoding/binary": { + {"(AppendByteOrder).AppendUint16", Method, 19, ""}, + {"(AppendByteOrder).AppendUint32", Method, 19, ""}, + {"(AppendByteOrder).AppendUint64", Method, 19, ""}, + {"(AppendByteOrder).String", Method, 19, ""}, + {"(ByteOrder).PutUint16", Method, 0, ""}, + {"(ByteOrder).PutUint32", Method, 0, ""}, + {"(ByteOrder).PutUint64", Method, 0, ""}, + {"(ByteOrder).String", Method, 0, ""}, + {"(ByteOrder).Uint16", Method, 0, ""}, + {"(ByteOrder).Uint32", Method, 0, ""}, + {"(ByteOrder).Uint64", Method, 0, ""}, {"Append", Func, 23, "func(buf []byte, order ByteOrder, data any) ([]byte, error)"}, {"AppendByteOrder", Type, 19, ""}, {"AppendUvarint", Func, 19, "func(buf []byte, x uint64) []byte"}, @@ -4767,6 +4943,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*Decoder).DecodeValue", Method, 0, ""}, {"(*Encoder).Encode", Method, 0, ""}, {"(*Encoder).EncodeValue", Method, 0, ""}, + {"(GobDecoder).GobDecode", Method, 0, ""}, + {"(GobEncoder).GobEncode", Method, 0, ""}, {"CommonType", Type, 0, ""}, {"CommonType.Id", Field, 0, ""}, {"CommonType.Name", Field, 0, ""}, @@ -4819,10 +4997,12 @@ var PackageSymbols = map[string][]Symbol{ {"(*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).String", Method, 1, ""}, {"(RawMessage).MarshalJSON", Method, 8, ""}, + {"(Unmarshaler).UnmarshalJSON", Method, 0, ""}, {"Compact", Func, 0, "func(dst *bytes.Buffer, src []byte) error"}, {"Decoder", Type, 0, ""}, {"Delim", Type, 5, ""}, @@ -4894,10 +5074,15 @@ var PackageSymbols = map[string][]Symbol{ {"(CharData).Copy", Method, 0, ""}, {"(Comment).Copy", Method, 0, ""}, {"(Directive).Copy", Method, 0, ""}, + {"(Marshaler).MarshalXML", Method, 2, ""}, + {"(MarshalerAttr).MarshalXMLAttr", Method, 2, ""}, {"(ProcInst).Copy", Method, 0, ""}, {"(StartElement).Copy", Method, 0, ""}, {"(StartElement).End", Method, 2, ""}, + {"(TokenReader).Token", Method, 10, ""}, {"(UnmarshalError).Error", Method, 0, ""}, + {"(Unmarshaler).UnmarshalXML", Method, 2, ""}, + {"(UnmarshalerAttr).UnmarshalXMLAttr", Method, 2, ""}, {"Attr", Type, 0, ""}, {"Attr.Name", Field, 0, ""}, {"Attr.Value", Field, 0, ""}, @@ -4984,6 +5169,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*String).Value", Method, 8, ""}, {"(Func).String", Method, 0, ""}, {"(Func).Value", Method, 8, ""}, + {"(Var).String", Method, 0, ""}, {"Do", Func, 0, "func(f func(KeyValue))"}, {"Float", Type, 0, ""}, {"Func", Type, 0, ""}, @@ -5039,6 +5225,11 @@ var PackageSymbols = map[string][]Symbol{ {"(*FlagSet).Var", Method, 0, ""}, {"(*FlagSet).Visit", Method, 0, ""}, {"(*FlagSet).VisitAll", Method, 0, ""}, + {"(Getter).Get", Method, 2, ""}, + {"(Getter).Set", Method, 2, ""}, + {"(Getter).String", Method, 2, ""}, + {"(Value).Set", Method, 0, ""}, + {"(Value).String", Method, 0, ""}, {"Arg", Func, 0, "func(i int) string"}, {"Args", Func, 0, "func() []string"}, {"Bool", Func, 0, "func(name string, value bool, usage string) *bool"}, @@ -5090,6 +5281,20 @@ var PackageSymbols = map[string][]Symbol{ {"VisitAll", Func, 0, "func(fn func(*Flag))"}, }, "fmt": { + {"(Formatter).Format", Method, 0, ""}, + {"(GoStringer).GoString", Method, 0, ""}, + {"(ScanState).Read", Method, 0, ""}, + {"(ScanState).ReadRune", Method, 0, ""}, + {"(ScanState).SkipSpace", Method, 0, ""}, + {"(ScanState).Token", Method, 0, ""}, + {"(ScanState).UnreadRune", Method, 0, ""}, + {"(ScanState).Width", Method, 0, ""}, + {"(Scanner).Scan", Method, 0, ""}, + {"(State).Flag", Method, 0, ""}, + {"(State).Precision", Method, 0, ""}, + {"(State).Width", Method, 0, ""}, + {"(State).Write", Method, 0, ""}, + {"(Stringer).String", Method, 0, ""}, {"Append", Func, 19, "func(b []byte, a ...any) []byte"}, {"Appendf", Func, 19, "func(b []byte, format string, a ...any) []byte"}, {"Appendln", Func, 19, "func(b []byte, a ...any) []byte"}, @@ -5248,7 +5453,18 @@ var PackageSymbols = map[string][]Symbol{ {"(CommentMap).Filter", Method, 1, ""}, {"(CommentMap).String", Method, 1, ""}, {"(CommentMap).Update", Method, 1, ""}, + {"(Decl).End", Method, 0, ""}, + {"(Decl).Pos", Method, 0, ""}, + {"(Expr).End", Method, 0, ""}, + {"(Expr).Pos", Method, 0, ""}, + {"(Node).End", Method, 0, ""}, + {"(Node).Pos", Method, 0, ""}, {"(ObjKind).String", Method, 0, ""}, + {"(Spec).End", Method, 0, ""}, + {"(Spec).Pos", Method, 0, ""}, + {"(Stmt).End", Method, 0, ""}, + {"(Stmt).Pos", Method, 0, ""}, + {"(Visitor).Visit", Method, 0, ""}, {"ArrayType", Type, 0, ""}, {"ArrayType.Elt", Field, 0, ""}, {"ArrayType.Lbrack", Field, 0, ""}, @@ -5271,6 +5487,7 @@ var PackageSymbols = map[string][]Symbol{ {"BasicLit", Type, 0, ""}, {"BasicLit.Kind", Field, 0, ""}, {"BasicLit.Value", Field, 0, ""}, + {"BasicLit.ValueEnd", Field, 26, ""}, {"BasicLit.ValuePos", Field, 0, ""}, {"BinaryExpr", Type, 0, ""}, {"BinaryExpr.Op", Field, 0, ""}, @@ -5320,7 +5537,6 @@ var PackageSymbols = map[string][]Symbol{ {"CompositeLit.Rbrace", Field, 0, ""}, {"CompositeLit.Type", Field, 0, ""}, {"Con", Const, 0, ""}, - {"Decl", Type, 0, ""}, {"DeclStmt", Type, 0, ""}, {"DeclStmt.Decl", Field, 0, ""}, {"DeferStmt", Type, 0, ""}, @@ -5341,7 +5557,6 @@ var PackageSymbols = map[string][]Symbol{ {"EmptyStmt", Type, 0, ""}, {"EmptyStmt.Implicit", Field, 5, ""}, {"EmptyStmt.Semicolon", Field, 0, ""}, - {"Expr", Type, 0, ""}, {"ExprStmt", Type, 0, ""}, {"ExprStmt.X", Field, 0, ""}, {"Field", Type, 0, ""}, @@ -5525,11 +5740,9 @@ var PackageSymbols = map[string][]Symbol{ {"SliceExpr.Slice3", Field, 2, ""}, {"SliceExpr.X", Field, 0, ""}, {"SortImports", Func, 0, "func(fset *token.FileSet, f *File)"}, - {"Spec", Type, 0, ""}, {"StarExpr", Type, 0, ""}, {"StarExpr.Star", Field, 0, ""}, {"StarExpr.X", Field, 0, ""}, - {"Stmt", Type, 0, ""}, {"StructType", Type, 0, ""}, {"StructType.Fields", Field, 0, ""}, {"StructType.Incomplete", Field, 0, ""}, @@ -5684,10 +5897,11 @@ var PackageSymbols = map[string][]Symbol{ {"(*SyntaxError).Error", Method, 16, ""}, {"(*TagExpr).Eval", Method, 16, ""}, {"(*TagExpr).String", Method, 16, ""}, + {"(Expr).Eval", Method, 16, ""}, + {"(Expr).String", Method, 16, ""}, {"AndExpr", Type, 16, ""}, {"AndExpr.X", Field, 16, ""}, {"AndExpr.Y", Field, 16, ""}, - {"Expr", Type, 16, ""}, {"GoVersion", Func, 21, "func(x Expr) string"}, {"IsGoBuild", Func, 16, "func(line string) bool"}, {"IsPlusBuild", Func, 16, "func(line string) bool"}, @@ -5706,6 +5920,9 @@ var PackageSymbols = map[string][]Symbol{ }, "go/constant": { {"(Kind).String", Method, 18, ""}, + {"(Value).ExactString", Method, 6, ""}, + {"(Value).Kind", Method, 5, ""}, + {"(Value).String", Method, 5, ""}, {"BinaryOp", Func, 5, "func(x_ Value, op token.Token, y_ Value) Value"}, {"BitLen", Func, 5, "func(x Value) int"}, {"Bool", Const, 5, ""}, @@ -5744,7 +5961,6 @@ var PackageSymbols = map[string][]Symbol{ {"UnaryOp", Func, 5, "func(op token.Token, y Value, prec uint) Value"}, {"Unknown", Const, 5, ""}, {"Val", Func, 13, "func(x Value) any"}, - {"Value", Type, 5, ""}, }, "go/doc": { {"(*Package).Filter", Method, 0, ""}, @@ -5828,7 +6044,6 @@ var PackageSymbols = map[string][]Symbol{ {"(*Printer).HTML", Method, 19, ""}, {"(*Printer).Markdown", Method, 19, ""}, {"(*Printer).Text", Method, 19, ""}, - {"Block", Type, 19, ""}, {"Code", Type, 19, ""}, {"Code.Text", Field, 19, ""}, {"DefaultLookupPackage", Func, 19, "func(name string) (importPath string, ok bool)"}, @@ -5873,7 +6088,6 @@ var PackageSymbols = map[string][]Symbol{ {"Printer.TextCodePrefix", Field, 19, ""}, {"Printer.TextPrefix", Field, 19, ""}, {"Printer.TextWidth", Field, 19, ""}, - {"Text", Type, 19, ""}, }, "go/format": { {"Node", Func, 1, "func(dst io.Writer, fset *token.FileSet, node any) error"}, @@ -5945,6 +6159,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*File).AddLineColumnInfo", Method, 11, ""}, {"(*File).AddLineInfo", Method, 0, ""}, {"(*File).Base", Method, 0, ""}, + {"(*File).End", Method, 26, ""}, {"(*File).Line", Method, 0, ""}, {"(*File).LineCount", Method, 0, ""}, {"(*File).LineStart", Method, 12, ""}, @@ -6307,6 +6522,22 @@ var PackageSymbols = map[string][]Symbol{ {"(Checker).PkgNameOf", Method, 22, ""}, {"(Checker).TypeOf", Method, 5, ""}, {"(Error).Error", Method, 5, ""}, + {"(Importer).Import", Method, 5, ""}, + {"(ImporterFrom).Import", Method, 6, ""}, + {"(ImporterFrom).ImportFrom", Method, 6, ""}, + {"(Object).Exported", Method, 5, ""}, + {"(Object).Id", Method, 5, ""}, + {"(Object).Name", Method, 5, ""}, + {"(Object).Parent", Method, 5, ""}, + {"(Object).Pkg", Method, 5, ""}, + {"(Object).Pos", Method, 5, ""}, + {"(Object).String", Method, 5, ""}, + {"(Object).Type", Method, 5, ""}, + {"(Sizes).Alignof", Method, 5, ""}, + {"(Sizes).Offsetsof", Method, 5, ""}, + {"(Sizes).Sizeof", Method, 5, ""}, + {"(Type).String", Method, 5, ""}, + {"(Type).Underlying", Method, 5, ""}, {"(TypeAndValue).Addressable", Method, 5, ""}, {"(TypeAndValue).Assignable", Method, 5, ""}, {"(TypeAndValue).HasOk", Method, 5, ""}, @@ -6445,7 +6676,6 @@ var PackageSymbols = map[string][]Symbol{ {"NewUnion", Func, 18, "func(terms []*Term) *Union"}, {"NewVar", Func, 5, "func(pos token.Pos, pkg *Package, name string, typ Type) *Var"}, {"Nil", Type, 5, ""}, - {"Object", Type, 5, ""}, {"ObjectString", Func, 5, "func(obj Object, qf Qualifier) string"}, {"Package", Type, 5, ""}, {"PackageVar", Const, 25, ""}, @@ -6516,6 +6746,33 @@ var PackageSymbols = map[string][]Symbol{ {"Lang", Func, 22, "func(x string) string"}, }, "hash": { + {"(Cloner).BlockSize", Method, 25, ""}, + {"(Cloner).Clone", Method, 25, ""}, + {"(Cloner).Reset", Method, 25, ""}, + {"(Cloner).Size", Method, 25, ""}, + {"(Cloner).Sum", Method, 25, ""}, + {"(Cloner).Write", Method, 25, ""}, + {"(Hash).BlockSize", Method, 0, ""}, + {"(Hash).Reset", Method, 0, ""}, + {"(Hash).Size", Method, 0, ""}, + {"(Hash).Sum", Method, 0, ""}, + {"(Hash).Write", Method, 0, ""}, + {"(Hash32).BlockSize", Method, 0, ""}, + {"(Hash32).Reset", Method, 0, ""}, + {"(Hash32).Size", Method, 0, ""}, + {"(Hash32).Sum", Method, 0, ""}, + {"(Hash32).Sum32", Method, 0, ""}, + {"(Hash32).Write", Method, 0, ""}, + {"(Hash64).BlockSize", Method, 0, ""}, + {"(Hash64).Reset", Method, 0, ""}, + {"(Hash64).Size", Method, 0, ""}, + {"(Hash64).Sum", Method, 0, ""}, + {"(Hash64).Sum64", Method, 0, ""}, + {"(Hash64).Write", Method, 0, ""}, + {"(XOF).BlockSize", Method, 25, ""}, + {"(XOF).Read", Method, 25, ""}, + {"(XOF).Reset", Method, 25, ""}, + {"(XOF).Write", Method, 25, ""}, {"Cloner", Type, 25, ""}, {"Hash", Type, 0, ""}, {"Hash32", Type, 0, ""}, @@ -6781,6 +7038,13 @@ var PackageSymbols = map[string][]Symbol{ {"(*YCbCr).SubImage", Method, 0, ""}, {"(*YCbCr).YCbCrAt", Method, 4, ""}, {"(*YCbCr).YOffset", Method, 0, ""}, + {"(Image).At", Method, 0, ""}, + {"(Image).Bounds", Method, 0, ""}, + {"(Image).ColorModel", Method, 0, ""}, + {"(PalettedImage).At", Method, 0, ""}, + {"(PalettedImage).Bounds", Method, 0, ""}, + {"(PalettedImage).ColorIndexAt", Method, 0, ""}, + {"(PalettedImage).ColorModel", Method, 0, ""}, {"(Point).Add", Method, 0, ""}, {"(Point).Div", Method, 0, ""}, {"(Point).Eq", Method, 0, ""}, @@ -6789,6 +7053,10 @@ var PackageSymbols = map[string][]Symbol{ {"(Point).Mul", Method, 0, ""}, {"(Point).String", Method, 0, ""}, {"(Point).Sub", Method, 0, ""}, + {"(RGBA64Image).At", Method, 17, ""}, + {"(RGBA64Image).Bounds", Method, 17, ""}, + {"(RGBA64Image).ColorModel", Method, 17, ""}, + {"(RGBA64Image).RGBA64At", Method, 17, ""}, {"(Rectangle).Add", Method, 0, ""}, {"(Rectangle).At", Method, 5, ""}, {"(Rectangle).Bounds", Method, 5, ""}, @@ -6913,8 +7181,10 @@ var PackageSymbols = map[string][]Symbol{ {"(Alpha).RGBA", Method, 0, ""}, {"(Alpha16).RGBA", Method, 0, ""}, {"(CMYK).RGBA", Method, 5, ""}, + {"(Color).RGBA", Method, 0, ""}, {"(Gray).RGBA", Method, 0, ""}, {"(Gray16).RGBA", Method, 0, ""}, + {"(Model).Convert", Method, 0, ""}, {"(NRGBA).RGBA", Method, 0, ""}, {"(NRGBA64).RGBA", Method, 0, ""}, {"(NYCbCrA).RGBA", Method, 6, ""}, @@ -6992,7 +7262,19 @@ var PackageSymbols = map[string][]Symbol{ {"WebSafe", Var, 2, ""}, }, "image/draw": { + {"(Drawer).Draw", Method, 2, ""}, + {"(Image).At", Method, 0, ""}, + {"(Image).Bounds", Method, 0, ""}, + {"(Image).ColorModel", Method, 0, ""}, + {"(Image).Set", Method, 0, ""}, {"(Op).Draw", Method, 2, ""}, + {"(Quantizer).Quantize", Method, 2, ""}, + {"(RGBA64Image).At", Method, 17, ""}, + {"(RGBA64Image).Bounds", Method, 17, ""}, + {"(RGBA64Image).ColorModel", Method, 17, ""}, + {"(RGBA64Image).RGBA64At", Method, 17, ""}, + {"(RGBA64Image).Set", Method, 17, ""}, + {"(RGBA64Image).SetRGBA64", Method, 17, ""}, {"Draw", Func, 0, "func(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)"}, {"DrawMask", Func, 0, "func(dst Image, r image.Rectangle, src image.Image, sp image.Point, mask image.Image, mp image.Point, op Op)"}, {"Drawer", Type, 2, ""}, @@ -7027,6 +7309,8 @@ var PackageSymbols = map[string][]Symbol{ }, "image/jpeg": { {"(FormatError).Error", Method, 0, ""}, + {"(Reader).Read", Method, 0, ""}, + {"(Reader).ReadByte", Method, 0, ""}, {"(UnsupportedError).Error", Method, 0, ""}, {"Decode", Func, 0, "func(r io.Reader) (image.Image, error)"}, {"DecodeConfig", Func, 0, "func(r io.Reader) (image.Config, error)"}, @@ -7040,6 +7324,8 @@ var PackageSymbols = map[string][]Symbol{ }, "image/png": { {"(*Encoder).Encode", Method, 4, ""}, + {"(EncoderBufferPool).Get", Method, 9, ""}, + {"(EncoderBufferPool).Put", Method, 9, ""}, {"(FormatError).Error", Method, 0, ""}, {"(UnsupportedError).Error", Method, 0, ""}, {"BestCompression", Const, 4, ""}, @@ -7083,6 +7369,41 @@ var PackageSymbols = map[string][]Symbol{ {"(*SectionReader).ReadAt", Method, 0, ""}, {"(*SectionReader).Seek", Method, 0, ""}, {"(*SectionReader).Size", Method, 0, ""}, + {"(ByteReader).ReadByte", Method, 0, ""}, + {"(ByteScanner).ReadByte", Method, 0, ""}, + {"(ByteScanner).UnreadByte", Method, 0, ""}, + {"(ByteWriter).WriteByte", Method, 1, ""}, + {"(Closer).Close", Method, 0, ""}, + {"(ReadCloser).Close", Method, 0, ""}, + {"(ReadCloser).Read", Method, 0, ""}, + {"(ReadSeekCloser).Close", Method, 16, ""}, + {"(ReadSeekCloser).Read", Method, 16, ""}, + {"(ReadSeekCloser).Seek", Method, 16, ""}, + {"(ReadSeeker).Read", Method, 0, ""}, + {"(ReadSeeker).Seek", Method, 0, ""}, + {"(ReadWriteCloser).Close", Method, 0, ""}, + {"(ReadWriteCloser).Read", Method, 0, ""}, + {"(ReadWriteCloser).Write", Method, 0, ""}, + {"(ReadWriteSeeker).Read", Method, 0, ""}, + {"(ReadWriteSeeker).Seek", Method, 0, ""}, + {"(ReadWriteSeeker).Write", Method, 0, ""}, + {"(ReadWriter).Read", Method, 0, ""}, + {"(ReadWriter).Write", Method, 0, ""}, + {"(Reader).Read", Method, 0, ""}, + {"(ReaderAt).ReadAt", Method, 0, ""}, + {"(ReaderFrom).ReadFrom", Method, 0, ""}, + {"(RuneReader).ReadRune", Method, 0, ""}, + {"(RuneScanner).ReadRune", Method, 0, ""}, + {"(RuneScanner).UnreadRune", Method, 0, ""}, + {"(Seeker).Seek", Method, 0, ""}, + {"(StringWriter).WriteString", Method, 12, ""}, + {"(WriteCloser).Close", Method, 0, ""}, + {"(WriteCloser).Write", Method, 0, ""}, + {"(WriteSeeker).Seek", Method, 0, ""}, + {"(WriteSeeker).Write", Method, 0, ""}, + {"(Writer).Write", Method, 0, ""}, + {"(WriterAt).WriteAt", Method, 0, ""}, + {"(WriterTo).WriteTo", Method, 0, ""}, {"ByteReader", Type, 0, ""}, {"ByteScanner", Type, 0, ""}, {"ByteWriter", Type, 1, ""}, @@ -7142,11 +7463,42 @@ var PackageSymbols = map[string][]Symbol{ {"(*PathError).Error", Method, 16, ""}, {"(*PathError).Timeout", Method, 16, ""}, {"(*PathError).Unwrap", Method, 16, ""}, + {"(DirEntry).Info", Method, 16, ""}, + {"(DirEntry).IsDir", Method, 16, ""}, + {"(DirEntry).Name", Method, 16, ""}, + {"(DirEntry).Type", Method, 16, ""}, + {"(FS).Open", Method, 16, ""}, + {"(File).Close", Method, 16, ""}, + {"(File).Read", Method, 16, ""}, + {"(File).Stat", Method, 16, ""}, + {"(FileInfo).IsDir", Method, 16, ""}, + {"(FileInfo).ModTime", Method, 16, ""}, + {"(FileInfo).Mode", Method, 16, ""}, + {"(FileInfo).Name", Method, 16, ""}, + {"(FileInfo).Size", Method, 16, ""}, + {"(FileInfo).Sys", Method, 16, ""}, {"(FileMode).IsDir", Method, 16, ""}, {"(FileMode).IsRegular", Method, 16, ""}, {"(FileMode).Perm", Method, 16, ""}, {"(FileMode).String", Method, 16, ""}, {"(FileMode).Type", Method, 16, ""}, + {"(GlobFS).Glob", Method, 16, ""}, + {"(GlobFS).Open", Method, 16, ""}, + {"(ReadDirFS).Open", Method, 16, ""}, + {"(ReadDirFS).ReadDir", Method, 16, ""}, + {"(ReadDirFile).Close", Method, 16, ""}, + {"(ReadDirFile).Read", Method, 16, ""}, + {"(ReadDirFile).ReadDir", Method, 16, ""}, + {"(ReadDirFile).Stat", Method, 16, ""}, + {"(ReadFileFS).Open", Method, 16, ""}, + {"(ReadFileFS).ReadFile", Method, 16, ""}, + {"(ReadLinkFS).Lstat", Method, 25, ""}, + {"(ReadLinkFS).Open", Method, 25, ""}, + {"(ReadLinkFS).ReadLink", Method, 25, ""}, + {"(StatFS).Open", Method, 16, ""}, + {"(StatFS).Stat", Method, 16, ""}, + {"(SubFS).Open", Method, 16, ""}, + {"(SubFS).Sub", Method, 16, ""}, {"DirEntry", Type, 16, ""}, {"ErrClosed", Var, 16, ""}, {"ErrExist", Var, 16, ""}, @@ -7299,12 +7651,18 @@ var PackageSymbols = map[string][]Symbol{ {"(*TextHandler).WithGroup", Method, 21, ""}, {"(Attr).Equal", Method, 21, ""}, {"(Attr).String", Method, 21, ""}, + {"(Handler).Enabled", Method, 21, ""}, + {"(Handler).Handle", Method, 21, ""}, + {"(Handler).WithAttrs", Method, 21, ""}, + {"(Handler).WithGroup", Method, 21, ""}, {"(Kind).String", Method, 21, ""}, {"(Level).AppendText", Method, 24, ""}, {"(Level).Level", Method, 21, ""}, {"(Level).MarshalJSON", Method, 21, ""}, {"(Level).MarshalText", Method, 21, ""}, {"(Level).String", Method, 21, ""}, + {"(Leveler).Level", Method, 21, ""}, + {"(LogValuer).LogValue", Method, 21, ""}, {"(Record).Attrs", Method, 21, ""}, {"(Record).Clone", Method, 21, ""}, {"(Record).NumAttrs", Method, 21, ""}, @@ -7833,6 +8191,11 @@ var PackageSymbols = map[string][]Symbol{ {"(*Rand).Uint32", Method, 0, ""}, {"(*Rand).Uint64", Method, 8, ""}, {"(*Zipf).Uint64", Method, 0, ""}, + {"(Source).Int63", Method, 0, ""}, + {"(Source).Seed", Method, 0, ""}, + {"(Source64).Int63", Method, 8, ""}, + {"(Source64).Seed", Method, 8, ""}, + {"(Source64).Uint64", Method, 8, ""}, {"ExpFloat64", Func, 0, "func() float64"}, {"Float32", Func, 0, "func() float32"}, {"Float64", Func, 0, "func() float64"}, @@ -7888,6 +8251,7 @@ var PackageSymbols = map[string][]Symbol{ {"(*Rand).Uint64N", Method, 22, ""}, {"(*Rand).UintN", Method, 22, ""}, {"(*Zipf).Uint64", Method, 22, ""}, + {"(Source).Uint64", Method, 22, ""}, {"ChaCha8", Type, 22, ""}, {"ExpFloat64", Func, 22, "func() float64"}, {"Float32", Func, 22, "func() float32"}, @@ -7951,6 +8315,10 @@ var PackageSymbols = map[string][]Symbol{ {"(*Writer).FormDataContentType", Method, 0, ""}, {"(*Writer).SetBoundary", Method, 1, ""}, {"(*Writer).WriteField", Method, 0, ""}, + {"(File).Close", Method, 0, ""}, + {"(File).Read", Method, 0, ""}, + {"(File).ReadAt", Method, 0, ""}, + {"(File).Seek", Method, 0, ""}, {"ErrMessageTooLarge", Var, 9, ""}, {"File", Type, 0, ""}, {"FileContentDisposition", Func, 25, "func(fieldname string, filename string) string"}, @@ -8135,6 +8503,19 @@ var PackageSymbols = map[string][]Symbol{ {"(*UnixListener).SetDeadline", Method, 0, ""}, {"(*UnixListener).SetUnlinkOnClose", Method, 8, ""}, {"(*UnixListener).SyscallConn", Method, 10, ""}, + {"(Addr).Network", Method, 0, ""}, + {"(Addr).String", Method, 0, ""}, + {"(Conn).Close", Method, 0, ""}, + {"(Conn).LocalAddr", Method, 0, ""}, + {"(Conn).Read", Method, 0, ""}, + {"(Conn).RemoteAddr", Method, 0, ""}, + {"(Conn).SetDeadline", Method, 0, ""}, + {"(Conn).SetReadDeadline", Method, 0, ""}, + {"(Conn).SetWriteDeadline", Method, 0, ""}, + {"(Conn).Write", Method, 0, ""}, + {"(Error).Error", Method, 0, ""}, + {"(Error).Temporary", Method, 0, ""}, + {"(Error).Timeout", Method, 0, ""}, {"(Flags).String", Method, 0, ""}, {"(HardwareAddr).String", Method, 0, ""}, {"(IP).AppendText", Method, 24, ""}, @@ -8158,6 +8539,16 @@ var PackageSymbols = map[string][]Symbol{ {"(InvalidAddrError).Error", Method, 0, ""}, {"(InvalidAddrError).Temporary", Method, 0, ""}, {"(InvalidAddrError).Timeout", Method, 0, ""}, + {"(Listener).Accept", Method, 0, ""}, + {"(Listener).Addr", Method, 0, ""}, + {"(Listener).Close", Method, 0, ""}, + {"(PacketConn).Close", Method, 0, ""}, + {"(PacketConn).LocalAddr", Method, 0, ""}, + {"(PacketConn).ReadFrom", Method, 0, ""}, + {"(PacketConn).SetDeadline", Method, 0, ""}, + {"(PacketConn).SetReadDeadline", Method, 0, ""}, + {"(PacketConn).SetWriteDeadline", Method, 0, ""}, + {"(PacketConn).WriteTo", Method, 0, ""}, {"(UnknownNetworkError).Error", Method, 0, ""}, {"(UnknownNetworkError).Temporary", Method, 0, ""}, {"(UnknownNetworkError).Timeout", Method, 0, ""}, @@ -8333,6 +8724,14 @@ var PackageSymbols = map[string][]Symbol{ {"(*Client).Head", Method, 0, ""}, {"(*Client).Post", Method, 0, ""}, {"(*Client).PostForm", Method, 0, ""}, + {"(*ClientConn).Available", Method, 26, ""}, + {"(*ClientConn).Close", Method, 26, ""}, + {"(*ClientConn).Err", Method, 26, ""}, + {"(*ClientConn).InFlight", Method, 26, ""}, + {"(*ClientConn).Release", Method, 26, ""}, + {"(*ClientConn).Reserve", Method, 26, ""}, + {"(*ClientConn).RoundTrip", Method, 26, ""}, + {"(*ClientConn).SetStateHook", Method, 26, ""}, {"(*Cookie).String", Method, 0, ""}, {"(*Cookie).Valid", Method, 18, ""}, {"(*CrossOriginProtection).AddInsecureBypassPattern", Method, 25, ""}, @@ -8392,10 +8791,22 @@ var PackageSymbols = map[string][]Symbol{ {"(*Transport).CancelRequest", Method, 1, ""}, {"(*Transport).Clone", Method, 13, ""}, {"(*Transport).CloseIdleConnections", Method, 0, ""}, + {"(*Transport).NewClientConn", Method, 26, ""}, {"(*Transport).RegisterProtocol", Method, 0, ""}, {"(*Transport).RoundTrip", Method, 0, ""}, + {"(CloseNotifier).CloseNotify", Method, 1, ""}, {"(ConnState).String", Method, 3, ""}, + {"(CookieJar).Cookies", Method, 0, ""}, + {"(CookieJar).SetCookies", Method, 0, ""}, {"(Dir).Open", Method, 0, ""}, + {"(File).Close", Method, 0, ""}, + {"(File).Read", Method, 0, ""}, + {"(File).Readdir", Method, 0, ""}, + {"(File).Seek", Method, 0, ""}, + {"(File).Stat", Method, 0, ""}, + {"(FileSystem).Open", Method, 0, ""}, + {"(Flusher).Flush", Method, 0, ""}, + {"(Handler).ServeHTTP", Method, 0, ""}, {"(HandlerFunc).ServeHTTP", Method, 0, ""}, {"(Header).Add", Method, 0, ""}, {"(Header).Clone", Method, 13, ""}, @@ -8405,10 +8816,16 @@ var PackageSymbols = map[string][]Symbol{ {"(Header).Values", Method, 14, ""}, {"(Header).Write", Method, 0, ""}, {"(Header).WriteSubset", Method, 0, ""}, + {"(Hijacker).Hijack", Method, 0, ""}, {"(Protocols).HTTP1", Method, 24, ""}, {"(Protocols).HTTP2", Method, 24, ""}, {"(Protocols).String", Method, 24, ""}, {"(Protocols).UnencryptedHTTP2", Method, 24, ""}, + {"(Pusher).Push", Method, 8, ""}, + {"(ResponseWriter).Header", Method, 0, ""}, + {"(ResponseWriter).Write", Method, 0, ""}, + {"(ResponseWriter).WriteHeader", Method, 0, ""}, + {"(RoundTripper).RoundTrip", Method, 0, ""}, {"AllowQuerySemicolons", Func, 17, "func(h Handler) Handler"}, {"CanonicalHeaderKey", Func, 0, "func(s string) string"}, {"Client", Type, 0, ""}, @@ -8416,6 +8833,7 @@ var PackageSymbols = map[string][]Symbol{ {"Client.Jar", Field, 0, ""}, {"Client.Timeout", Field, 3, ""}, {"Client.Transport", Field, 0, ""}, + {"ClientConn", Type, 26, ""}, {"CloseNotifier", Type, 1, ""}, {"ConnState", Type, 3, ""}, {"Cookie", Type, 0, ""}, @@ -8726,6 +9144,8 @@ var PackageSymbols = map[string][]Symbol{ "net/http/cookiejar": { {"(*Jar).Cookies", Method, 1, ""}, {"(*Jar).SetCookies", Method, 1, ""}, + {"(PublicSuffixList).PublicSuffix", Method, 1, ""}, + {"(PublicSuffixList).String", Method, 1, ""}, {"Jar", Type, 1, ""}, {"New", Func, 1, "func(o *Options) (*Jar, error)"}, {"Options", Type, 1, ""}, @@ -8819,6 +9239,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*ServerConn).Pending", Method, 0, ""}, {"(*ServerConn).Read", Method, 0, ""}, {"(*ServerConn).Write", Method, 0, ""}, + {"(BufferPool).Get", Method, 6, ""}, + {"(BufferPool).Put", Method, 6, ""}, {"BufferPool", Type, 6, ""}, {"ClientConn", Type, 0, ""}, {"DumpRequest", Func, 0, "func(req *http.Request, body bool) ([]byte, error)"}, @@ -8972,6 +9394,14 @@ var PackageSymbols = map[string][]Symbol{ {"(*Server).ServeConn", Method, 0, ""}, {"(*Server).ServeHTTP", Method, 0, ""}, {"(*Server).ServeRequest", Method, 0, ""}, + {"(ClientCodec).Close", Method, 0, ""}, + {"(ClientCodec).ReadResponseBody", Method, 0, ""}, + {"(ClientCodec).ReadResponseHeader", Method, 0, ""}, + {"(ClientCodec).WriteRequest", Method, 0, ""}, + {"(ServerCodec).Close", Method, 0, ""}, + {"(ServerCodec).ReadRequestBody", Method, 0, ""}, + {"(ServerCodec).ReadRequestHeader", Method, 0, ""}, + {"(ServerCodec).WriteResponse", Method, 0, ""}, {"(ServerError).Error", Method, 0, ""}, {"Accept", Func, 0, "func(lis net.Listener)"}, {"Call", Type, 0, ""}, @@ -9030,6 +9460,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*Client).StartTLS", Method, 0, ""}, {"(*Client).TLSConnectionState", Method, 5, ""}, {"(*Client).Verify", Method, 0, ""}, + {"(Auth).Next", Method, 0, ""}, + {"(Auth).Start", Method, 0, ""}, {"Auth", Type, 0, ""}, {"CRAMMD5Auth", Func, 0, "func(username string, secret string) Auth"}, {"Client", Type, 0, ""}, @@ -9241,10 +9673,18 @@ var PackageSymbols = map[string][]Symbol{ {"(*SyscallError).Error", Method, 0, ""}, {"(*SyscallError).Timeout", Method, 10, ""}, {"(*SyscallError).Unwrap", Method, 13, ""}, + {"(FileInfo).IsDir", Method, 0, ""}, + {"(FileInfo).ModTime", Method, 0, ""}, + {"(FileInfo).Mode", Method, 0, ""}, + {"(FileInfo).Name", Method, 0, ""}, + {"(FileInfo).Size", Method, 0, ""}, + {"(FileInfo).Sys", Method, 0, ""}, {"(FileMode).IsDir", Method, 0, ""}, {"(FileMode).IsRegular", Method, 1, ""}, {"(FileMode).Perm", Method, 0, ""}, {"(FileMode).String", Method, 0, ""}, + {"(Signal).Signal", Method, 0, ""}, + {"(Signal).String", Method, 0, ""}, {"Args", Var, 0, ""}, {"Chdir", Func, 0, "func(dir string) error"}, {"Chmod", Func, 0, "func(name string, mode FileMode) error"}, @@ -9521,6 +9961,45 @@ var PackageSymbols = map[string][]Symbol{ {"(StructField).IsExported", Method, 17, ""}, {"(StructTag).Get", Method, 0, ""}, {"(StructTag).Lookup", Method, 7, ""}, + {"(Type).Align", Method, 0, ""}, + {"(Type).AssignableTo", Method, 0, ""}, + {"(Type).Bits", Method, 0, ""}, + {"(Type).CanSeq", Method, 23, ""}, + {"(Type).CanSeq2", Method, 23, ""}, + {"(Type).ChanDir", Method, 0, ""}, + {"(Type).Comparable", Method, 4, ""}, + {"(Type).ConvertibleTo", Method, 1, ""}, + {"(Type).Elem", Method, 0, ""}, + {"(Type).Field", Method, 0, ""}, + {"(Type).FieldAlign", Method, 0, ""}, + {"(Type).FieldByIndex", Method, 0, ""}, + {"(Type).FieldByName", Method, 0, ""}, + {"(Type).FieldByNameFunc", Method, 0, ""}, + {"(Type).Fields", Method, 26, ""}, + {"(Type).Implements", Method, 0, ""}, + {"(Type).In", Method, 0, ""}, + {"(Type).Ins", Method, 26, ""}, + {"(Type).IsVariadic", Method, 0, ""}, + {"(Type).Key", Method, 0, ""}, + {"(Type).Kind", Method, 0, ""}, + {"(Type).Len", Method, 0, ""}, + {"(Type).Method", Method, 0, ""}, + {"(Type).MethodByName", Method, 0, ""}, + {"(Type).Methods", Method, 26, ""}, + {"(Type).Name", Method, 0, ""}, + {"(Type).NumField", Method, 0, ""}, + {"(Type).NumIn", Method, 0, ""}, + {"(Type).NumMethod", Method, 0, ""}, + {"(Type).NumOut", Method, 0, ""}, + {"(Type).Out", Method, 0, ""}, + {"(Type).Outs", Method, 26, ""}, + {"(Type).OverflowComplex", Method, 23, ""}, + {"(Type).OverflowFloat", Method, 23, ""}, + {"(Type).OverflowInt", Method, 23, ""}, + {"(Type).OverflowUint", Method, 23, ""}, + {"(Type).PkgPath", Method, 0, ""}, + {"(Type).Size", Method, 0, ""}, + {"(Type).String", Method, 0, ""}, {"(Value).Addr", Method, 0, ""}, {"(Value).Bool", Method, 0, ""}, {"(Value).Bytes", Method, 0, ""}, @@ -9547,6 +10026,7 @@ var PackageSymbols = map[string][]Symbol{ {"(Value).FieldByIndexErr", Method, 18, ""}, {"(Value).FieldByName", Method, 0, ""}, {"(Value).FieldByNameFunc", Method, 0, ""}, + {"(Value).Fields", Method, 26, ""}, {"(Value).Float", Method, 0, ""}, {"(Value).Grow", Method, 20, ""}, {"(Value).Index", Method, 0, ""}, @@ -9563,6 +10043,7 @@ var PackageSymbols = map[string][]Symbol{ {"(Value).MapRange", Method, 12, ""}, {"(Value).Method", Method, 0, ""}, {"(Value).MethodByName", Method, 0, ""}, + {"(Value).Methods", Method, 26, ""}, {"(Value).NumField", Method, 0, ""}, {"(Value).NumMethod", Method, 0, ""}, {"(Value).OverflowComplex", Method, 0, ""}, @@ -9678,7 +10159,6 @@ var PackageSymbols = map[string][]Symbol{ {"StructOf", Func, 7, "func(fields []StructField) Type"}, {"StructTag", Type, 0, ""}, {"Swapper", Func, 8, "func(slice any) func(i int, j int)"}, - {"Type", Type, 0, ""}, {"TypeAssert", Func, 25, "func[T any](v Value) (T, bool)"}, {"TypeFor", Func, 22, "func[T any]() Type"}, {"TypeOf", Func, 0, "func(i any) Type"}, @@ -9880,6 +10360,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*TypeAssertionError).Error", Method, 0, ""}, {"(*TypeAssertionError).RuntimeError", Method, 0, ""}, {"(Cleanup).Stop", Method, 24, ""}, + {"(Error).Error", Method, 0, ""}, + {"(Error).RuntimeError", Method, 0, ""}, {"AddCleanup", Func, 24, "func[T, S any](ptr *T, cleanup func(S), arg S) Cleanup"}, {"BlockProfile", Func, 1, "func(p []BlockProfileRecord) (n int, ok bool)"}, {"BlockProfileRecord", Type, 1, ""}, @@ -10154,6 +10636,9 @@ var PackageSymbols = map[string][]Symbol{ {"(IntSlice).Search", Method, 0, ""}, {"(IntSlice).Sort", Method, 0, ""}, {"(IntSlice).Swap", Method, 0, ""}, + {"(Interface).Len", Method, 0, ""}, + {"(Interface).Less", Method, 0, ""}, + {"(Interface).Swap", Method, 0, ""}, {"(StringSlice).Len", Method, 0, ""}, {"(StringSlice).Less", Method, 0, ""}, {"(StringSlice).Search", Method, 0, ""}, @@ -10345,6 +10830,8 @@ var PackageSymbols = map[string][]Symbol{ {"(*WaitGroup).Done", Method, 0, ""}, {"(*WaitGroup).Go", Method, 25, ""}, {"(*WaitGroup).Wait", Method, 0, ""}, + {"(Locker).Lock", Method, 0, ""}, + {"(Locker).Unlock", Method, 0, ""}, {"Cond", Type, 0, ""}, {"Cond.L", Field, 0, ""}, {"Locker", Type, 0, ""}, @@ -10486,10 +10973,14 @@ var PackageSymbols = map[string][]Symbol{ {"(*Timeval).Nano", Method, 0, ""}, {"(*Timeval).Nanoseconds", Method, 0, ""}, {"(*Timeval).Unix", Method, 0, ""}, + {"(Conn).SyscallConn", Method, 9, ""}, {"(Errno).Error", Method, 0, ""}, {"(Errno).Is", Method, 13, ""}, {"(Errno).Temporary", Method, 0, ""}, {"(Errno).Timeout", Method, 0, ""}, + {"(RawConn).Control", Method, 9, ""}, + {"(RawConn).Read", Method, 9, ""}, + {"(RawConn).Write", Method, 9, ""}, {"(Signal).Signal", Method, 0, ""}, {"(Signal).String", Method, 0, ""}, {"(Token).Close", Method, 0, ""}, @@ -14409,7 +14900,7 @@ var PackageSymbols = map[string][]Symbol{ {"RouteMessage.Data", Field, 0, ""}, {"RouteMessage.Header", Field, 0, ""}, {"RouteRIB", Func, 0, ""}, - {"RoutingMessage", Type, 0, ""}, + {"RoutingMessage", Type, 14, ""}, {"RtAttr", Type, 0, ""}, {"RtAttr.Len", Field, 0, ""}, {"RtAttr.Type", Field, 0, ""}, @@ -15895,7 +16386,6 @@ var PackageSymbols = map[string][]Symbol{ {"SockFprog.Filter", Field, 0, ""}, {"SockFprog.Len", Field, 0, ""}, {"SockFprog.Pad_cgo_0", Field, 0, ""}, - {"Sockaddr", Type, 0, ""}, {"SockaddrDatalink", Type, 0, ""}, {"SockaddrDatalink.Alen", Field, 0, ""}, {"SockaddrDatalink.Data", Field, 0, ""}, @@ -16801,6 +17291,29 @@ var PackageSymbols = map[string][]Symbol{ {"(BenchmarkResult).MemString", Method, 1, ""}, {"(BenchmarkResult).NsPerOp", Method, 0, ""}, {"(BenchmarkResult).String", Method, 0, ""}, + {"(TB).ArtifactDir", Method, 26, ""}, + {"(TB).Attr", Method, 25, ""}, + {"(TB).Chdir", Method, 24, ""}, + {"(TB).Cleanup", Method, 14, ""}, + {"(TB).Context", Method, 24, ""}, + {"(TB).Error", Method, 2, ""}, + {"(TB).Errorf", Method, 2, ""}, + {"(TB).Fail", Method, 2, ""}, + {"(TB).FailNow", Method, 2, ""}, + {"(TB).Failed", Method, 2, ""}, + {"(TB).Fatal", Method, 2, ""}, + {"(TB).Fatalf", Method, 2, ""}, + {"(TB).Helper", Method, 9, ""}, + {"(TB).Log", Method, 2, ""}, + {"(TB).Logf", Method, 2, ""}, + {"(TB).Name", Method, 8, ""}, + {"(TB).Output", Method, 25, ""}, + {"(TB).Setenv", Method, 17, ""}, + {"(TB).Skip", Method, 2, ""}, + {"(TB).SkipNow", Method, 2, ""}, + {"(TB).Skipf", Method, 2, ""}, + {"(TB).Skipped", Method, 2, ""}, + {"(TB).TempDir", Method, 15, ""}, {"AllocsPerRun", Func, 1, "func(runs int, f func()) (avg float64)"}, {"B", Type, 0, ""}, {"B.N", Field, 0, ""}, @@ -16851,7 +17364,6 @@ var PackageSymbols = map[string][]Symbol{ {"RunTests", Func, 0, "func(matchString func(pat string, str string) (bool, error), tests []InternalTest) (ok bool)"}, {"Short", Func, 0, "func() bool"}, {"T", Type, 0, ""}, - {"TB", Type, 2, ""}, {"Testing", Func, 21, "func() bool"}, {"Verbose", Func, 1, "func() bool"}, }, @@ -16887,6 +17399,7 @@ var PackageSymbols = map[string][]Symbol{ "testing/quick": { {"(*CheckEqualError).Error", Method, 0, ""}, {"(*CheckError).Error", Method, 0, ""}, + {"(Generator).Generate", Method, 0, ""}, {"(SetupError).Error", Method, 0, ""}, {"Check", Func, 0, "func(f any, config *Config) error"}, {"CheckEqual", Func, 0, "func(f any, g any, config *Config) error"}, @@ -17093,6 +17606,10 @@ var PackageSymbols = map[string][]Symbol{ {"(ListNode).Position", Method, 1, ""}, {"(ListNode).Type", Method, 0, ""}, {"(NilNode).Position", Method, 1, ""}, + {"(Node).Copy", Method, 0, ""}, + {"(Node).Position", Method, 1, ""}, + {"(Node).String", Method, 0, ""}, + {"(Node).Type", Method, 0, ""}, {"(NodeType).Type", Method, 0, ""}, {"(NumberNode).Position", Method, 1, ""}, {"(NumberNode).Type", Method, 0, ""}, diff --git a/internal/stdlib/stdlib.go b/internal/stdlib/stdlib.go index e223e0f3405..59a5de36a23 100644 --- a/internal/stdlib/stdlib.go +++ b/internal/stdlib/stdlib.go @@ -39,7 +39,7 @@ const ( Var // "EOF" Const // "Pi" Field // "Point.X" - Method // "(*Buffer).Grow" + Method // "(*Buffer).Grow" or "(Reader).Read" ) func (kind Kind) String() string { diff --git a/internal/stdlib/testdata/nethttp.deps b/internal/stdlib/testdata/nethttp.deps index 542527fcbbc..a3fe417481d 100644 --- a/internal/stdlib/testdata/nethttp.deps +++ b/internal/stdlib/testdata/nethttp.deps @@ -25,6 +25,7 @@ internal/msan internal/race internal/runtime/math internal/runtime/maps +internal/runtime/pprof/label internal/stringslite internal/trace/tracev2 runtime @@ -130,6 +131,8 @@ crypto/hmac crypto/internal/fips140/mlkem crypto/internal/fips140/tls12 crypto/internal/fips140/tls13 +crypto/mlkem +crypto/sha256 vendor/golang.org/x/crypto/internal/alias vendor/golang.org/x/crypto/chacha20 vendor/golang.org/x/crypto/internal/poly1305 @@ -141,7 +144,6 @@ crypto/rc4 crypto/internal/fips140/rsa crypto/rsa crypto/sha1 -crypto/sha256 crypto/fips140 crypto/tls/internal/fips140tls crypto/dsa From e1317381e4978ade829bf38cdc21d67308ca7c89 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 20 Nov 2025 17:21:36 -0500 Subject: [PATCH 35/60] go/packages: suppress test on (e.g.) wasm Change-Id: I4bb4da6a31e6dcfca591277954db61f4b97e7e5f Reviewed-on: https://go-review.googlesource.com/c/tools/+/722600 LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Matloob Reviewed-by: Michael Matloob --- go/packages/packages_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/go/packages/packages_test.go b/go/packages/packages_test.go index 94961cd4057..5a3ba1e66db 100644 --- a/go/packages/packages_test.go +++ b/go/packages/packages_test.go @@ -3407,6 +3407,8 @@ func TestLoadFileRelativePath(t *testing.T) { } func TestLoad_populatesInfoMapsForUnsafePackage(t *testing.T) { + testenv.NeedsGoPackages(t) + pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadAllSyntax}, "unsafe") if err != nil { t.Fatal(err) From 98d172d8bd13237ac9bc6d08fd294324ddc791dd Mon Sep 17 00:00:00 2001 From: Hongxiang Jiang Date: Mon, 17 Nov 2025 23:40:43 -0500 Subject: [PATCH 36/60] gopls/internal/protocol: add form field in type CodeAction The detailed protocol description is added to ResolveCodeAction method doc comments and relevant type's doc comments. For golang/go#76331 Change-Id: I8a7f67a7c57cefab4df674775f14ef9036bed245 Reviewed-on: https://go-review.googlesource.com/c/tools/+/721540 Reviewed-by: Alan Donovan Auto-Submit: Hongxiang Jiang LUCI-TryBot-Result: Go LUCI --- gopls/internal/protocol/form.go | 88 ++++++++++++++++++++++++ gopls/internal/protocol/generate/main.go | 61 ++++++++++++++++ gopls/internal/protocol/tsprotocol.go | 30 ++++++++ gopls/internal/protocol/tsserver.go | 14 ++++ 4 files changed, 193 insertions(+) create mode 100644 gopls/internal/protocol/form.go diff --git a/gopls/internal/protocol/form.go b/gopls/internal/protocol/form.go new file mode 100644 index 00000000000..811ced948f5 --- /dev/null +++ b/gopls/internal/protocol/form.go @@ -0,0 +1,88 @@ +// 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. + +// This file defines types for an experimental feature (golang/go#76331, +// microsoft/language-server-protocol#1164). As an experimental feature, +// these types are not yet included in tsprotocol.go. + +package protocol + +// FormFieldTypeString defines a text input. +// +// It is defined as a struct to allow for future extensibility, such as +// adding regex validation or file URI constraints. +type FormFieldTypeString struct { + // Kind must be "string". + Kind string `json:"kind"` +} + +// FormFieldTypeBool defines a boolean input. +type FormFieldTypeBool struct { + // Kind must be "bool". + Kind string `json:"kind"` +} + +// FormFieldTypeNumber defines a numeric input. +// +// It is defined as a struct to allow for future extensibility, such as +// adding range constraints (min/max) or precision requirements. +type FormFieldTypeNumber struct { + // Kind must be "number". + Kind string `json:"kind"` +} + +// FormFieldTypeEnum defines a selection from a set of values. +type FormFieldTypeEnum struct { + // Kind must be "enum". + Kind string `json:"kind"` + + // Name is an optional identifier for the enum type. + Name string `json:"name,omitempty"` + + // Values is the set of allowable options. + Values []string `json:"values"` + + // Description provides human-readable labels for the options. + // + // This slice must have the same length as Values, where Description[i] + // corresponds to Values[i]. + Description []string `json:"description"` +} + +// FormFieldTypeList defines a homogenous list of items. +type FormFieldTypeList struct { + // Kind must be "list". + Kind string `json:"kind"` + + // ElementType specifies the type of the items in the list. + // It must be one of the FormFieldType* structs (e.g., FormFieldTypeString). + ElementType any `json:"elementType"` +} + +// FormField describes a single question in a form and its validation state. +type FormField struct { + // Description is the text content of the question (the prompt) presented + // to the user. + Description string `json:"description"` + + // Type specifies the data type and validation constraints for the answer. + // + // It must be one of the FormFieldType* structs. The Kind field within the + // struct determines the expected data type. + // + // The language client is expected to render an input appropriate for this + // type. If the client does not support the specified type, it should + // fall back to a string input. + Type any `json:"type"` + + // Default specifies an optional initial value for the answer. + // + // If Type is FormFieldTypeEnum, this value must be present in the enum's + // Values slice. + Default any `json:"default,omitempty"` + + // Error provides a validation message from the language server. + // If empty, the current answer is considered valid. + Error string `json:"error,omitempty"` +} diff --git a/gopls/internal/protocol/generate/main.go b/gopls/internal/protocol/generate/main.go index 828fddd94ba..be633cc7aff 100644 --- a/gopls/internal/protocol/generate/main.go +++ b/gopls/internal/protocol/generate/main.go @@ -147,6 +147,23 @@ func writeserver() { `) out.WriteString("type Server interface {\n") for _, k := range sdecls.keys() { + if k == "codeAction/resolve" { + sdecls[k] = `// To support microsoft/language-server-protocol#1164, the language server +// need to read the client-supplied form answers and either returns a +// CodeAction with errors in the form fields surfacing the error to the +// client, or a CodeAction with properties the language client is waiting +// for (e.g. edits, commands). +// +// The language client may call "codeAction/resolve" if the language server +// returns a CodeAction with errors or try asking the user for completing the +// form again. +// +// The language client may call "codeAction/resolve" multiple times with user +// filled (re-filled) answers in the form until it obtains a CodeAction with +// properties (e.g. edits, commands) it's waiting for. +// +` + sdecls[k] + } out.WriteString(sdecls[k]) } out.WriteString(` @@ -195,6 +212,50 @@ func writeprotocol() { hack("WorkspaceFoldersServerCapabilities", "WorkspaceFolders5Gn") hack("_InitializeParams", "XInitializeParams") + // Insert a block of content into a type. + insert := func(key, content string) { + if _, ok := types[key]; !ok { + log.Fatalf("types[%q] not found", key) + } + idx := strings.LastIndex(types[key], "}") + if idx == -1 { + log.Fatalf("could not find '}' in type %q", key) + } + types[key] = types[key][:idx] + content + types[key][idx:] + } + + // TODO(hxjiang): extend form resolve to codelens resolve. + insert("CodeAction", ` +// FormFields and FormAnswers allow the server and client to exchange +// interactive questions and answers during a resolveCodeAction request. +// +// The server populates FormFields to define the schema. The server may +// optionally populate FormAnswers to preserve previous user input; if +// provided, the client may present these as default values. +// +// When the client responds, it must provide FormAnswers. The client is not +// required to send FormFields back to the server. + +// FormFields defines the questions and validation errors. +// +// This is a server-to-client field. The language server defines these, and +// the client uses them to render the form. +// +// Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#1164. +FormFields []FormField `+"`json:\"formFields,omitempty\"`") + insert("CodeAction", ` +// FormAnswers contains the values 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 client, 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. +// +// Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#1164. +FormAnswers []any `+"`json:\"formAnswers,omitempty\"`") + for _, k := range types.keys() { if k == "WatchKind" { types[k] = "type WatchKind = uint32" // strict gopls compatibility needs the '=' diff --git a/gopls/internal/protocol/tsprotocol.go b/gopls/internal/protocol/tsprotocol.go index 1046ec20b60..a409c019e04 100644 --- a/gopls/internal/protocol/tsprotocol.go +++ b/gopls/internal/protocol/tsprotocol.go @@ -577,6 +577,36 @@ type CodeAction struct { // // @since 3.18.0 - proposed Tags []CodeActionTag `json:"tags,omitempty"` + + // FormFields and FormAnswers allow the server and client to exchange + // interactive questions and answers during a resolveCodeAction request. + // + // The server populates FormFields to define the schema. The server may + // optionally populate FormAnswers to preserve previous user input; if + // provided, the client may present these as default values. + // + // When the client responds, it must provide FormAnswers. The client is not + // required to send FormFields back to the server. + + // FormFields defines the questions and validation errors. + // + // This is a server-to-client field. The language server defines these, and + // the client uses them to render the form. + // + // 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. + // + // When sent by the language server, this field is optional but recommended + // to support editing previous values. + // + // When sent by the language client, 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. + // + // Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#1164. + FormAnswers []any `json:"formAnswers,omitempty"` } // The Client Capabilities of a {@link CodeActionRequest}. diff --git a/gopls/internal/protocol/tsserver.go b/gopls/internal/protocol/tsserver.go index c21e98b6e7b..7b010e9ab14 100644 --- a/gopls/internal/protocol/tsserver.go +++ b/gopls/internal/protocol/tsserver.go @@ -27,6 +27,20 @@ type Server interface { IncomingCalls(context.Context, *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#callHierarchy_outgoingCalls OutgoingCalls(context.Context, *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) + // To support microsoft/language-server-protocol#1164, the language server + // need to read the client-supplied form answers and either returns a + // CodeAction with errors in the form fields surfacing the error to the + // client, or a CodeAction with properties the language client is waiting + // for (e.g. edits, commands). + // + // The language client may call "codeAction/resolve" if the language server + // returns a CodeAction with errors or try asking the user for completing the + // form again. + // + // The language client may call "codeAction/resolve" multiple times with user + // filled (re-filled) answers in the form until it obtains a CodeAction with + // properties (e.g. edits, commands) it's waiting for. + // // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#codeAction_resolve ResolveCodeAction(context.Context, *CodeAction) (*CodeAction, error) // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#codeLens_resolve From d32ec344545c517da66ce368d0298ed9655d27ac Mon Sep 17 00:00:00 2001 From: Hongxiang Jiang Date: Fri, 21 Nov 2025 02:38:34 -0500 Subject: [PATCH 37/60] gopls/internal/protocol/generate: move injections to tables.go For golang/go#76331 Change-Id: Id14b41c56789ad0fefc6fbd43bca9065a9008b60 Reviewed-on: https://go-review.googlesource.com/c/tools/+/722740 Auto-Submit: Hongxiang Jiang LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- gopls/internal/protocol/generate/main.go | 61 ---------------------- gopls/internal/protocol/generate/output.go | 7 +++ gopls/internal/protocol/generate/tables.go | 52 ++++++++++++++++++ 3 files changed, 59 insertions(+), 61 deletions(-) diff --git a/gopls/internal/protocol/generate/main.go b/gopls/internal/protocol/generate/main.go index be633cc7aff..828fddd94ba 100644 --- a/gopls/internal/protocol/generate/main.go +++ b/gopls/internal/protocol/generate/main.go @@ -147,23 +147,6 @@ func writeserver() { `) out.WriteString("type Server interface {\n") for _, k := range sdecls.keys() { - if k == "codeAction/resolve" { - sdecls[k] = `// To support microsoft/language-server-protocol#1164, the language server -// need to read the client-supplied form answers and either returns a -// CodeAction with errors in the form fields surfacing the error to the -// client, or a CodeAction with properties the language client is waiting -// for (e.g. edits, commands). -// -// The language client may call "codeAction/resolve" if the language server -// returns a CodeAction with errors or try asking the user for completing the -// form again. -// -// The language client may call "codeAction/resolve" multiple times with user -// filled (re-filled) answers in the form until it obtains a CodeAction with -// properties (e.g. edits, commands) it's waiting for. -// -` + sdecls[k] - } out.WriteString(sdecls[k]) } out.WriteString(` @@ -212,50 +195,6 @@ func writeprotocol() { hack("WorkspaceFoldersServerCapabilities", "WorkspaceFolders5Gn") hack("_InitializeParams", "XInitializeParams") - // Insert a block of content into a type. - insert := func(key, content string) { - if _, ok := types[key]; !ok { - log.Fatalf("types[%q] not found", key) - } - idx := strings.LastIndex(types[key], "}") - if idx == -1 { - log.Fatalf("could not find '}' in type %q", key) - } - types[key] = types[key][:idx] + content + types[key][idx:] - } - - // TODO(hxjiang): extend form resolve to codelens resolve. - insert("CodeAction", ` -// FormFields and FormAnswers allow the server and client to exchange -// interactive questions and answers during a resolveCodeAction request. -// -// The server populates FormFields to define the schema. The server may -// optionally populate FormAnswers to preserve previous user input; if -// provided, the client may present these as default values. -// -// When the client responds, it must provide FormAnswers. The client is not -// required to send FormFields back to the server. - -// FormFields defines the questions and validation errors. -// -// This is a server-to-client field. The language server defines these, and -// the client uses them to render the form. -// -// Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#1164. -FormFields []FormField `+"`json:\"formFields,omitempty\"`") - insert("CodeAction", ` -// FormAnswers contains the values 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 client, 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. -// -// Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#1164. -FormAnswers []any `+"`json:\"formAnswers,omitempty\"`") - for _, k := range types.keys() { if k == "WatchKind" { types[k] = "type WatchKind = uint32" // strict gopls compatibility needs the '=' diff --git a/gopls/internal/protocol/generate/output.go b/gopls/internal/protocol/generate/output.go index 04aec0e5892..4c954f86a65 100644 --- a/gopls/internal/protocol/generate/output.go +++ b/gopls/internal/protocol/generate/output.go @@ -74,6 +74,9 @@ func genDecl(model *Model, method string, param, result *Type, dir string) { } fragment := strings.ReplaceAll(strings.TrimPrefix(method, "$/"), "/", "_") msg := fmt.Sprintf("\t%s\t%s(context.Context%s) %s\n", lspLink(model, fragment), fname, p, ret) + if doc, ok := prependMethodDocComments[fname]; ok { + msg = doc + "\n\t//\n" + msg + } switch dir { case "clientToServer": sdecls[method] = msg @@ -294,6 +297,10 @@ func genProps(out *bytes.Buffer, props []NameType, name string) { fmt.Fprintf(out, "\t%s %s %s\n", goName(p.Name), tp, json) } } + + if block, ok := appendTypeProp[name]; ok { + out.WriteString(block) + } } func genAliases(model *Model) { diff --git a/gopls/internal/protocol/generate/tables.go b/gopls/internal/protocol/generate/tables.go index 9d48eda1117..e8b965a0c68 100644 --- a/gopls/internal/protocol/generate/tables.go +++ b/gopls/internal/protocol/generate/tables.go @@ -284,3 +284,55 @@ func methodName(method string) string { } return ans } + +// prependMethodDocComments specifies doc comments that will be prepend to +// an LSP method name defined in both Server and Client interface. +var prependMethodDocComments = map[string]string{ + "ResolveCodeAction": `// To support microsoft/language-server-protocol#1164, the language server + // need to read the form with client-supplied answers and either returns a + // CodeAction with errors in the form surfacing the error to the client, or a + // CodeAction with properties the language client is waiting for (e.g. edits, + // commands). + // + // The language client may call "codeAction/resolve" if the language server + // returns a CodeAction with errors or try asking the user for completing the + // form again. + // The language client may call "codeAction/resolve" multiple times with user + // filled (re-filled) answers in the form until it obtains a CodeAction with + // properties (e.g. edits, commands) it's waiting for.`, +} + +// appendTypeProp specifies block of code (typically properties with doc comment) +// that will be append to a struct. +var appendTypeProp = map[string]string{ + "CodeAction": ` + // FormFields and FormAnswers allow the server and client to exchange + // interactive questions and answers during a resolveCodeAction request. + // + // The server populates FormFields to define the schema. The server may + // optionally populate FormAnswers to preserve previous user input; if + // provided, the client may present these as default values. + // + // When the client responds, it must provide FormAnswers. The client is not + // required to send FormFields back to the server. + + // FormFields defines the questions and validation errors. + // + // This is a server-to-client field. The language server defines these, and + // the client uses them to render the form. + // + // 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. + // + // When sent by the language server, this field is optional but recommended + // to support editing previous values. + // + // When sent by the language client, 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. + // + // Note: This is a non-standard protocol extension. See microsoft/language-server-protocol#1164. + FormAnswers []any ` + "`json:\"formAnswers,omitempty\"`", +} From 2e3e83a0503ddd0df601b5ea038a9c2df959b38e Mon Sep 17 00:00:00 2001 From: Gavin Lam Date: Wed, 26 Nov 2025 04:11:59 +0000 Subject: [PATCH 38/60] internal/refactor/inline: preserve local package name used by callee The existing implementation generates local package names by appending a numeric suffix to package name, which reduces readability. This improvement preserves the local package name used by the callee when available and fall back to the existing name generation mechanism if a conflict occurs. Updates golang/go#63352 Fixes golang/go#74965 Change-Id: I5fdf5948cf6036786f6e92458c9d53210f2d5450 GitHub-Last-Rev: f67b6a92da4d269b2a63ac4f352bd6e3fc263601 GitHub-Pull-Request: golang/tools#601 Reviewed-on: https://go-review.googlesource.com/c/tools/+/712981 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley Reviewed-by: Alan Donovan --- internal/refactor/inline/inline.go | 44 ++++++++------ .../refactor/inline/testdata/assignment.txtar | 6 +- .../import-preserve-local-pkgname.txtar | 57 +++++++++++++++++++ .../refactor/inline/testdata/issue63298.txtar | 4 +- 4 files changed, 89 insertions(+), 22 deletions(-) create mode 100644 internal/refactor/inline/testdata/import-preserve-local-pkgname.txtar diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index af1252cee86..ae8e5b850d0 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -526,14 +526,10 @@ func (i *importState) importName(pkgPath string, shadow shadowMap) string { return "" } -// localName returns the local name for a given imported package path, -// adding one if it doesn't exists. -func (i *importState) localName(pkgPath, pkgName string, shadow shadowMap) string { - // Does an import already exist that works in this shadowing context? - if name := i.importName(pkgPath, shadow); name != "" { - return name - } - +// findNewLocalName returns a new local package name to use in a particular shadowing context. +// It considers the existing local name used by the callee, or construct a new local name +// based on the package name. +func (i *importState) findNewLocalName(pkgName, calleePkgName string, shadow shadowMap) string { newlyAdded := func(name string) bool { return slices.ContainsFunc(i.newImports, func(n newImport) bool { return n.pkgName == name }) } @@ -551,20 +547,34 @@ func (i *importState) localName(pkgPath, pkgName string, shadow shadowMap) strin // import added by callee // - // Choose local PkgName based on last segment of - // package path plus, if needed, a numeric suffix to - // ensure uniqueness. + // Try to preserve the local package name used by the callee first. // - // "init" is not a legal PkgName. + // If that is shadowed, choose a local package name based on last segment of + // package path plus, if needed, a numeric suffix to ensure uniqueness. // - // TODO(rfindley): is it worth preserving local package names for callee - // imports? Are they likely to be better or worse than the name we choose - // here? + // "init" is not a legal PkgName. + if shadow[calleePkgName] == 0 && !shadowedInCaller(calleePkgName) && !newlyAdded(calleePkgName) && calleePkgName != "init" { + return calleePkgName + } + base := pkgName name := base for n := 0; shadow[name] != 0 || shadowedInCaller(name) || newlyAdded(name) || name == "init"; n++ { name = fmt.Sprintf("%s%d", base, n) } + + return name +} + +// localName returns the local name for a given imported package path, +// adding one if it doesn't exists. +func (i *importState) localName(pkgPath, pkgName, calleePkgName string, shadow shadowMap) string { + // Does an import already exist that works in this shadowing context? + if name := i.importName(pkgPath, shadow); name != "" { + return name + } + + name := i.findNewLocalName(pkgName, calleePkgName, shadow) i.logf("adding import %s %q", name, pkgPath) spec := &ast.ImportSpec{ Path: &ast.BasicLit{ @@ -1381,7 +1391,7 @@ func (st *state) renameFreeObjs(istate *importState) ([]ast.Expr, error) { var newName ast.Expr if obj.Kind == "pkgname" { // Use locally appropriate import, creating as needed. - n := istate.localName(obj.PkgPath, obj.PkgName, obj.Shadow) + n := istate.localName(obj.PkgPath, obj.PkgName, obj.Name, obj.Shadow) newName = makeIdent(n) // imported package } else if !obj.ValidPos { // Built-in function, type, or value (e.g. nil, zero): @@ -1426,7 +1436,7 @@ func (st *state) renameFreeObjs(istate *importState) ([]ast.Expr, error) { // Form a qualified identifier, pkg.Name. if qualify { - pkgName := istate.localName(obj.PkgPath, obj.PkgName, obj.Shadow) + pkgName := istate.localName(obj.PkgPath, obj.PkgName, obj.PkgName, obj.Shadow) newName = &ast.SelectorExpr{ X: makeIdent(pkgName), Sel: makeIdent(obj.Name), diff --git a/internal/refactor/inline/testdata/assignment.txtar b/internal/refactor/inline/testdata/assignment.txtar index e201d601480..6e7ffc5f2b7 100644 --- a/internal/refactor/inline/testdata/assignment.txtar +++ b/internal/refactor/inline/testdata/assignment.txtar @@ -126,11 +126,11 @@ func _() { package a import ( - "testdata/c" - c0 "testdata/c2" + c1 "testdata/c" + c2 "testdata/c2" ) func _() { - x, y := c.C(0), c0.C(1) //@ inline(re"B", b4) + x, y := c1.C(0), c2.C(1) //@ inline(re"B", b4) _, _ = x, y } diff --git a/internal/refactor/inline/testdata/import-preserve-local-pkgname.txtar b/internal/refactor/inline/testdata/import-preserve-local-pkgname.txtar new file mode 100644 index 00000000000..70e11d47bbf --- /dev/null +++ b/internal/refactor/inline/testdata/import-preserve-local-pkgname.txtar @@ -0,0 +1,57 @@ +Test that when choosing local package names, we prefer the +name used by the callee over a disambiguating numeric suffix. + +-- go.mod -- +module example.com +go 1.19 + +-- main/main.go -- +package main + +import stringutil "example.com/string/util" +import "example.com/util" + +func main() { + util.A() //@ inline(re"A", result) + stringutil.B() +} + +-- util/util.go -- +package util + +import stringutil "example.com/string/util" +import urlutil "example.com/url/util" + +func A() { + stringutil.A() + urlutil.A() +} + +-- string/util/util.go -- +package util + +func A() { +} + +func B() { +} + +-- url/util/util.go -- +package util + +func A() { +} + +-- result -- +package main + +import ( + stringutil "example.com/string/util" + urlutil "example.com/url/util" +) + +func main() { + stringutil.A() + urlutil.A() //@ inline(re"A", result) + stringutil.B() +} diff --git a/internal/refactor/inline/testdata/issue63298.txtar b/internal/refactor/inline/testdata/issue63298.txtar index e7f36351219..b1b5d0c6a4f 100644 --- a/internal/refactor/inline/testdata/issue63298.txtar +++ b/internal/refactor/inline/testdata/issue63298.txtar @@ -38,11 +38,11 @@ func B() {} package a import ( - b0 "testdata/another/b" + anotherb "testdata/another/b" "testdata/b" ) func _() { b.B() - b0.B() //@ inline(re"a2", result) + anotherb.B() //@ inline(re"a2", result) } From ffbdcac342555420f0f6dfcdba3afef7e389daf6 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 3 Nov 2025 11:18:30 -0500 Subject: [PATCH 39/60] go/analysis/passes/modernize: stditerators: add reflect iters This CL adds to the stditerators table 6 new iterators in the reflect package. It also adds support for iter.Seq2, since the loop must be "for _, x", not "for x". (See CL 707356 for the addition of the actual iterators.) + tests For golang/go#66631 For golang/go#75693 Change-Id: I59b4f55a45316ecf6a9dda180a7d1f0f9134d130 Reviewed-on: https://go-review.googlesource.com/c/tools/+/717620 Reviewed-by: Madeline Kalil LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/modernize/stditerators.go | 68 ++++++++++++------- .../testdata/src/stditerators/stditerators.go | 33 ++++++++- .../src/stditerators/stditerators.go.golden | 33 ++++++++- 3 files changed, 109 insertions(+), 25 deletions(-) diff --git a/go/analysis/passes/modernize/stditerators.go b/go/analysis/passes/modernize/stditerators.go index cc595806714..f7318b123da 100644 --- a/go/analysis/passes/modernize/stditerators.go +++ b/go/analysis/passes/modernize/stditerators.go @@ -43,23 +43,29 @@ func init() { // iter.Seq. var stditeratorsTable = [...]struct { pkgpath, typename, lenmethod, atmethod, itermethod, elemname string + + seqn int // 1 or 2 => "for x" or "for _, x" }{ // Example: in go/types, (*Tuple).Variables returns an // iterator that replaces a loop over (*Tuple).{Len,At}. // The loop variable is named "v". - {"go/types", "Interface", "NumEmbeddeds", "EmbeddedType", "EmbeddedTypes", "etyp"}, - {"go/types", "Interface", "NumExplicitMethods", "ExplicitMethod", "ExplicitMethods", "method"}, - {"go/types", "Interface", "NumMethods", "Method", "Methods", "method"}, - {"go/types", "MethodSet", "Len", "At", "Methods", "method"}, - {"go/types", "Named", "NumMethods", "Method", "Methods", "method"}, - {"go/types", "Scope", "NumChildren", "Child", "Children", "child"}, - {"go/types", "Struct", "NumFields", "Field", "Fields", "field"}, - {"go/types", "Tuple", "Len", "At", "Variables", "v"}, - {"go/types", "TypeList", "Len", "At", "Types", "t"}, - {"go/types", "TypeParamList", "Len", "At", "TypeParams", "tparam"}, - {"go/types", "Union", "Len", "Term", "Terms", "term"}, - // TODO(adonovan): support Seq2. Bonus: transform uses of both key and value. - // {"reflect", "Value", "NumFields", "Field", "Fields", "field"}, + {"go/types", "Interface", "NumEmbeddeds", "EmbeddedType", "EmbeddedTypes", "etyp", 1}, + {"go/types", "Interface", "NumExplicitMethods", "ExplicitMethod", "ExplicitMethods", "method", 1}, + {"go/types", "Interface", "NumMethods", "Method", "Methods", "method", 1}, + {"go/types", "MethodSet", "Len", "At", "Methods", "method", 1}, + {"go/types", "Named", "NumMethods", "Method", "Methods", "method", 1}, + {"go/types", "Scope", "NumChildren", "Child", "Children", "child", 1}, + {"go/types", "Struct", "NumFields", "Field", "Fields", "field", 1}, + {"go/types", "Tuple", "Len", "At", "Variables", "v", 1}, + {"go/types", "TypeList", "Len", "At", "Types", "t", 1}, + {"go/types", "TypeParamList", "Len", "At", "TypeParams", "tparam", 1}, + {"go/types", "Union", "Len", "Term", "Terms", "term", 1}, + {"reflect", "Type", "NumField", "Field", "Fields", "field", 1}, + {"reflect", "Type", "NumMethod", "Method", "Methods", "method", 1}, + {"reflect", "Type", "NumIn", "In", "Ins", "in", 1}, + {"reflect", "Type", "NumOut", "Out", "Outs", "out", 1}, + {"reflect", "Value", "NumField", "Field", "Fields", "field", 2}, + {"reflect", "Value", "NumMethod", "Method", "Methods", "method", 2}, } // stditerators suggests fixes to replace loops using Len/At-style @@ -86,6 +92,19 @@ var stditeratorsTable = [...]struct { // the user hasn't intentionally chosen not to use an // iterator for that reason? We don't want to go fix to // undo optimizations. Do we need a suppression mechanism? +// +// TODO(adonovan): recognize the more complex patterns that +// could make full use of both components of an iter.Seq2, e.g. +// +// for i := 0; i < v.NumField(); i++ { +// use(v.Field(i), v.Type().Field(i)) +// } +// +// => +// +// for structField, field := range v.Fields() { +// use(structField, field) +// } func stditerators(pass *analysis.Pass) (any, error) { var ( index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) @@ -228,15 +247,17 @@ func stditerators(pass *analysis.Pass) (any, error) { indexVar = v curBody = curFor.ChildAt(edge.ForStmt_Body, -1) elem, elemVar = chooseName(curBody, lenSel.X, indexVar) + elemPrefix := cond(row.seqn == 2, "_, ", "") - // for i := 0; i < x.Len(); i++ { - // ---- ------- --- ----- - // for elem := range x.All() { + // for i := 0; i < x.Len(); i++ { + // ---- ------- --- ----- + // for elem := range x.All() { + // or for _, elem := ... edits = []analysis.TextEdit{ { Pos: v.Pos(), End: v.Pos() + token.Pos(len(v.Name())), - NewText: []byte(elem), + NewText: []byte(elemPrefix + elem), }, { Pos: loop.Init.(*ast.AssignStmt).Rhs[0].Pos(), @@ -271,6 +292,7 @@ func stditerators(pass *analysis.Pass) (any, error) { indexVar = info.Defs[id].(*types.Var) curBody = curRange.ChildAt(edge.RangeStmt_Body, -1) elem, elemVar = chooseName(curBody, lenSel.X, indexVar) + elemPrefix := cond(row.seqn == 2, "_, ", "") // for i := range x.Len() { // ---- --- @@ -279,7 +301,7 @@ func stditerators(pass *analysis.Pass) (any, error) { { Pos: loop.Key.Pos(), End: loop.Key.End(), - NewText: []byte(elem), + NewText: []byte(elemPrefix + elem), }, { Pos: lenSel.Sel.Pos(), @@ -344,8 +366,8 @@ func stditerators(pass *analysis.Pass) (any, error) { // (In the long run, version filters are not highly selective, // so there's no need to do them first, especially as this check // may be somewhat expensive.) - if v, ok := methodGoVersion(row.pkgpath, row.typename, row.itermethod); !ok { - panic("no version found") + if v, err := methodGoVersion(row.pkgpath, row.typename, row.itermethod); err != nil { + panic(err) } else if !analyzerutil.FileUsesGoVersion(pass, astutil.EnclosingFile(curLenCall), v.String()) { continue nextCall } @@ -371,7 +393,7 @@ func stditerators(pass *analysis.Pass) (any, error) { // methodGoVersion reports the version at which the method // (pkgpath.recvtype).method appeared in the standard library. -func methodGoVersion(pkgpath, recvtype, method string) (stdlib.Version, bool) { +func methodGoVersion(pkgpath, recvtype, method string) (stdlib.Version, error) { // TODO(adonovan): opt: this might be inefficient for large packages // like go/types. If so, memoize using a map (and kill two birds with // one stone by also memoizing the 'within' check above). @@ -379,9 +401,9 @@ func methodGoVersion(pkgpath, recvtype, method string) (stdlib.Version, bool) { if sym.Kind == stdlib.Method { _, recv, name := sym.SplitMethod() if recv == recvtype && name == method { - return sym.Version, true + return sym.Version, nil } } } - return 0, false + return 0, fmt.Errorf("methodGoVersion: %s.%s.%s missing from stdlib manifest", pkgpath, recvtype, method) } diff --git a/go/analysis/passes/modernize/testdata/src/stditerators/stditerators.go b/go/analysis/passes/modernize/testdata/src/stditerators/stditerators.go index 6bd9627615b..0d59fc8e315 100644 --- a/go/analysis/passes/modernize/testdata/src/stditerators/stditerators.go +++ b/go/analysis/passes/modernize/testdata/src/stditerators/stditerators.go @@ -1,6 +1,9 @@ package stditerators -import "go/types" +import ( + "go/types" + "reflect" +) func _(tuple *types.Tuple) { for i := 0; i < tuple.Len(); i++ { // want "Len/At loop can simplified using Tuple.Variables iteration" @@ -88,3 +91,31 @@ func _(tuple *types.Tuple) { } } } + +func _(t reflect.Type) { + for i := 0; i < t.NumField(); i++ { // want "NumField/Field loop can simplified using Type.Fields iteration" + print(t.Field(i)) + } + for i := 0; i < t.NumMethod(); i++ { // want "NumMethod/Method loop can simplified using Type.Methods iteration" + print(t.Method(i)) + } + for i := 0; i < t.NumIn(); i++ { // want "NumIn/In loop can simplified using Type.Ins iteration" + print(t.In(i)) + } + for i := 0; i < t.NumOut(); i++ { // want "NumOut/Out loop can simplified using Type.Outs iteration" + print(t.Out(i)) + } +} + +func _(v reflect.Value) { + for i := 0; i < v.NumField(); i++ { // want "NumField/Field loop can simplified using Value.Fields iteration" + print(v.Field(i)) + } + // Ideally we would use both parts of Value.Field's iter.Seq2 here. + for i := 0; i < v.NumField(); i++ { + print(v.Field(i), v.Type().Field(i)) // nope + } + for i := 0; i < v.NumMethod(); i++ { // want "NumMethod/Method loop can simplified using Value.Methods iteration" + print(v.Method(i)) + } +} diff --git a/go/analysis/passes/modernize/testdata/src/stditerators/stditerators.go.golden b/go/analysis/passes/modernize/testdata/src/stditerators/stditerators.go.golden index 514837d05ae..95d16393ca9 100644 --- a/go/analysis/passes/modernize/testdata/src/stditerators/stditerators.go.golden +++ b/go/analysis/passes/modernize/testdata/src/stditerators/stditerators.go.golden @@ -1,6 +1,9 @@ package stditerators -import "go/types" +import ( + "go/types" + "reflect" +) func _(tuple *types.Tuple) { for v := range tuple.Variables() { // want "Len/At loop can simplified using Tuple.Variables iteration" @@ -88,3 +91,31 @@ func _(tuple *types.Tuple) { } } } + +func _(t reflect.Type) { + for field := range t.Fields() { // want "NumField/Field loop can simplified using Type.Fields iteration" + print(field) + } + for method := range t.Methods() { // want "NumMethod/Method loop can simplified using Type.Methods iteration" + print(method) + } + for in := range t.Ins() { // want "NumIn/In loop can simplified using Type.Ins iteration" + print(in) + } + for out := range t.Outs() { // want "NumOut/Out loop can simplified using Type.Outs iteration" + print(out) + } +} + +func _(v reflect.Value) { + for _, field := range v.Fields() { // want "NumField/Field loop can simplified using Value.Fields iteration" + print(field) + } + // Ideally we would use both parts of Value.Field's iter.Seq2 here. + for i := 0; i < v.NumField(); i++ { + print(v.Field(i), v.Type().Field(i)) // nope + } + for _, method := range v.Methods() { // want "NumMethod/Method loop can simplified using Value.Methods iteration" + print(method) + } +} From 92a094998a1ac33852d1d611f7412511f6028818 Mon Sep 17 00:00:00 2001 From: "Nicholas S. Husin" Date: Wed, 26 Nov 2025 18:14:38 -0500 Subject: [PATCH 40/60] go/analysis/passes/modernize: rangeint: handle usages of loop label Currently, when a label is used, the rangeint modernizer fails to properly detect when i is used after a loop. This is because we only search for i usage within the for loop's parent subtree. When a label is used, the label becomes the loop's parent, and i can only be found in the loop's loop's parent subtree. To fix this, this change makes the rangeint modernizer look for i usage in the first ancestor of a for loop that is not a label. Fixes golang/go#76470 Change-Id: I79395efba26ad689514b7841d343932448ea9318 Reviewed-on: https://go-review.googlesource.com/c/tools/+/724960 Auto-Submit: Nicholas Husin Reviewed-by: Nicholas Husin Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Josh Rickmar --- go/analysis/passes/modernize/rangeint.go | 17 ++++++++++++++++- .../modernize/testdata/src/rangeint/rangeint.go | 16 ++++++++++++++++ .../testdata/src/rangeint/rangeint.go.golden | 15 +++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/go/analysis/passes/modernize/rangeint.go b/go/analysis/passes/modernize/rangeint.go index 6b1edf38b37..c42ec58ec3a 100644 --- a/go/analysis/passes/modernize/rangeint.go +++ b/go/analysis/passes/modernize/rangeint.go @@ -161,7 +161,22 @@ func rangeint(pass *analysis.Pass) (any, error) { // don't offer a fix, as a range loop // leaves i with a different final value (limit-1). if init.Tok == token.ASSIGN { - for curId := range curLoop.Parent().Preorder((*ast.Ident)(nil)) { + // Find the nearest ancestor that is not a label. + // Otherwise, checking for i usage outside of a for + // loop might not function properly further below. + // This is because the i usage might be a child of + // the loop's parent's parent, for example: + // var i int + // Loop: + // for i = 0; i < 10; i++ { break loop } + // // i is in the sibling of the label, not the loop + // fmt.Println(i) + // + ancestor := curLoop.Parent() + for is[*ast.LabeledStmt](ancestor.Node()) { + ancestor = ancestor.Parent() + } + for curId := range ancestor.Preorder((*ast.Ident)(nil)) { id := curId.Node().(*ast.Ident) if info.Uses[id] == v { // Is i used after loop? diff --git a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go index 6622a89e343..2a3de6b5986 100644 --- a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go +++ b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go @@ -271,3 +271,19 @@ func issue74687() { println(i) } } + +func issue76470() { + var i, j int +UsedAfter: + for i = 0; i < 10; i++ { // nope: i is accessed after the loop + break UsedAfter + } + if i == 9 { + panic("Modernizer changes behavior") + } + +NotUsedAfter: + for j = 0; j < 10; j++ { // want "for loop can be modernized using range over int" + break NotUsedAfter + } +} diff --git a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden index 70ecac4fc62..b8d9d7c743f 100644 --- a/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden +++ b/go/analysis/passes/modernize/testdata/src/rangeint/rangeint.go.golden @@ -271,3 +271,18 @@ func issue74687() { } } +func issue76470() { + var i, j int +UsedAfter: + for i = 0; i < 10; i++ { // nope: i is accessed after the loop + break UsedAfter + } + if i == 9 { + panic("Modernizer changes behavior") + } + +NotUsedAfter: + for j = range(10) { // want "for loop can be modernized using range over int" + break NotUsedAfter + } +} From d5d7d21fe76f3bcf697a30ec7f9ce708c125abf2 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Thu, 27 Nov 2025 15:44:02 -0500 Subject: [PATCH 41/60] gopls/internal/cache: fix %q verb use with wrong type Caught early by the improved vet check gated behind the 1.26 language version combined with a tiplang builder that tests with 1.26 language version. Fixes golang/go#76547. Change-Id: Ia23d5872f357e2ce97c22f38bca6114c04a42075 Cq-Include-Trybots: luci.golang.try:x_tools-gotip-linux-amd64-tiplang Reviewed-on: https://go-review.googlesource.com/c/tools/+/724403 Auto-Submit: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov Reviewed-by: Alan Donovan --- gopls/internal/cache/check.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go index dfefd595be3..ba4e785db63 100644 --- a/gopls/internal/cache/check.go +++ b/gopls/internal/cache/check.go @@ -1323,7 +1323,7 @@ func (b *packageHandleBuilder) evaluatePackageHandle(ctx context.Context, n *han dh := b.nodes[id] if dh == nil { // Previous code reported an error (not a bug) here. - bug.Reportf("missing reachable node for %q", id) + bug.Reportf("missing reachable node for %v", id) } else { reachableNodes = append(reachableNodes, dh) } From 4a3f2f81ebf05a0a5c1fc694aac57b6cbfb7870a Mon Sep 17 00:00:00 2001 From: Oliver Eikemeier Date: Fri, 28 Nov 2025 12:58:20 +0000 Subject: [PATCH 42/60] go/analysis/passes/printf: panic when function literal is assigned to the blank identifier the printf analyzer fails on func _() { _ = func() {} } info.ObjectOf returns nil for the blank identifier ('_'), which fails the type assertion to *types.Var. Fixes golang/go#76616 Change-Id: I045c2dd55f92c398614e4e4f90ceeed5718265d2 GitHub-Last-Rev: d8027045c24872975e3921af9b1f2824a0c9c7ec GitHub-Pull-Request: golang/tools#607 Reviewed-on: https://go-review.googlesource.com/c/tools/+/725060 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Dmitri Shuralyov Commit-Queue: Alan Donovan Reviewed-by: Dmitri Shuralyov Auto-Submit: Alan Donovan --- go/analysis/passes/printf/printf.go | 2 +- go/analysis/passes/printf/printf_test.go | 2 +- .../passes/printf/testdata/src/issue76616/issue76616.go | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 go/analysis/passes/printf/testdata/src/issue76616/issue76616.go diff --git a/go/analysis/passes/printf/printf.go b/go/analysis/passes/printf/printf.go index e6088b33024..1afb07c452b 100644 --- a/go/analysis/passes/printf/printf.go +++ b/go/analysis/passes/printf/printf.go @@ -247,7 +247,7 @@ func findPrintLike(pass *analysis.Pass, res *Result) { switch lhs := lhs.(type) { case *ast.Ident: // variable: wrapf = func(...) - v = info.ObjectOf(lhs).(*types.Var) + v, _ = info.ObjectOf(lhs).(*types.Var) case *ast.SelectorExpr: if sel, ok := info.Selections[lhs]; ok { // struct field: x.wrapf = func(...) diff --git a/go/analysis/passes/printf/printf_test.go b/go/analysis/passes/printf/printf_test.go index c4f21edf318..a582474d2e2 100644 --- a/go/analysis/passes/printf/printf_test.go +++ b/go/analysis/passes/printf/printf_test.go @@ -19,7 +19,7 @@ func Test(t *testing.T) { printf.Analyzer.Flags.Set("funcs", "Warn,Warnf") analysistest.Run(t, testdata, printf.Analyzer, - "a", "b", "nofmt", "nonconst", "typeparams", "issue68744", "issue70572", "issue72850") + "a", "b", "nofmt", "nonconst", "typeparams", "issue68744", "issue70572", "issue72850", "issue76616") } func TestNonConstantFmtString_Go123(t *testing.T) { diff --git a/go/analysis/passes/printf/testdata/src/issue76616/issue76616.go b/go/analysis/passes/printf/testdata/src/issue76616/issue76616.go new file mode 100644 index 00000000000..cdf92ee316f --- /dev/null +++ b/go/analysis/passes/printf/testdata/src/issue76616/issue76616.go @@ -0,0 +1,5 @@ +package issue76616 + +func _() { + _ = func() {} +} From 1ad6f3d027132312b80febf5a638d009a98a4cfd Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Sat, 22 Nov 2025 13:57:10 -0500 Subject: [PATCH 43/60] internal/refactor/inline: simplify import handling Previously, the inliner had a very heavyweight API that accepted a whole file []byte as input and produced a whole fixed file []byte as output, and used various whole-file parse and format steps along the way. (Also, the clients still had to compute the diff afterwards!) Not only was this expensive, showing up in profiles (see golang/go#75773), but it was unnecessary: by construction, the only edits due to inlining are at the call site (or some ancestor) and around the import declaration. The new logic reflects this. The new API accepts only the AST, and returns only a list of []refactor.Edit. (refactor.Edit is an alias for analysis.TextEdit. Ideally it would be a distinct isomorphic type without a dependency between the two packages, but slices of different named types are cumbersome to convert between.) The import computation is now much simpler: - Addition uses refactor.AddImportEdits (the bottom half of refactor.AddImport), which does the surgery without the analytical validation. - Import deletion is done by new ad-hoc logic, but only for the packages made redundant from call position (pkg.F(...)), since it is common to replace dir1/pkg by dir2/pkg and we don't want to leave an ambiguous import. For all other packages that are made redundant by the transformation (e.g. mentioned in parameter type decls), we let the client remove them, which is something that the analysis drivers and tests now do, freeing the inliner from having to compute unused imports generally. (gopls' inline-all code action also does it after each step.) Also: - The inliner now rejects all callers in generated files. (Previously it was only from cgo files, which required the whole file content to detect.) - The inconsistent offsets assertion is gone. (It required the whole file content.) - Added test case import-comments2.txtar (from golang/go#67336) to inline/, not just gopls. The new behavior actually fixes comment migration, which was oddly not tested by the CL that fixed that issue, given that it was in the title of the bug. - Fixed a bug in AddImport (re: 'before') that caused it to create a new import decl if the existing one had a comment. - Fixed some @codeaction tests now that we don't gofmt or goimports (=> no import decl merging) - Tweaked got/want/diff section headers in test frameworks for ease of reading. Updates golang/go#67336 Fixes golang/go#76358 Change-Id: I223747aed58b3287df2494df4814b7982ccc9f77 Reviewed-on: https://go-review.googlesource.com/c/tools/+/723781 LUCI-TryBot-Result: Go LUCI Reviewed-by: Robert Findley --- go/analysis/passes/inline/inline.go | 29 +- gopls/internal/golang/inline.go | 4 +- gopls/internal/golang/inline_all.go | 35 +- gopls/internal/test/marker/marker_test.go | 2 +- .../marker/testdata/codeaction/inline.txt | 1 + .../testdata/codeaction/inline_issue67336.txt | 9 +- .../testdata/codeaction/inline_issue68554.txt | 2 - .../testdata/codeaction/inline_resolve.txt | 1 + .../codeaction/removeparam_imports.txt | 6 + internal/analysis/driverutil/fix.go | 7 + internal/refactor/delete.go | 39 +- internal/refactor/edit.go | 15 + internal/refactor/imports.go | 54 ++- internal/refactor/imports_test.go | 7 +- internal/refactor/inline/everything_test.go | 19 +- internal/refactor/inline/inline.go | 354 +++++------------- internal/refactor/inline/inline_test.go | 61 ++- .../refactor/inline/testdata/assignment.txtar | 17 +- internal/refactor/inline/testdata/cgo.txtar | 4 +- .../refactor/inline/testdata/dotimport.txtar | 4 +- .../inline/testdata/import-comments.txtar | 9 +- .../inline/testdata/import-comments2.txtar | 71 ++++ .../import-preserve-local-pkgname.txtar | 7 +- .../inline/testdata/import-rename.txtar | 4 +- .../inline/testdata/import-shadow-1.txtar | 5 +- .../inline/testdata/import-shadow-2.txtar | 11 +- .../inline/testdata/import-shadow.txtar | 3 +- .../refactor/inline/testdata/issue62667.txtar | 4 +- .../refactor/inline/testdata/issue63298.txtar | 7 +- .../inline/testdata/revdotimport.txtar | 7 +- internal/refactor/refactor.go | 3 +- 31 files changed, 393 insertions(+), 408 deletions(-) create mode 100644 internal/refactor/edit.go create mode 100644 internal/refactor/inline/testdata/import-comments2.txtar diff --git a/go/analysis/passes/inline/inline.go b/go/analysis/passes/inline/inline.go index c0b75202589..9049145e225 100644 --- a/go/analysis/passes/inline/inline.go +++ b/go/analysis/passes/inline/inline.go @@ -7,7 +7,6 @@ package inline import ( "fmt" "go/ast" - "go/token" "go/types" "slices" "strings" @@ -23,7 +22,6 @@ import ( "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/diff" "golang.org/x/tools/internal/moreiters" "golang.org/x/tools/internal/packagepath" "golang.org/x/tools/internal/refactor" @@ -204,19 +202,12 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur inspector.Cursor) { var edits []analysis.TextEdit if !lazyEdits { // Inline the call. - content, err := a.readFile(call) - if err != nil { - a.pass.Reportf(call.Lparen, "invalid inlining candidate: cannot read source file: %v", err) - return - } - curFile := astutil.EnclosingFile(cur) caller := &inline.Caller{ - Fset: a.pass.Fset, - Types: a.pass.Pkg, - Info: a.pass.TypesInfo, - File: curFile, - Call: call, - Content: content, + Fset: a.pass.Fset, + Types: a.pass.Pkg, + Info: a.pass.TypesInfo, + File: astutil.EnclosingFile(cur), + Call: call, CountUses: func(pkgname *types.PkgName) int { return moreiters.Len(a.index.Uses(pkgname)) }, @@ -245,15 +236,7 @@ func (a *analyzer) inlineCall(call *ast.CallExpr, cur inspector.Cursor) { // The flag allows them to decline such fixes. return } - got := res.Content - - for _, edit := range diff.Bytes(content, got) { - edits = append(edits, analysis.TextEdit{ - Pos: curFile.FileStart + token.Pos(edit.Start), - End: curFile.FileStart + token.Pos(edit.End), - NewText: []byte(edit.New), - }) - } + edits = res.Edits } a.pass.Report(analysis.Diagnostic{ diff --git a/gopls/internal/golang/inline.go b/gopls/internal/golang/inline.go index e1a3a4331e6..520d1992e37 100644 --- a/gopls/internal/golang/inline.go +++ b/gopls/internal/golang/inline.go @@ -23,7 +23,6 @@ import ( "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/astutil" - "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/refactor/inline" ) @@ -115,7 +114,6 @@ func inlineCall(ctx context.Context, snapshot *cache.Snapshot, callerPkg *cache. Info: callerPkg.TypesInfo(), File: callerPGF.File, Call: call, - Content: callerPGF.Src, CountUses: nil, // (use inefficient default implementation) } @@ -126,7 +124,7 @@ func inlineCall(ctx context.Context, snapshot *cache.Snapshot, callerPkg *cache. return callerPkg.FileSet(), &analysis.SuggestedFix{ Message: fmt.Sprintf("inline call of %v", callee), - TextEdits: diffToTextEdits(callerPGF.Tok, diff.Bytes(callerPGF.Src, res.Content)), + TextEdits: res.Edits, }, nil } diff --git a/gopls/internal/golang/inline_all.go b/gopls/internal/golang/inline_all.go index 73e3504a76e..59109cde3ba 100644 --- a/gopls/internal/golang/inline_all.go +++ b/gopls/internal/golang/inline_all.go @@ -17,6 +17,9 @@ import ( "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/internal/analysis/driverutil" + "golang.org/x/tools/internal/diff" + "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor/inline" ) @@ -229,14 +232,42 @@ func inlineAllCalls(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Pa Info: tinfo, File: file, Call: calls[currentCall], - Content: content, CountUses: nil, // TODO(adonovan): opt: amortize across callInfo.pkg } res, err := inline.Inline(caller, callee, opts) if err != nil { return nil, fmt.Errorf("inlining failed: %v", err) } - content = res.Content + + // applyEdits transforms content by applying the specified edits + // (whose positions are defined by fset), reformatting the file, and + // removing unused imports (using pkg for names of imported packages). + // (Adapted from inline_test.applyEdits.) + applyEdits := func(content []byte, edits []refactor.Edit) ([]byte, error) { + dedits := make([]diff.Edit, len(edits)) + for i, edit := range edits { + dedits[i] = diff.Edit{ + Start: int(edit.Pos - file.FileStart), + End: int(edit.End - file.FileStart), + New: string(edit.NewText), + } + } + got, err := diff.ApplyBytes(content, dedits) + if err != nil { + return nil, err // inliner produced invalid edits + } + got, err = driverutil.FormatSourceRemoveImports(tpkg, got) + if err != nil { + return nil, err // failed to parse+print + } + return []byte(got), nil + } + + content, err = applyEdits(content, res.Edits) + if err != nil { + return nil, fmt.Errorf("applying inliner edits failed: %v", err) + } + if post != nil { content = post(content) } diff --git a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go index 47ca885af38..c5ff7a21a53 100644 --- a/gopls/internal/test/marker/marker_test.go +++ b/gopls/internal/test/marker/marker_test.go @@ -1442,7 +1442,7 @@ func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) { mark.errorf("%s: unexpected change to file %s; got:\n%s", mark.note.Name, filename, got) } else if string(got) != string(want) { - mark.errorf("%s: wrong file content for %s: got:\n%s\nwant:\n%s\ndiff:\n%s", + mark.errorf("%s: wrong file content for %s:\n-- got --\n%s\n-- want --\n%s\n-- diff --\n%s", mark.note.Name, filename, got, want, compare.Bytes(want, got)) } diff --git a/gopls/internal/test/marker/testdata/codeaction/inline.txt b/gopls/internal/test/marker/testdata/codeaction/inline.txt index 1871a303d2b..645a21e1b4a 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline.txt @@ -32,3 +32,4 @@ func _() { } func add(x, y int) int { return x + y } + diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_issue67336.txt b/gopls/internal/test/marker/testdata/codeaction/inline_issue67336.txt index b2bffc105f4..6cd5f6ef70f 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline_issue67336.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline_issue67336.txt @@ -1,6 +1,8 @@ This is the test case from golang/go#67335, where the inlining resulted in bad formatting. +The __unused__ import results in removal after a subsequent "organize imports" step. + -- go.mod -- module example.com @@ -51,12 +53,12 @@ func _() { -- @inline/b/c/foo.go -- package c - import ( "context" - "example.com/define/my/typ" + "example.com/one/more/pkg" pkg0 "example.com/some/other/pkg" + "example.com/define/my/typ" ) const ( @@ -66,6 +68,7 @@ const ( func _() { var _ context.Context = context.TODO() - pkg0.Foo(typ.T(5)) //@ codeaction("Baz", "refactor.inline.call", result=inline) +pkg0.Foo(typ.T(5)) //@ codeaction("Baz", "refactor.inline.call", result=inline) pkg.Bar() } + diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_issue68554.txt b/gopls/internal/test/marker/testdata/codeaction/inline_issue68554.txt index 7ad9f0c1e65..7a123cb57bf 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline_issue68554.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline_issue68554.txt @@ -32,7 +32,5 @@ func _(d discard) { func g(w io.Writer) { fmt.Println(w) } var _ discard - type discard struct{} - func (discard) Write(p []byte) (int, error) { return len(p), nil } diff --git a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt index cf311838706..8abed66d579 100644 --- a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt +++ b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt @@ -22,3 +22,4 @@ func _() { } func add(x, y int) int { return x + y } + diff --git a/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt index cd5f910a70d..ad6aec4dfc1 100644 --- a/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt +++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt @@ -1,6 +1,9 @@ This test checks the behavior of removing a parameter with respect to various import scenarios. +Wholly unused imports are expected to be removed by a subsequent +"organize imports" step. + -- go.mod -- module mod.test @@ -86,6 +89,8 @@ func _() { -- @b/a/a1.go -- package a +import "mod.test/c" + import "mod.test/b" func _() { @@ -99,6 +104,7 @@ package a import ( "mod.test/b" . "mod.test/b" + "mod.test/c" ) func _() { diff --git a/internal/analysis/driverutil/fix.go b/internal/analysis/driverutil/fix.go index ef06cf9bde2..37b09588a7a 100644 --- a/internal/analysis/driverutil/fix.go +++ b/internal/analysis/driverutil/fix.go @@ -339,6 +339,9 @@ fixloop: // information for the fixed file and thus cannot accurately tell // whether k is among the free names of T{k: 0}, which requires // knowledge of whether T is a struct type. +// +// Like [imports.Process] (the core of x/tools/cmd/goimports), it also +// merges import decls. func FormatSourceRemoveImports(pkg *types.Package, src []byte) ([]byte, error) { // This function was reduced from the "strict entire file" // path through [format.Source]. @@ -353,6 +356,10 @@ func FormatSourceRemoveImports(pkg *types.Package, src []byte) ([]byte, error) { removeUnneededImports(fset, pkg, file) + // TODO(adonovan): to generate cleaner edits when adding an import, + // consider adding a call to imports.mergeImports; however, it does + // cause comments to migrate. + // printerNormalizeNumbers means to canonicalize number literal prefixes // and exponents while printing. See https://golang.org/doc/go1.13#gofmt. // diff --git a/internal/refactor/delete.go b/internal/refactor/delete.go index 9b96b1dbf1f..54d0b5f0386 100644 --- a/internal/refactor/delete.go +++ b/internal/refactor/delete.go @@ -13,7 +13,6 @@ import ( "go/types" "slices" - "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/edge" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/astutil" @@ -32,7 +31,7 @@ import ( // // If it cannot make the necessary edits, such as for a function // parameter or result, it returns nil. -func DeleteVar(tokFile *token.File, info *types.Info, curId inspector.Cursor) []analysis.TextEdit { +func DeleteVar(tokFile *token.File, info *types.Info, curId inspector.Cursor) []Edit { switch ek, _ := curId.ParentEdge(); ek { case edge.ValueSpec_Names: return deleteVarFromValueSpec(tokFile, info, curId) @@ -52,7 +51,7 @@ func DeleteVar(tokFile *token.File, info *types.Info, curId inspector.Cursor) [] // Precondition: curId is Ident beneath ValueSpec.Names beneath GenDecl. // // See also [deleteVarFromAssignStmt], which has parallel structure. -func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []analysis.TextEdit { +func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []Edit { var ( id = curIdent.Node().(*ast.Ident) curSpec = curIdent.Parent() @@ -95,7 +94,7 @@ func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent insp pos = spec.Names[index].Pos() end = spec.Names[index+1].Pos() } - return []analysis.TextEdit{{ + return []Edit{{ Pos: pos, End: end, }} @@ -111,7 +110,7 @@ func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent insp // // var _, lhs1 = rhs0, rhs1 // ------ ------ - return []analysis.TextEdit{ + return []Edit{ { Pos: spec.Names[index-1].End(), End: spec.Names[index].End(), @@ -126,7 +125,7 @@ func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent insp // // var lhs0, _ = rhs0, rhs1 // ------ ------ - return []analysis.TextEdit{ + return []Edit{ { Pos: spec.Names[index].Pos(), End: spec.Names[index+1].Pos(), @@ -141,7 +140,7 @@ func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent insp // We cannot delete the RHS. // Blank out the LHS. - return []analysis.TextEdit{{ + return []Edit{{ Pos: id.Pos(), End: id.End(), NewText: []byte("_"), @@ -151,7 +150,7 @@ func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent insp // Precondition: curId is Ident beneath AssignStmt.Lhs. // // See also [deleteVarFromValueSpec], which has parallel structure. -func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []analysis.TextEdit { +func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []Edit { var ( id = curIdent.Node().(*ast.Ident) curStmt = curIdent.Parent() @@ -192,7 +191,7 @@ func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent ins // // _, lhs1 := rhs0, rhs1 // ------ ------ - return []analysis.TextEdit{ + return []Edit{ { Pos: assign.Lhs[index-1].End(), End: assign.Lhs[index].End(), @@ -207,7 +206,7 @@ func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent ins // // lhs0, _ := rhs0, rhs1 // ------ ------ - return []analysis.TextEdit{ + return []Edit{ { Pos: assign.Lhs[index].Pos(), End: assign.Lhs[index+1].Pos(), @@ -222,7 +221,7 @@ func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent ins // We cannot delete the RHS. // Blank out the LHS. - edits := []analysis.TextEdit{{ + edits := []Edit{{ Pos: id.Pos(), End: id.End(), NewText: []byte("_"), @@ -233,7 +232,7 @@ func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent ins // assignment to avoid a "no new variables on left // side of :=" error. if !declaresOtherNames { - edits = append(edits, analysis.TextEdit{ + edits = append(edits, Edit{ Pos: assign.TokPos, End: assign.TokPos + token.Pos(len(":=")), NewText: []byte("="), @@ -246,7 +245,7 @@ func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent ins // DeleteSpec returns edits to delete the {Type,Value}Spec identified by curSpec. // // TODO(adonovan): add test suite. Test for consts as well. -func DeleteSpec(tokFile *token.File, curSpec inspector.Cursor) []analysis.TextEdit { +func DeleteSpec(tokFile *token.File, curSpec inspector.Cursor) []Edit { var ( spec = curSpec.Node().(ast.Spec) curDecl = curSpec.Parent() @@ -277,7 +276,7 @@ func DeleteSpec(tokFile *token.File, curSpec inspector.Cursor) []analysis.TextEd // ----- end = decl.Specs[index+1].Pos() } - return []analysis.TextEdit{{ + return []Edit{{ Pos: pos, End: end, }} @@ -286,7 +285,7 @@ func DeleteSpec(tokFile *token.File, curSpec inspector.Cursor) []analysis.TextEd // DeleteDecl returns edits to delete the ast.Decl identified by curDecl. // // TODO(adonovan): add test suite. -func DeleteDecl(tokFile *token.File, curDecl inspector.Cursor) []analysis.TextEdit { +func DeleteDecl(tokFile *token.File, curDecl inspector.Cursor) []Edit { decl := curDecl.Node().(ast.Decl) ek, _ := curDecl.ParentEdge() @@ -321,7 +320,7 @@ func DeleteDecl(tokFile *token.File, curDecl inspector.Cursor) []analysis.TextEd } } - return []analysis.TextEdit{{ + return []Edit{{ Pos: pos, End: end, }} @@ -366,7 +365,7 @@ func filterPos(nds []*ast.Comment, start, end token.Pos) (token.Pos, token.Pos, // it removes whole lines like // // stmt // comment -func DeleteStmt(file *token.File, curStmt inspector.Cursor) []analysis.TextEdit { +func DeleteStmt(file *token.File, curStmt inspector.Cursor) []Edit { // if the stmt is on a line by itself, or a range of lines, delete the whole thing // including comments. Except for the heads of switches, type // switches, and for-statements that's the usual case. Complexity occurs where @@ -516,13 +515,13 @@ Big: } } - return []analysis.TextEdit{{Pos: leftEdit, End: rightEdit}} + return []Edit{{Pos: leftEdit, End: rightEdit}} } // DeleteUnusedVars computes the edits required to delete the // declarations of any local variables whose last uses are in the // curDelend subtree, which is about to be deleted. -func DeleteUnusedVars(index *typeindex.Index, info *types.Info, tokFile *token.File, curDelend inspector.Cursor) []analysis.TextEdit { +func DeleteUnusedVars(index *typeindex.Index, info *types.Info, tokFile *token.File, curDelend inspector.Cursor) []Edit { // TODO(adonovan): we might want to generalize this by // splitting the two phases below, so that we can gather // across a whole sequence of deletions then finally compute the @@ -539,7 +538,7 @@ func DeleteUnusedVars(index *typeindex.Index, info *types.Info, tokFile *token.F } // Delete declaration of each var that became unused. - var edits []analysis.TextEdit + var edits []Edit for v, count := range delcount { if len(slices.Collect(index.Uses(v))) == count { if curDefId, ok := index.Def(v); ok { diff --git a/internal/refactor/edit.go b/internal/refactor/edit.go new file mode 100644 index 00000000000..42be9a54b41 --- /dev/null +++ b/internal/refactor/edit.go @@ -0,0 +1,15 @@ +// 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.p + +package refactor + +// This is the only file in this package that should import analysis. +// +// TODO(adonovan): consider unaliasing the type to break the +// dependency. (The ergonomics of slice append are unfortunate.) + +import "golang.org/x/tools/go/analysis" + +// An Edit describes a deletion and/or an insertion. +type Edit = analysis.TextEdit diff --git a/internal/refactor/imports.go b/internal/refactor/imports.go index b5440d896b9..e1860ab0659 100644 --- a/internal/refactor/imports.go +++ b/internal/refactor/imports.go @@ -7,13 +7,12 @@ package refactor // This file defines operations for computing edits to imports. import ( - "fmt" "go/ast" "go/token" "go/types" pathpkg "path" + "strconv" - "golang.org/x/tools/go/analysis" "golang.org/x/tools/internal/packagepath" ) @@ -35,7 +34,7 @@ import ( // package declares member. // // AddImport does not mutate its arguments. -func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member string, pos token.Pos) (prefix string, edits []analysis.TextEdit) { +func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member string, pos token.Pos) (prefix string, edits []Edit) { // Find innermost enclosing lexical block. scope := info.Scopes[file].Innermost(pos) if scope == nil { @@ -69,33 +68,53 @@ func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member newName := preferredName if preferredName != "_" { newName = FreshName(scope, pos, preferredName) + prefix = newName + "." } - // 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. - // // Use a renaming import whenever the preferred name is not // available, or the chosen name does not match the last // segment of its path. - newText := fmt.Sprintf("%q", pkgpath) - if newName != preferredName || newName != pathpkg.Base(pkgpath) { - newText = fmt.Sprintf("%s %q", newName, pkgpath) + if newName == preferredName && newName == pathpkg.Base(pkgpath) { + newName = "" + } + + return prefix, AddImportEdits(file, newName, pkgpath) +} + +// AddImportEdits returns the edits to add an import of the specified +// package, without any analysis of whether this is necessary or safe. +// If name is nonempty, it is used as an explicit [ImportSpec.Name]. +// +// A sequence of calls to AddImportEdits that each add the file's +// first import (or in a file that does not have a grouped import) may +// result in multiple import declarations, rather than a single one +// with multiple ImportSpecs. However, a subsequent run of +// x/tools/cmd/goimports ([imports.Process]) will combine them. +// +// AddImportEdits does not mutate the AST. +func AddImportEdits(file *ast.File, name, pkgpath string) []Edit { + newText := strconv.Quote(pkgpath) + if name != "" { + newText = name + " " + newText } + // 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] - var before ast.Node = decl0 + before := decl0.Pos() switch decl0 := decl0.(type) { case *ast.GenDecl: if decl0.Doc != nil { - before = decl0.Doc + before = decl0.Doc.Pos() } case *ast.FuncDecl: if decl0.Doc != nil { - before = decl0.Doc + before = decl0.Doc.Pos() } } - if gd, ok := before.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() { + var pos token.Pos + if gd, ok := decl0.(*ast.GenDecl); ok && gd.Tok == token.IMPORT && gd.Rparen.IsValid() { // Have existing grouped import ( ... ) decl. if packagepath.IsStdPackage(pkgpath) && len(gd.Specs) > 0 { // Add spec for a std package before @@ -116,10 +135,13 @@ func AddImport(info *types.Info, file *ast.File, preferredName, pkgpath, member // No import decl, or non-grouped import. // Add a new import decl before first decl. // (gofmt will merge multiple import decls.) - pos = before.Pos() + // + // TODO(adonovan): do better here; plunder the + // mergeImports logic from [imports.Process]. + pos = before newText = "import " + newText + "\n\n" } - return newName + ".", []analysis.TextEdit{{ + return []Edit{{ Pos: pos, End: pos, NewText: []byte(newText), diff --git a/internal/refactor/imports_test.go b/internal/refactor/imports_test.go index 072a9a95549..56e34de4d81 100644 --- a/internal/refactor/imports_test.go +++ b/internal/refactor/imports_test.go @@ -16,7 +16,6 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "golang.org/x/tools/go/analysis" "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/testenv" ) @@ -154,7 +153,7 @@ func _(fmt fmt.Stringer) { src: `package a // hello -import () +import "os" // world func _() { @@ -165,7 +164,7 @@ func _() { import "fmt" // hello -import () +import "os" // world func _() { @@ -360,7 +359,7 @@ func _(io.Reader) { prefix, edits := refactor.AddImport(info, f, name, path, member, pos) - var edit analysis.TextEdit + var edit refactor.Edit switch len(edits) { case 0: case 1: diff --git a/internal/refactor/inline/everything_test.go b/internal/refactor/inline/everything_test.go index a32e0709be1..aecaa7da54c 100644 --- a/internal/refactor/inline/everything_test.go +++ b/internal/refactor/inline/everything_test.go @@ -115,12 +115,11 @@ func TestEverything(t *testing.T) { t.Fatal(err) } caller := &inline.Caller{ - Fset: callerPkg.Fset, - Types: callerPkg.Types, - Info: callerPkg.TypesInfo, - File: callerFile, - Call: call, - Content: callerContent, + Fset: callerPkg.Fset, + Types: callerPkg.Types, + Info: callerPkg.TypesInfo, + File: callerFile, + Call: call, } // Analyze callee. @@ -182,11 +181,15 @@ func TestEverything(t *testing.T) { t.Log(err) return } - got := res.Content + + got, err := applyEdits(caller.Types, caller.File.FileStart, callerContent, res.Edits) + if err != nil { + t.Fatalf("can't apply inliner edits: %v", err) + } // Print the diff. t.Logf("Got diff:\n%s", - diff.Unified("old", "new", string(callerContent), string(res.Content))) + diff.Unified("old", "new", string(callerContent), string(got))) // Parse and type-check the transformed source. f, err := parser.ParseFile(caller.Fset, callPosn.Filename, got, parser.SkipObjectResolution) diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go index ae8e5b850d0..f7e37fd7da8 100644 --- a/internal/refactor/inline/inline.go +++ b/internal/refactor/inline/inline.go @@ -11,14 +11,12 @@ import ( "go/constant" "go/format" "go/parser" - "go/printer" "go/token" "go/types" "maps" pathpkg "path" "reflect" "slices" - "strconv" "strings" "golang.org/x/tools/go/ast/astutil" @@ -26,6 +24,7 @@ import ( internalastutil "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/astutil/free" "golang.org/x/tools/internal/packagepath" + "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/typeparams" "golang.org/x/tools/internal/typesinternal" "golang.org/x/tools/internal/versions" @@ -35,12 +34,11 @@ import ( // // The client is responsible for populating this struct and passing it to Inline. type Caller struct { - Fset *token.FileSet - Types *types.Package - Info *types.Info - File *ast.File - Call *ast.CallExpr - Content []byte // source of file containing (TODO(adonovan): see comment at Result.Content) + Fset *token.FileSet + Types *types.Package + Info *types.Info + File *ast.File + Call *ast.CallExpr // CountUses is an optional optimized computation of // the number of times pkgname appears in Info.Uses. @@ -61,26 +59,9 @@ type Options struct { // Result holds the result of code transformation. type Result struct { - // TODO(adonovan): the only textual results that should be - // needed are (1) an edit in the vicinity of the call (either - // to the CallExpr or one of its ancestors), and optionally - // (2) an edit to the import declaration. - // Change the inliner API to return a list of edits, - // and not to accept a Caller.Content, as it is only - // temptation to use such algorithmically expensive - // operations as reformatting the entire file, which is - // a significant source of non-linear dynamic behavior; - // see https://go.dev/issue/75773. - // This will require a sequence of changes to the tests - // and the inliner algorithm itself. - Content []byte // formatted, transformed content of caller file - Literalized bool // chosen strategy replaced callee() with func(){...}() - BindingDecl bool // transformation added "var params = args" declaration - - // TODO(adonovan): provide an API for clients that want structured - // output: a list of import additions and deletions plus one or more - // localized diffs (or even AST transformations, though ownership and - // mutation are tricky) near the call site. + Edits []refactor.Edit // edits around CallExpr and imports + Literalized bool // chosen strategy replaced callee() with func(){...}() + BindingDecl bool // transformation added "var params = args" declaration } // Inline inlines the called function (callee) into the function call (caller) @@ -117,14 +98,8 @@ func (st *state) inline() (*Result, error) { debugFormatNode(caller.Fset, caller.Call), caller.Fset.PositionFor(caller.Call.Lparen, false)) - if !consistentOffsets(caller) { - return nil, fmt.Errorf("internal error: caller syntax positions are inconsistent with file content (did you forget to use FileSet.PositionFor when computing the file name?)") - } - - // Break the string literal so we can use inlining in this file. :) - if ast.IsGenerated(caller.File) && - bytes.Contains(caller.Content, []byte("// Code generated by "+"cmd/cgo; DO NOT EDIT.")) { - return nil, fmt.Errorf("cannot inline calls from files that import \"C\"") + if ast.IsGenerated(caller.File) { + return nil, fmt.Errorf("cannot inline calls from generated files") } res, err := st.inlineCall() @@ -224,37 +199,10 @@ func (st *state) inline() (*Result, error) { } } - // File rewriting. This proceeds in multiple passes, in order to maximally - // preserve comment positioning. (This could be greatly simplified once - // comments are stored in the tree.) - // - // Don't call replaceNode(caller.File, res.old, res.new) - // as it mutates the caller's syntax tree. - // Instead, splice the file, replacing the extent of the "old" - // node by a formatting of the "new" node, and re-parse. - // We'll fix up the imports on this new tree, and format again. - // - // Inv: f is the result of parsing content, using fset. - var ( - content = caller.Content - fset = caller.Fset - f *ast.File // parsed below - ) - reparse := func() error { - const mode = parser.ParseComments | parser.SkipObjectResolution | parser.AllErrors - f, err = parser.ParseFile(fset, "callee.go", content, mode) - if err != nil { - // Something has gone very wrong. - logf("failed to reparse <<%s>>: %v", string(content), err) // debugging - return err - } - return nil - } + var edits []refactor.Edit + + // Format the cloned callee. { - start := offsetOf(fset, res.old.Pos()) - end := offsetOf(fset, res.old.End()) - var out bytes.Buffer - out.Write(content[:start]) // TODO(adonovan): might it make more sense to use // callee.Fset when formatting res.new? // The new tree is a mix of (cloned) caller nodes for @@ -269,148 +217,106 @@ func (st *state) inline() (*Result, error) { // Precise comment handling would make this a // non-issue. Formatting wouldn't really need a // FileSet at all. + + var out bytes.Buffer if elideBraces { for i, stmt := range res.new.(*ast.BlockStmt).List { if i > 0 { out.WriteByte('\n') } - if err := format.Node(&out, fset, stmt); err != nil { + if err := format.Node(&out, caller.Fset, stmt); err != nil { return nil, err } } } else { - if err := format.Node(&out, fset, res.new); err != nil { + if err := format.Node(&out, caller.Fset, res.new); err != nil { return nil, err } } - out.Write(content[end:]) - content = out.Bytes() - if err := reparse(); err != nil { - return nil, err - } - } - // Add new imports that are still used. - newImports := trimNewImports(res.newImports, res.new) - // Insert new imports after last existing import, - // to avoid migration of pre-import comments. - // The imports will be organized below. - if len(newImports) > 0 { - // If we have imports to add, do so independent of the rest of the file. - // Otherwise, the length of the new imports may consume floating comments, - // causing them to be printed inside the imports block. - var ( - importDecl *ast.GenDecl - comments []*ast.CommentGroup // relevant comments. - before, after []byte // pre- and post-amble for the imports block. - ) - if len(f.Imports) > 0 { - // Append specs to existing import decl - importDecl = f.Decls[0].(*ast.GenDecl) - for _, comment := range f.Comments { - // Filter comments. Don't use CommentMap.Filter here, because we don't - // want to include comments that document the import decl itself, for - // example: - // - // // We don't want this comment to be duplicated. - // import ( - // "something" - // ) - if importDecl.Pos() <= comment.Pos() && comment.Pos() < importDecl.End() { - comments = append(comments, comment) - } - } - before = content[:offsetOf(fset, importDecl.Pos())] - importDecl.Doc = nil // present in before - after = content[offsetOf(fset, importDecl.End()):] - } else { - // Insert new import decl. - importDecl = &ast.GenDecl{Tok: token.IMPORT} - f.Decls = prepend[ast.Decl](importDecl, f.Decls...) - - // Make room for the new declaration after the package declaration. - pkgEnd := f.Name.End() - file := fset.File(pkgEnd) - if file == nil { - logf("internal error: missing pkg file") - return nil, fmt.Errorf("missing pkg file for %s", f.Name.Name) - } - // Preserve any comments after the package declaration, by splicing in - // the new import block after the end of the package declaration line. - line := file.Line(pkgEnd) - if line < len(file.Lines()) { // line numbers are 1-based - nextLinePos := file.LineStart(line + 1) - nextLine := offsetOf(fset, nextLinePos) - before = slices.Concat(content[:nextLine], []byte("\n")) - after = slices.Concat([]byte("\n\n"), content[nextLine:]) - } else { - before = slices.Concat(content, []byte("\n\n")) - } - } - // Add new imports. - // Set their position to after the last position of the old imports, to keep - // comments on the old imports from moving. - lastPos := token.NoPos - if lastSpec := last(importDecl.Specs); lastSpec != nil { - lastPos = lastSpec.Pos() - if c := lastSpec.(*ast.ImportSpec).Comment; c != nil { - lastPos = c.Pos() - } - } - for _, imp := range newImports { - // Check that the new imports are accessible. - path, _ := strconv.Unquote(imp.spec.Path.Value) - if !packagepath.CanImport(caller.Types.Path(), path) { - return nil, fmt.Errorf("can't inline function %v as its body refers to inaccessible package %q", callee, path) - } - if lastPos.IsValid() { - lastPos++ - imp.spec.Path.ValuePos = lastPos - } - importDecl.Specs = append(importDecl.Specs, imp.spec) - } + edits = append(edits, refactor.Edit{ + Pos: res.old.Pos(), + End: res.old.End(), + NewText: out.Bytes(), + }) + } - var out bytes.Buffer - out.Write(before) - commented := &printer.CommentedNode{ - Node: importDecl, - Comments: comments, + // Add new imports. + // + // It's possible that not all are needed (e.g. for type names + // that melted away), but we'll let the client (such as an + // analysis driver) clean it up since it must remove unused + // imports anyway. + for _, imp := range res.newImports { + // Check that the new imports are accessible. + if !packagepath.CanImport(caller.Types.Path(), imp.path) { + return nil, fmt.Errorf("can't inline function %v as its body refers to inaccessible package %q", callee, imp.path) } - if err := format.Node(&out, fset, commented); err != nil { - logf("failed to format new importDecl: %v", err) // debugging - return nil, err - } - out.Write(after) - content = out.Bytes() - if err := reparse(); err != nil { - return nil, err - } - } - // Delete imports referenced only by caller.Call.Fun. - for _, oldImport := range res.oldImports { - specToDelete := oldImport.spec + // We've already validated the import, so we call + // AddImportEdits directly to compute the edit. name := "" - if specToDelete.Name != nil { - name = specToDelete.Name.Name + if imp.explicit { + name = imp.name } - path, _ := strconv.Unquote(specToDelete.Path.Value) - astutil.DeleteNamedImport(caller.Fset, f, name, path) - } - - var out bytes.Buffer - if err := format.Node(&out, caller.Fset, f); err != nil { - return nil, err + edits = append(edits, refactor.AddImportEdits(caller.File, name, imp.path)...) } - newSrc := out.Bytes() literalized := false if call, ok := res.new.(*ast.CallExpr); ok && is[*ast.FuncLit](call.Fun) { literalized = true } + // Delete imports referenced only by caller.Call.Fun. + // + // It's ambiguous to let the client (e.g. analysis driver) + // remove unneeded imports in this case because it is common + // to inlining a call from "dir1/a".F to "dir2/a".F, which + // leaves two imports of packages named 'a', both providing a.F. + // + // However, the only two import deletion tools at our disposal + // are astutil.DeleteNamedImport, which mutates the AST, and + // refactor.Delete{Spec,Decl}, which need a Cursor. So we need + // to reinvent the wheel here. + for _, oldImport := range res.oldImports { + spec := oldImport.spec + + // Include adjacent comments. + pos := spec.Pos() + if doc := spec.Doc; doc != nil { + pos = doc.Pos() + } + end := spec.End() + if doc := spec.Comment; doc != nil { + end = doc.End() + } + + // Find the enclosing import decl. + // If it's paren-less, we must delete it too. + for _, decl := range caller.File.Decls { + decl, ok := decl.(*ast.GenDecl) + if !(ok && decl.Tok == token.IMPORT) { + break // stop at first non-import decl + } + if internalastutil.NodeContainsPos(decl, spec.Pos()) && !decl.Rparen.IsValid() { + // Include adjacent comments. + pos = decl.Pos() + if doc := decl.Doc; doc != nil { + pos = doc.Pos() + } + end = decl.End() + break + } + } + + edits = append(edits, refactor.Edit{ + Pos: pos, + End: end, + }) + } + return &Result{ - Content: newSrc, + Edits: edits, Literalized: literalized, BindingDecl: res.bindingDecl, }, nil @@ -424,8 +330,9 @@ type oldImport struct { // A newImport is an import that will be added to the caller file. type newImport struct { - pkgName string - spec *ast.ImportSpec + name string + path string + explicit bool // use name as ImportSpec.Name } // importState tracks information about imports. @@ -531,7 +438,7 @@ func (i *importState) importName(pkgPath string, shadow shadowMap) string { // based on the package name. func (i *importState) findNewLocalName(pkgName, calleePkgName string, shadow shadowMap) string { newlyAdded := func(name string) bool { - return slices.ContainsFunc(i.newImports, func(n newImport) bool { return n.pkgName == name }) + return slices.ContainsFunc(i.newImports, func(n newImport) bool { return n.name == name }) } // shadowedInCaller reports whether a candidate package name @@ -576,61 +483,17 @@ func (i *importState) localName(pkgPath, pkgName, calleePkgName string, shadow s name := i.findNewLocalName(pkgName, calleePkgName, shadow) i.logf("adding import %s %q", name, pkgPath) - spec := &ast.ImportSpec{ - Path: &ast.BasicLit{ - Kind: token.STRING, - Value: strconv.Quote(pkgPath), - }, - } // Use explicit pkgname (out of necessity) when it differs from the declared name, // or (for good style) when it differs from base(pkgpath). - if name != pkgName || name != pathpkg.Base(pkgPath) { - spec.Name = makeIdent(name) - } i.newImports = append(i.newImports, newImport{ - pkgName: name, - spec: spec, + name: name, + path: pkgPath, + explicit: name != pkgName || name != pathpkg.Base(pkgPath), }) i.importMap[pkgPath] = append(i.importMap[pkgPath], name) return name } -// trimNewImports removes imports that are no longer needed. -// -// The list of new imports as constructed by calls to [importState.localName] -// includes all of the packages referenced by the callee. -// But in the process of inlining, we may have dropped some of those references. -// For example, if the callee looked like this: -// -// func F(x int) (p.T) {... /* no mention of p */ ...} -// -// and we inlined by assignment: -// -// v := ... -// -// then the reference to package p drops away. -// -// Remove the excess imports by seeing which remain in new, the expression -// to be inlined. -// We can find those by looking at the free names in new. -// The list of free names cannot include spurious package names. -// Free-name tracking is precise except for the case of an identifier -// key in a composite literal, which names either a field or a value. -// Neither fields nor values are package names. -// Since they are not relevant to removing unused imports, we instruct -// freeishNames to omit composite-literal keys that are identifiers. -func trimNewImports(newImports []newImport, new ast.Node) []newImport { - const omitComplitIdents = false - free := free.Names(new, omitComplitIdents) - var res []newImport - for _, ni := range newImports { - if free[ni.pkgName] { - res = append(res, ni) - } - } - return res -} - type inlineCallResult struct { newImports []newImport // to add oldImports []oldImport // to remove @@ -665,14 +528,6 @@ type inlineCallResult struct { // allows inlining a statement list. However, due to loss of comments, more // sophisticated rewrites are challenging. // -// TODO(adonovan): in earlier drafts, the transformation was expressed -// by splicing substrings of the two source files because syntax -// trees don't preserve comments faithfully (see #20744), but such -// transformations don't compose. The current implementation is -// tree-based but is very lossy wrt comments. It would make a good -// candidate for evaluating an alternative fully self-contained tree -// representation, such as any proposed solution to #20744, or even -// dst or some private fork of go/ast.) // TODO(rfindley): see if we can reduce the amount of comment lossiness by // using printer.CommentedNode, which has been useful elsewhere. // @@ -3282,25 +3137,6 @@ func last[T any](slice []T) T { return *new(T) } -// consistentOffsets reports whether the portion of caller.Content -// that corresponds to caller.Call can be parsed as a call expression. -// If not, the client has provided inconsistent information, possibly -// because they forgot to ignore line directives when computing the -// filename enclosing the call. -// This is just a heuristic. -func consistentOffsets(caller *Caller) bool { - start := offsetOf(caller.Fset, caller.Call.Pos()) - end := offsetOf(caller.Fset, caller.Call.End()) - if !(0 < start && start < end && end <= len(caller.Content)) { - return false - } - expr, err := parser.ParseExpr(string(caller.Content[start:end])) - if err != nil { - return false - } - return is[*ast.CallExpr](expr) -} - // needsParens reports whether parens are required to avoid ambiguity // around the new node replacing the specified old node (which is some // ancestor of the CallExpr identified by its PathEnclosingInterval). diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 31e20af32f3..797db4ac41f 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -25,8 +25,10 @@ import ( "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/packages" "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/internal/analysis/driverutil" "golang.org/x/tools/internal/diff" "golang.org/x/tools/internal/expect" + "golang.org/x/tools/internal/refactor" "golang.org/x/tools/internal/refactor/inline" "golang.org/x/tools/internal/testenv" "golang.org/x/tools/internal/testfiles" @@ -101,7 +103,7 @@ func TestData(t *testing.T) { // Report parse/type errors; they may be benign. packages.Visit(pkgs, nil, func(pkg *packages.Package) { for _, err := range pkg.Errors { - t.Log(err) + t.Logf("warning: %v", err) } }) @@ -212,12 +214,11 @@ func doInlineNote(logf func(string, ...any), pkg *packages.Package, file *ast.Fi for _, n := range path { if call, ok := n.(*ast.CallExpr); ok { caller = &inline.Caller{ - Fset: pkg.Fset, - Types: pkg.Types, - Info: pkg.TypesInfo, - File: file, - Call: call, - Content: content, + Fset: pkg.Fset, + Types: pkg.Types, + Info: pkg.TypesInfo, + File: file, + Call: call, } break } @@ -305,7 +306,10 @@ func doInlineNote(logf func(string, ...any), pkg *packages.Package, file *ast.Fi } // Inline succeeded. - got := res.Content + got, err := applyEdits(caller.Types, caller.File.FileStart, content, res.Edits) + if err != nil { + return err + } if want, ok := want.([]byte); ok { got = append(bytes.TrimSpace(got), '\n') want = append(bytes.TrimSpace(want), '\n') @@ -318,7 +322,7 @@ func doInlineNote(logf func(string, ...any), pkg *packages.Package, file *ast.Fi } } else { if diff := diff.Unified("want", "got", string(want), string(got)); diff != "" { - return fmt.Errorf("Inline returned wrong output:\n%s\nWant:\n%s\nDiff:\n%s", + return fmt.Errorf("Inline returned wrong output:\n-- got --\n%s\n-- want --\n%s\n-- diff --\n%s", got, want, diff) } } @@ -1903,12 +1907,11 @@ func runTests(t *testing.T, tests []testcase) { } caller := &inline.Caller{ - Fset: fset, - Types: pkg, - Info: info, - File: callerFile, - Call: call, - Content: []byte(callerContent), + Fset: fset, + Types: pkg, + Info: info, + File: callerFile, + Call: call, } check := checkNoMutation(caller.File) defer check() @@ -1938,7 +1941,10 @@ func runTests(t *testing.T, tests []testcase) { t.Fatal(err) } - gotContent := res.Content + gotContent, err := applyEdits(pkg, callerFile.FileStart, []byte(callerContent), res.Edits) + if err != nil { + t.Fatal(err) + } // Compute a single-hunk line-based diff. srcLines := strings.Split(callerContent, "\n") @@ -2108,3 +2114,26 @@ func TestDeepHash(t *testing.T) { t.Fatal("bad") } } + +// applyEdits transforms content by applying the specified edits +// to the file whose positions are defined by fset), reformatting the file, and +// removing unused imports (using pkg for names of imported packages). +func applyEdits(pkg *types.Package, fileStart token.Pos, content []byte, edits []refactor.Edit) ([]byte, error) { + dedits := make([]diff.Edit, len(edits)) + for i, edit := range edits { + dedits[i] = diff.Edit{ + Start: int(edit.Pos - fileStart), + End: int(edit.End - fileStart), + New: string(edit.NewText), + } + } + got, err := diff.ApplyBytes(content, dedits) + if err != nil { + return nil, err + } + got, err = driverutil.FormatSourceRemoveImports(pkg, got) + if err != nil { + return nil, err + } + return []byte(got), nil +} diff --git a/internal/refactor/inline/testdata/assignment.txtar b/internal/refactor/inline/testdata/assignment.txtar index 6e7ffc5f2b7..f2b1c4e0846 100644 --- a/internal/refactor/inline/testdata/assignment.txtar +++ b/internal/refactor/inline/testdata/assignment.txtar @@ -91,9 +91,7 @@ type C int -- b1 -- package a -import ( - "testdata/c" -) +import "testdata/c" func _() { var y int @@ -103,6 +101,8 @@ func _() { -- b2 -- package a +import "testdata/c" + import "testdata/b" func _() { @@ -113,9 +113,7 @@ func _() { -- b3 -- package a -import ( - "testdata/c" -) +import "testdata/c" func _() { x, y := c.C(0), c.C(1) //@ inline(re"B", b3) @@ -125,10 +123,9 @@ func _() { -- b4 -- package a -import ( - c1 "testdata/c" - c2 "testdata/c2" -) +import c1 "testdata/c" + +import c2 "testdata/c2" func _() { x, y := c1.C(0), c2.C(1) //@ inline(re"B", b4) diff --git a/internal/refactor/inline/testdata/cgo.txtar b/internal/refactor/inline/testdata/cgo.txtar index 41567ed7cbb..2a93fa2b415 100644 --- a/internal/refactor/inline/testdata/cgo.txtar +++ b/internal/refactor/inline/testdata/cgo.txtar @@ -1,5 +1,5 @@ Test that attempts to inline with caller or callee in a cgo-generated -file are rejected. +file are rejected. (This is just a special case of rejecting generated files.) -- go.mod -- module testdata @@ -15,7 +15,7 @@ import "C" func a() { C.f() //@ inline(re"f", re"cannot inline cgo-generated functions") - g() //@ inline(re"g", re`cannot inline calls from files that import "C"`) + g() //@ inline(re"g", re`cannot inline calls from generated files`) } func g() { diff --git a/internal/refactor/inline/testdata/dotimport.txtar b/internal/refactor/inline/testdata/dotimport.txtar index 644398b1df0..927ecc90236 100644 --- a/internal/refactor/inline/testdata/dotimport.txtar +++ b/internal/refactor/inline/testdata/dotimport.txtar @@ -28,9 +28,7 @@ func _() { -- result -- package c -import ( - "testdata/a" -) +import "testdata/a" func _() { a.A() //@ inline(re"B", result) diff --git a/internal/refactor/inline/testdata/import-comments.txtar b/internal/refactor/inline/testdata/import-comments.txtar index b5319e48846..2ddebd9d5d9 100644 --- a/internal/refactor/inline/testdata/import-comments.txtar +++ b/internal/refactor/inline/testdata/import-comments.txtar @@ -49,9 +49,9 @@ package a // This is package a. import ( // This is an import of io. "io" + "testdata/b" // This is an import of c. - "testdata/b" "testdata/c" // yes, of c ) @@ -80,11 +80,10 @@ func _() { -- noparens -- package a // This is package a. +import "testdata/b" + // This is an import of c. -import ( - "testdata/b" - "testdata/c" -) +import "testdata/c" func _() { var _ c.C diff --git a/internal/refactor/inline/testdata/import-comments2.txtar b/internal/refactor/inline/testdata/import-comments2.txtar new file mode 100644 index 00000000000..55d36059cd5 --- /dev/null +++ b/internal/refactor/inline/testdata/import-comments2.txtar @@ -0,0 +1,71 @@ +Regression test for migration of comments in deleted imports (#67336). + +-- go.mod -- +module testdata +go 1.20 + +-- define/my/typ/foo.go -- +package typ +type T int + +-- some/other/pkg/foo.go -- +package pkg +import "context" +import "testdata/define/my/typ" +func Foo(typ.T) context.Context{ return nil } + +-- one/more/pkg/foo.go -- +package pkg +func Bar() {} + +-- to/be/inlined/foo.go -- +package inlined + +import "context" +import "testdata/some/other/pkg" +import "testdata/define/my/typ" + +// inlineme +func Baz(ctx context.Context) context.Context { + return pkg.Foo(typ.T(5)) +} + +-- b/c/foo.go -- +package c +import ( + "context" + "testdata/to/be/inlined" + "testdata/one/more/pkg" +) + +const ( + // This is a constant + someConst = 5 +) + +func foo() { + inlined.Baz(context.TODO()) //@inline(re"Baz", out) + pkg.Bar() +} + +-- out -- +package c + +import ( + "context" + "testdata/define/my/typ" + pkg0 "testdata/some/other/pkg" + + "testdata/one/more/pkg" +) + +const ( + // This is a constant + someConst = 5 +) + +func foo() { + var _ context.Context = context.TODO() + pkg0.Foo(typ.T(5)) //@inline(re"Baz", out) + pkg.Bar() +} diff --git a/internal/refactor/inline/testdata/import-preserve-local-pkgname.txtar b/internal/refactor/inline/testdata/import-preserve-local-pkgname.txtar index 70e11d47bbf..bea2620f9e7 100644 --- a/internal/refactor/inline/testdata/import-preserve-local-pkgname.txtar +++ b/internal/refactor/inline/testdata/import-preserve-local-pkgname.txtar @@ -45,10 +45,9 @@ func A() { -- result -- package main -import ( - stringutil "example.com/string/util" - urlutil "example.com/url/util" -) +import urlutil "example.com/url/util" + +import stringutil "example.com/string/util" func main() { stringutil.A() diff --git a/internal/refactor/inline/testdata/import-rename.txtar b/internal/refactor/inline/testdata/import-rename.txtar index 0b567f626e0..cfd9d6c64b0 100644 --- a/internal/refactor/inline/testdata/import-rename.txtar +++ b/internal/refactor/inline/testdata/import-rename.txtar @@ -31,9 +31,7 @@ func A() { -- result -- package main -import ( - "example.com/other/a" -) +import "example.com/other/a" func main() { a.A() //@ inline(re"A", result) diff --git a/internal/refactor/inline/testdata/import-shadow-1.txtar b/internal/refactor/inline/testdata/import-shadow-1.txtar index dc960ac3213..c92591897e9 100644 --- a/internal/refactor/inline/testdata/import-shadow-1.txtar +++ b/internal/refactor/inline/testdata/import-shadow-1.txtar @@ -35,9 +35,8 @@ func B() { -- bresult -- package a -import ( - log0 "log" -) +import log0 "log" + import "log" func A() { diff --git a/internal/refactor/inline/testdata/import-shadow-2.txtar b/internal/refactor/inline/testdata/import-shadow-2.txtar index 14cd045c6c3..6792559b23d 100644 --- a/internal/refactor/inline/testdata/import-shadow-2.txtar +++ b/internal/refactor/inline/testdata/import-shadow-2.txtar @@ -31,10 +31,9 @@ func Two() {} -- fresult -- package a -import ( - "testdata/b" - b0 "testdata/b" -) +import b0 "testdata/b" + +import "testdata/b" var x b.T @@ -65,9 +64,7 @@ func E() { -- eresult -- package d -import ( - log0 "log" -) +import log0 "log" func D() { const log = "shadow" diff --git a/internal/refactor/inline/testdata/import-shadow.txtar b/internal/refactor/inline/testdata/import-shadow.txtar index c4ea9a61624..fc50b38d0a1 100644 --- a/internal/refactor/inline/testdata/import-shadow.txtar +++ b/internal/refactor/inline/testdata/import-shadow.txtar @@ -39,8 +39,9 @@ func B() { package a import ( - "log" log0 "log" + + "log" ) func A() { diff --git a/internal/refactor/inline/testdata/issue62667.txtar b/internal/refactor/inline/testdata/issue62667.txtar index b6ff83b4bce..af2fd8e8b61 100644 --- a/internal/refactor/inline/testdata/issue62667.txtar +++ b/internal/refactor/inline/testdata/issue62667.txtar @@ -33,9 +33,7 @@ func Split(string) {} -- result -- package a -import ( - path0 "testdata/path" -) +import path0 "testdata/path" func A() { func() { var path string = g(); defer func() {}(); path0.Split(path) }() //@ inline(re"Dir", result) diff --git a/internal/refactor/inline/testdata/issue63298.txtar b/internal/refactor/inline/testdata/issue63298.txtar index b1b5d0c6a4f..09310dfee04 100644 --- a/internal/refactor/inline/testdata/issue63298.txtar +++ b/internal/refactor/inline/testdata/issue63298.txtar @@ -37,10 +37,9 @@ func B() {} -- result -- package a -import ( - anotherb "testdata/another/b" - "testdata/b" -) +import "testdata/b" + +import anotherb "testdata/another/b" func _() { b.B() diff --git a/internal/refactor/inline/testdata/revdotimport.txtar b/internal/refactor/inline/testdata/revdotimport.txtar index f33304f9da3..9be5093cc46 100644 --- a/internal/refactor/inline/testdata/revdotimport.txtar +++ b/internal/refactor/inline/testdata/revdotimport.txtar @@ -31,10 +31,9 @@ func _() { -- result -- package c -import ( - "testdata/a" - . "testdata/a" -) +import "testdata/a" + +import . "testdata/a" func _() { A() diff --git a/internal/refactor/refactor.go b/internal/refactor/refactor.go index 26bc079808f..8664377f854 100644 --- a/internal/refactor/refactor.go +++ b/internal/refactor/refactor.go @@ -5,8 +5,7 @@ // Package refactor provides operators to compute common textual edits // for refactoring tools. // -// This package should not use features of the analysis API -// other than [analysis.TextEdit]. +// This package should not use features of the analysis API other than [Edit]. package refactor import ( From 5c62b7680464a154c257888b5ab7d249a760d461 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 26 Nov 2025 15:37:52 -0500 Subject: [PATCH 44/60] go/analysis/passes/ctrlflow: tabulate "may return" intrinsics too Previously, ctrlflow had a list of functions known never to return, despite appearances, such as os.Exit. This change makes it also record the converse: functions that appear never to return but that in fact may return. This includes compiler intrinsics such as abi.EscapeNonString whose body is ignored by the compiler but calls panic. + test For golang/go#76161 Change-Id: Ie8d478be39efbf7755598f65b6adfe5a85fafe13 Reviewed-on: https://go-review.googlesource.com/c/tools/+/724840 LUCI-TryBot-Result: Go LUCI Reviewed-by: Markus Kusano --- go/analysis/passes/ctrlflow/ctrlflow.go | 48 ++++++++++++++----- .../passes/ctrlflow/testdata/src/a/a.go | 9 ++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/go/analysis/passes/ctrlflow/ctrlflow.go b/go/analysis/passes/ctrlflow/ctrlflow.go index d6c2586e730..b84c8d6fec3 100644 --- a/go/analysis/passes/ctrlflow/ctrlflow.go +++ b/go/analysis/passes/ctrlflow/ctrlflow.go @@ -179,12 +179,13 @@ func (c *CFGs) buildDecl(fn *types.Func, di *declInfo) { } di.started = true - noreturn := isIntrinsicNoReturn(fn) - - if di.decl.Body != nil { - di.cfg = cfg.New(di.decl.Body, c.callMayReturn) - if cfginternal.IsNoReturn(di.cfg) { - noreturn = true + noreturn, known := knownIntrinsic(fn) + if !known { + if di.decl.Body != nil { + di.cfg = cfg.New(di.decl.Body, c.callMayReturn) + if cfginternal.IsNoReturn(di.cfg) { + noreturn = true + } } } if noreturn { @@ -233,11 +234,36 @@ func (c *CFGs) callMayReturn(call *ast.CallExpr) (r bool) { var panicBuiltin = types.Universe.Lookup("panic").(*types.Builtin) -// isIntrinsicNoReturn reports whether a function intrinsically never -// returns because it stops execution of the calling thread. +// knownIntrinsic reports whether a function intrinsically never +// returns because it stops execution of the calling thread, or does +// in fact return, contrary to its apparent body, because it is +// handled specially by the compiler. +// // It is the base case in the recursion. -func isIntrinsicNoReturn(fn *types.Func) bool { +func knownIntrinsic(fn *types.Func) (noreturn, known bool) { // Add functions here as the need arises, but don't allocate memory. - return typesinternal.IsFunctionNamed(fn, "syscall", "Exit", "ExitProcess", "ExitThread") || - typesinternal.IsFunctionNamed(fn, "runtime", "Goexit") + + // Functions known intrinsically never to return. + if typesinternal.IsFunctionNamed(fn, "syscall", "Exit", "ExitProcess", "ExitThread") || + typesinternal.IsFunctionNamed(fn, "runtime", "Goexit") { + return true, true + } + + // Compiler intrinsics known to return, contrary to + // what analysis of the function body would conclude. + // + // Not all such intrinsics must be listed here: ctrlflow + // considers any function called for its value--such as + // crypto/internal/constanttime.bool2Uint8--to potentially + // return; only functions called as a statement, for effects, + // are no-return candidates. + // + // Unfortunately this does sometimes mean peering into internals. + // Where possible, use the nearest enclosing public API function. + if typesinternal.IsFunctionNamed(fn, "internal/abi", "EscapeNonString") || + typesinternal.IsFunctionNamed(fn, "hash/maphash", "Comparable") { + return false, true + } + + return // unknown } diff --git a/go/analysis/passes/ctrlflow/testdata/src/a/a.go b/go/analysis/passes/ctrlflow/testdata/src/a/a.go index c9126229f5f..e41ca70be2b 100644 --- a/go/analysis/passes/ctrlflow/testdata/src/a/a.go +++ b/go/analysis/passes/ctrlflow/testdata/src/a/a.go @@ -7,6 +7,7 @@ package a // This file tests facts produced by ctrlflow. import ( + "hash/maphash" "log" "os" "runtime" @@ -192,3 +193,11 @@ func osexit() { // want osexit:"noReturn" os.Exit(0) print(2) } + +func intrinsic() { // (no fact) + + // Comparable calls abi.EscapeNonString, whose body appears to panic; + // but that's a lie, as EscapeNonString is a compiler intrinsic. + // (go1.24 used a different intrinsic, maphash.escapeForHash.) + maphash.Comparable[int](maphash.Seed{}, 0) +} From 869ced31773aa6009975882daa7005641f1364fc Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Mon, 1 Dec 2025 11:55:03 -0500 Subject: [PATCH 45/60] gopls/internal/golang: fix panic in definition Fixes panic in definition on a "goto" with an undefined label. Fixes golang/go#76625 Change-Id: I15280f6b4d51dc8034fb5b9c0ecc60646760fe56 Reviewed-on: https://go-review.googlesource.com/c/tools/+/725520 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/definition.go | 3 ++ .../test/integration/misc/definition_test.go | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go index 5f928b5d2c5..1722d9d509e 100644 --- a/gopls/internal/golang/definition.go +++ b/gopls/internal/golang/definition.go @@ -118,6 +118,9 @@ func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, p label, isLabeled := pkg.TypesInfo().Uses[node.Label].(*types.Label) switch node.Tok { case token.GOTO: + if !isLabeled { + return nil, fmt.Errorf("undefined label") + } loc, err := pgf.PosLocation(label.Pos(), label.Pos()+token.Pos(len(label.Name()))) if err != nil { return nil, err diff --git a/gopls/internal/test/integration/misc/definition_test.go b/gopls/internal/test/integration/misc/definition_test.go index e54b7338682..7c0b16dbc17 100644 --- a/gopls/internal/test/integration/misc/definition_test.go +++ b/gopls/internal/test/integration/misc/definition_test.go @@ -750,3 +750,34 @@ func Bar() {} env.RegexpSearch(unsafePath, `\[()Sizeof\]`)) }) } +func TestUndefinedLabel_Issue76625(t *testing.T) { + const src = ` +-- go.mod -- +module mod.com + +go 1.22 +-- a.go -- +package a + +func UndefinedLabel() { + for i := range 10 { + if i > 2 { + goto undefinedLabel + } + } +} +` + Run(t, src, func(t *testing.T, env *Env) { + env.OpenFile("a.go") + expectedErr := "undefined label" + gotoUndefined := env.RegexpSearch("a.go", `goto`) + _, err := env.Editor.Definitions(env.Ctx, gotoUndefined) + if err == nil { + t.Fatalf("request succeeded unexpectedly, want error %s", err) + } + if !strings.Contains(err.Error(), expectedErr) { + t.Fatalf("received unexpected error message %s, want %s", err, expectedErr) + } + }) + +} From c3feb709497e4eaf04605359c225a266c6660bcf Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Mon, 24 Nov 2025 15:08:22 -0500 Subject: [PATCH 46/60] gopls/internal/golang: add skeleton for move type codeaction This is the first CL for implementing move type refactoring, which will allow users to move a type declaration to a different package. The code action producer for move type is off. Change-Id: I10bface65ab6e1cb94180433360c923e8fa78c59 Reviewed-on: https://go-review.googlesource.com/c/tools/+/724020 Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/codeaction.go | 10 +++++ gopls/internal/golang/movetype.go | 45 +++++++++++++++++++ .../internal/protocol/command/command_gen.go | 16 +++++++ gopls/internal/protocol/command/interface.go | 11 +++++ gopls/internal/server/command.go | 13 ++++++ gopls/internal/settings/codeactionkind.go | 3 ++ gopls/internal/settings/default.go | 1 + 7 files changed, 99 insertions(+) create mode 100644 gopls/internal/golang/movetype.go diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go index 04e57c5eed3..d09fc47b510 100644 --- a/gopls/internal/golang/codeaction.go +++ b/gopls/internal/golang/codeaction.go @@ -255,6 +255,7 @@ var codeActionProducers = [...]codeActionProducer{ {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.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote}, {kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true}, {kind: settings.RefactorRewriteFillSwitch, fn: refactorRewriteFillSwitch, needPkg: true}, @@ -1132,3 +1133,12 @@ func toggleCompilerOptDetails(ctx context.Context, req *codeActionsRequest) erro } return nil } + +func refactorMoveType(ctx context.Context, req *codeActionsRequest) error { + 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}) + req.addCommandAction(cmd, false) + } + return nil +} diff --git a/gopls/internal/golang/movetype.go b/gopls/internal/golang/movetype.go new file mode 100644 index 00000000000..8aefc9feeb0 --- /dev/null +++ b/gopls/internal/golang/movetype.go @@ -0,0 +1,45 @@ +// 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 golang + +import ( + "context" + "fmt" + "go/ast" + "go/token" + + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/gopls/internal/cache" + "golang.org/x/tools/gopls/internal/file" + "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/internal/moreiters" +) + +// 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") +} + +// 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 + } + + // 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 + } + + typSpec, ok := declNode.Specs[0].(*ast.TypeSpec) + if !ok { + return inspector.Cursor{}, &ast.GenDecl{}, &ast.TypeSpec{}, "", false + } + + return declCur, declNode, declNode.Specs[0].(*ast.TypeSpec), typSpec.Name.Name, true +} diff --git a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go index d667e72e30b..dbe47db7d40 100644 --- a/gopls/internal/protocol/command/command_gen.go +++ b/gopls/internal/protocol/command/command_gen.go @@ -49,6 +49,7 @@ const ( MemStats Command = "gopls.mem_stats" ModifyTags Command = "gopls.modify_tags" Modules Command = "gopls.modules" + MoveType Command = "gopls.move_type" PackageSymbols Command = "gopls.package_symbols" Packages Command = "gopls.packages" RegenerateCgo Command = "gopls.regenerate_cgo" @@ -97,6 +98,7 @@ var Commands = []Command{ MemStats, ModifyTags, Modules, + MoveType, PackageSymbols, Packages, RegenerateCgo, @@ -266,6 +268,12 @@ func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Inte return nil, err } return s.Modules(ctx, a0) + case MoveType: + var a0 MoveTypeArgs + if err := UnmarshalArgs(params.Arguments, &a0); err != nil { + return nil, err + } + return nil, s.MoveType(ctx, a0) case PackageSymbols: var a0 PackageSymbolsArgs if err := UnmarshalArgs(params.Arguments, &a0); err != nil { @@ -579,6 +587,14 @@ func NewModulesCommand(title string, a0 ModulesArgs) *protocol.Command { } } +func NewMoveTypeCommand(title string, a0 MoveTypeArgs) *protocol.Command { + return &protocol.Command{ + Title: title, + Command: MoveType.String(), + Arguments: MustMarshalArgs(a0), + } +} + func NewPackageSymbolsCommand(title string, a0 PackageSymbolsArgs) *protocol.Command { return &protocol.Command{ Title: title, diff --git a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go index f3e6f0135e4..e257252b1ea 100644 --- a/gopls/internal/protocol/command/interface.go +++ b/gopls/internal/protocol/command/interface.go @@ -322,6 +322,9 @@ type Interface interface { // ModifyTags: Add or remove struct tags on a given node. ModifyTags(context.Context, ModifyTagsArgs) error + + // MoveType: Move a type declaration to a different package. + MoveType(context.Context, MoveTypeArgs) error } type RunTestsArgs struct { @@ -876,3 +879,11 @@ type LSPArgs struct { Method string `json:"method"` Param json.RawMessage `json:"param"` } + +// MoveTypeArgs specifies a "move type" refactoring to perform. +type MoveTypeArgs struct { + // The location of the type to move. + Location protocol.Location + // TODO(mkalil): Determine format of the parameter that specifies where to + // move the type to. +} diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go index 675a29d8340..7e62ab527c1 100644 --- a/gopls/internal/server/command.go +++ b/gopls/internal/server/command.go @@ -1892,3 +1892,16 @@ func parseTransform(input string) (modifytags.Transform, error) { return modifytags.SnakeCase, fmt.Errorf("invalid Transform value") } } + +func (c *commandHandler) MoveType(ctx context.Context, args command.MoveTypeArgs) error { + 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") + if err != nil { + return err + } + return applyChanges(ctx, c.s.client, changes) + }) + return err +} diff --git a/gopls/internal/settings/codeactionkind.go b/gopls/internal/settings/codeactionkind.go index e083cae0ed6..8f9d1f4ae7a 100644 --- a/gopls/internal/settings/codeactionkind.go +++ b/gopls/internal/settings/codeactionkind.go @@ -114,6 +114,9 @@ const ( RefactorExtractVariableAll protocol.CodeActionKind = "refactor.extract.variable-all" RefactorExtractToNewFile protocol.CodeActionKind = "refactor.extract.toNewFile" + // refactor.move + RefactorMoveType protocol.CodeActionKind = "refactor.move.moveType" + // Note: add new kinds to: // - the SupportedCodeActions map in default.go // - the codeActionProducers table in ../golang/codeaction.go diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go index 514d031e299..f197ad26ae9 100644 --- a/gopls/internal/settings/default.go +++ b/gopls/internal/settings/default.go @@ -71,6 +71,7 @@ func DefaultOptions(overrides ...func(*Options)) *Options { RefactorExtractVariable: true, RefactorExtractVariableAll: true, RefactorExtractToNewFile: true, + RefactorMoveType: true, // off while implementation unfinished // Not GoTest: it must be explicit in CodeActionParams.Context.Only }, file.Mod: { From ac87d8414eb64cbc6fdfa14dcd6d332464b0209e Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 2 Dec 2025 11:04:29 -0500 Subject: [PATCH 47/60] internal/event/label: unsafe unsafe.String{,Data} not StringHeader The old way is deprecated and appears to create a liveness hole (i.e. hides a pointer from the GC), though the compiler does seem to have a special case for it (see ir.IsReflectHeaderDataField). Change-Id: I3e2d3b0d7a8d7b58d0adca55f5c8e87fce9c4d90 Reviewed-on: https://go-review.googlesource.com/c/tools/+/725900 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- internal/event/label/label.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/internal/event/label/label.go b/internal/event/label/label.go index 92a39105731..c37584af943 100644 --- a/internal/event/label/label.go +++ b/internal/event/label/label.go @@ -7,7 +7,6 @@ package label import ( "fmt" "io" - "reflect" "slices" "unsafe" ) @@ -103,11 +102,10 @@ type stringptr unsafe.Pointer // This method is for implementing new key types, label creation should // normally be done with the Of method of the key. func OfString(k Key, v string) Label { - hdr := (*reflect.StringHeader)(unsafe.Pointer(&v)) return Label{ key: k, - packed: uint64(hdr.Len), - untyped: stringptr(hdr.Data), + packed: uint64(len(v)), + untyped: stringptr(unsafe.StringData(v)), } } @@ -116,11 +114,7 @@ func OfString(k Key, v string) Label { // This method is for implementing new key types, for type safety normal // access should be done with the From method of the key. func (t Label) UnpackString() string { - var v string - hdr := (*reflect.StringHeader)(unsafe.Pointer(&v)) - hdr.Data = uintptr(t.untyped.(stringptr)) - hdr.Len = int(t.packed) - return v + return unsafe.String((*byte)(t.untyped.(stringptr)), int(t.packed)) } // Valid returns true if the Label is a valid one (it has a key). From d1bcb8b11a3ff1df877939edc7aadebdda983821 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Tue, 2 Dec 2025 11:19:06 -0500 Subject: [PATCH 48/60] x/tools: audit "unsafe" This CL contains a number of small cleanups from auditing all uses of the "unsafe" package (and the reflect.Value.Unsafe* methods) in dependencies of gopls as potential causes of memory corruption. None look actually problematic. Change-Id: I443f29caf00c2d3734a31c5fa496e5296952930c Reviewed-on: https://go-review.googlesource.com/c/tools/+/725940 Reviewed-by: Robert Findley Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI --- internal/event/core/export.go | 15 ++++++--------- internal/typesinternal/classify_call.go | 2 +- internal/typesinternal/types.go | 4 +--- refactor/satisfy/find.go | 4 +--- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/internal/event/core/export.go b/internal/event/core/export.go index 05f3a9a5791..16ae6bb0211 100644 --- a/internal/event/core/export.go +++ b/internal/event/core/export.go @@ -8,7 +8,6 @@ import ( "context" "sync/atomic" "time" - "unsafe" "golang.org/x/tools/internal/event/label" ) @@ -17,23 +16,21 @@ import ( // It may return a modified context and event. type Exporter func(context.Context, Event, label.Map) context.Context -var ( - exporter unsafe.Pointer -) +var exporter atomic.Pointer[Exporter] // SetExporter sets the global exporter function that handles all events. // The exporter is called synchronously from the event call site, so it should // return quickly so as not to hold up user code. func SetExporter(e Exporter) { - p := unsafe.Pointer(&e) if e == nil { // &e is always valid, and so p is always valid, but for the early abort // of ProcessEvent to be efficient it needs to make the nil check on the // pointer without having to dereference it, so we make the nil function // also a nil pointer - p = nil + exporter.Store(nil) + } else { + exporter.Store(&e) } - atomic.StorePointer(&exporter, p) } // deliver is called to deliver an event to the supplied exporter. @@ -48,7 +45,7 @@ func deliver(ctx context.Context, exporter Exporter, ev Event) context.Context { // Export is called to deliver an event to the global exporter if set. func Export(ctx context.Context, ev Event) context.Context { // get the global exporter and abort early if there is not one - exporterPtr := (*Exporter)(atomic.LoadPointer(&exporter)) + exporterPtr := exporter.Load() if exporterPtr == nil { return ctx } @@ -61,7 +58,7 @@ func Export(ctx context.Context, ev Event) context.Context { // It will fill in the time. func ExportPair(ctx context.Context, begin, end Event) (context.Context, func()) { // get the global exporter and abort early if there is not one - exporterPtr := (*Exporter)(atomic.LoadPointer(&exporter)) + exporterPtr := exporter.Load() if exporterPtr == nil { return ctx, func() {} } diff --git a/internal/typesinternal/classify_call.go b/internal/typesinternal/classify_call.go index 3db2a135b97..7ebe9768bc3 100644 --- a/internal/typesinternal/classify_call.go +++ b/internal/typesinternal/classify_call.go @@ -8,7 +8,7 @@ import ( "fmt" "go/ast" "go/types" - _ "unsafe" + _ "unsafe" // for go:linkname hack ) // CallKind describes the function position of an [*ast.CallExpr]. diff --git a/internal/typesinternal/types.go b/internal/typesinternal/types.go index fef74a78560..51001666ef1 100644 --- a/internal/typesinternal/types.go +++ b/internal/typesinternal/types.go @@ -23,7 +23,6 @@ import ( "go/token" "go/types" "reflect" - "unsafe" "golang.org/x/tools/go/ast/inspector" "golang.org/x/tools/internal/aliases" @@ -40,8 +39,7 @@ func SetUsesCgo(conf *types.Config) bool { } } - addr := unsafe.Pointer(f.UnsafeAddr()) - *(*bool)(addr) = true + *(*bool)(f.Addr().UnsafePointer()) = true return true } diff --git a/refactor/satisfy/find.go b/refactor/satisfy/find.go index 6d23aa690f0..bb383755317 100644 --- a/refactor/satisfy/find.go +++ b/refactor/satisfy/find.go @@ -8,9 +8,7 @@ // interface, and this fact is necessary for the package to be // well-typed. // -// THIS PACKAGE IS EXPERIMENTAL AND MAY CHANGE AT ANY TIME. -// -// It is provided only for the gopls tool. It requires well-typed inputs. +// It requires well-typed inputs. package satisfy // import "golang.org/x/tools/refactor/satisfy" // NOTES: From e1b23816d1ca25d0fd4240ee7c6128b5c20d7e8f Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 1 Dec 2025 17:36:06 -0500 Subject: [PATCH 49/60] go/analysis/passes/modernize: omitzero: suppress on kubebuilder This CL causes the omitzero pass to be silent if the file mentions "+kubebuilder" in a comment, since that system has its own interpretation of what "omitzero" means in a json tag. This a slightly sleazy heuristic; if it proves inadequate in practice then we should (sadly) disable this modernizer. Fixes golang/go#76649 Change-Id: I0683dd8ea99c777bf0fd1ce89b23f20284c15b00 Reviewed-on: https://go-review.googlesource.com/c/tools/+/725681 Auto-Submit: Alan Donovan LUCI-TryBot-Result: Go LUCI Reviewed-by: Madeline Kalil --- go/analysis/passes/modernize/doc.go | 13 +- .../passes/modernize/modernize_test.go | 2 +- go/analysis/passes/modernize/omitzero.go | 152 ++++++++++-------- .../testdata/src/omitzero/kube/kube.go | 9 ++ gopls/doc/analyzers.md | 4 +- gopls/internal/doc/api.json | 4 +- gopls/internal/protocol/tsserver.go | 9 +- 7 files changed, 118 insertions(+), 75 deletions(-) create mode 100644 go/analysis/passes/modernize/testdata/src/omitzero/kube/kube.go diff --git a/go/analysis/passes/modernize/doc.go b/go/analysis/passes/modernize/doc.go index 7469002f56e..45aed7909c3 100644 --- a/go/analysis/passes/modernize/doc.go +++ b/go/analysis/passes/modernize/doc.go @@ -199,12 +199,19 @@ are often used to express optionality. omitzero: suggest replacing omitempty with omitzero for struct fields -The omitzero analyzer identifies uses of the `omitempty` JSON struct tag on -fields that are themselves structs. The `omitempty` tag has no effect on -struct-typed fields. The analyzer offers two suggestions: either remove the +The omitzero analyzer identifies uses of the `omitempty` JSON struct +tag on fields that are themselves structs. For struct-typed fields, +the `omitempty` tag has no effect on the behavior of json.Marshal and +json.Unmarshal. The analyzer offers two suggestions: either remove the tag, or replace it with `omitzero` (added in Go 1.24), which correctly omits the field if the struct value is zero. +However, some other serialization packages (notably kubebuilder, see +https://book.kubebuilder.io/reference/markers.html) may have their own +interpretation of the `json:",omitzero"` tag, so removing it may affect +program behavior. For this reason, the omitzero modernizer will not +make changes in any package that contains +kubebuilder annotations. + Replacing `omitempty` with `omitzero` is a change in behavior. The original code would always encode the struct field, whereas the modified code will omit it if it is a zero-value. diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go index ce4341b0725..14d04b34a2e 100644 --- a/go/analysis/passes/modernize/modernize_test.go +++ b/go/analysis/passes/modernize/modernize_test.go @@ -55,7 +55,7 @@ func TestNewExpr(t *testing.T) { } func TestOmitZero(t *testing.T) { - RunWithSuggestedFixes(t, TestData(), modernize.OmitZeroAnalyzer, "omitzero") + RunWithSuggestedFixes(t, TestData(), modernize.OmitZeroAnalyzer, "omitzero/...") } func TestRangeInt(t *testing.T) { diff --git a/go/analysis/passes/modernize/omitzero.go b/go/analysis/passes/modernize/omitzero.go index 4a05d64f42a..59ba9506511 100644 --- a/go/analysis/passes/modernize/omitzero.go +++ b/go/analysis/passes/modernize/omitzero.go @@ -9,6 +9,8 @@ import ( "go/types" "reflect" "strconv" + "strings" + "sync" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" @@ -25,82 +27,106 @@ var OmitZeroAnalyzer = &analysis.Analyzer{ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero", } -func checkOmitEmptyField(pass *analysis.Pass, info *types.Info, curField *ast.Field) { - typ := info.TypeOf(curField.Type) - _, ok := typ.Underlying().(*types.Struct) - if !ok { - // Not a struct - return - } - tag := curField.Tag - if tag == nil { - // No tag to check - return - } - // The omitempty tag may be used by other packages besides json, but we should only modify its use with json - tagconv, _ := strconv.Unquote(tag.Value) - match := omitemptyRegex.FindStringSubmatchIndex(tagconv) - if match == nil { - // No omitempty in json tag - return - } - omitEmpty, err := astutil.RangeInStringLiteral(curField.Tag, match[2], match[3]) - if err != nil { - return - } - var remove analysis.Range = omitEmpty +// The omitzero pass searches for instances of "omitempty" in a json field tag on a +// struct. Since "omitfilesUsingGoVersions not have any effect when applied to a struct field, +// it suggests either deleting "omitempty" or replacing it with "omitzero", which +// correctly excludes structs from a json encoding. +func omitzero(pass *analysis.Pass) (any, error) { + // usesKubebuilder reports whether "+kubebuilder:" appears in + // any comment in the package, since it has its own + // interpretation of what omitzero means; see go.dev/issue/76649. + // It is computed once, on demand. + usesKubebuilder := sync.OnceValue[bool](func() bool { + for _, file := range pass.Files { + for _, comment := range file.Comments { + if strings.Contains(comment.Text(), "+kubebuilder:") { + return true + } + } + } + return false + }) + + checkField := func(field *ast.Field) { + typ := pass.TypesInfo.TypeOf(field.Type) + _, ok := typ.Underlying().(*types.Struct) + if !ok { + // Not a struct + return + } + tag := field.Tag + if tag == nil { + // No tag to check + return + } + // The omitempty tag may be used by other packages besides json, but we should only modify its use with json + tagconv, _ := strconv.Unquote(tag.Value) + match := omitemptyRegex.FindStringSubmatchIndex(tagconv) + if match == nil { + // No omitempty in json tag + return + } + omitEmpty, err := astutil.RangeInStringLiteral(field.Tag, match[2], match[3]) + if err != nil { + return + } + var remove analysis.Range = omitEmpty - jsonTag := reflect.StructTag(tagconv).Get("json") - if jsonTag == ",omitempty" { - // Remove the entire struct tag if json is the only package used - if match[1]-match[0] == len(tagconv) { - remove = curField.Tag - } else { - // Remove the json tag if omitempty is the only field - remove, err = astutil.RangeInStringLiteral(curField.Tag, match[0], match[1]) - if err != nil { - return + jsonTag := reflect.StructTag(tagconv).Get("json") + if jsonTag == ",omitempty" { + // Remove the entire struct tag if json is the only package used + if match[1]-match[0] == len(tagconv) { + remove = field.Tag + } else { + // Remove the json tag if omitempty is the only field + remove, err = astutil.RangeInStringLiteral(field.Tag, match[0], match[1]) + if err != nil { + return + } } } - } - pass.Report(analysis.Diagnostic{ - Pos: curField.Tag.Pos(), - End: curField.Tag.End(), - Message: "Omitempty has no effect on nested struct fields", - SuggestedFixes: []analysis.SuggestedFix{ - { - Message: "Remove redundant omitempty tag", - TextEdits: []analysis.TextEdit{ - { - Pos: remove.Pos(), - End: remove.End(), + + // Don't offer a fix if the package seems to use kubebuilder, + // as it has its own intepretation of "omitzero" tags. + // https://book.kubebuilder.io/reference/markers.html + if usesKubebuilder() { + return + } + + pass.Report(analysis.Diagnostic{ + Pos: field.Tag.Pos(), + End: field.Tag.End(), + Message: "Omitempty has no effect on nested struct fields", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Remove redundant omitempty tag", + TextEdits: []analysis.TextEdit{ + { + Pos: remove.Pos(), + End: remove.End(), + }, }, }, - }, - { - Message: "Replace omitempty with omitzero (behavior change)", - TextEdits: []analysis.TextEdit{ - { - Pos: omitEmpty.Pos(), - End: omitEmpty.End(), - NewText: []byte(",omitzero"), + { + Message: "Replace omitempty with omitzero (behavior change)", + TextEdits: []analysis.TextEdit{ + { + Pos: omitEmpty.Pos(), + End: omitEmpty.End(), + NewText: []byte(",omitzero"), + }, }, }, - }, - }}) -} + }}) + } -// The omitzero pass searches for instances of "omitempty" in a json field tag on a -// struct. Since "omitfilesUsingGoVersions not have any effect when applied to a struct field, -// it suggests either deleting "omitempty" or replacing it with "omitzero", which -// correctly excludes structs from a json encoding. -func omitzero(pass *analysis.Pass) (any, error) { for curFile := range filesUsingGoVersion(pass, versions.Go1_24) { for curStruct := range curFile.Preorder((*ast.StructType)(nil)) { for _, curField := range curStruct.Node().(*ast.StructType).Fields.List { - checkOmitEmptyField(pass, pass.TypesInfo, curField) + checkField(curField) } } } + return nil, nil } diff --git a/go/analysis/passes/modernize/testdata/src/omitzero/kube/kube.go b/go/analysis/passes/modernize/testdata/src/omitzero/kube/kube.go new file mode 100644 index 00000000000..fce6d3c01e7 --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/omitzero/kube/kube.go @@ -0,0 +1,9 @@ +package kube + +type Foo struct { + EmptyStruct struct{} `json:",omitempty"` // nope: the comment below mentions the k-word +} + +// +kubebuilder:validation:Optional +type Other struct { +} diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index dfd5f00d0e0..ddb4a1e0e93 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -3596,7 +3596,9 @@ Package documentation: [noresultvalues](https://pkg.go.dev/golang.org/x/tools/go ## `omitzero`: suggest replacing omitempty with omitzero for struct fields -The omitzero analyzer identifies uses of the \`omitempty\` JSON struct tag on fields that are themselves structs. The \`omitempty\` tag has no effect on struct-typed fields. The analyzer offers two suggestions: either remove the tag, or replace it with \`omitzero\` (added in Go 1.24), which correctly omits the field if the struct value is zero. +The omitzero analyzer identifies uses of the \`omitempty\` JSON struct tag on fields that are themselves structs. For struct-typed fields, the \`omitempty\` tag has no effect on the behavior of json.Marshal and json.Unmarshal. The analyzer offers two suggestions: either remove the tag, or replace it with \`omitzero\` (added in Go 1.24), which correctly omits the field if the struct value is zero. + +However, some other serialization packages (notably kubebuilder, see [https://book.kubebuilder.io/reference/markers.html](https://book.kubebuilder.io/reference/markers.html)) may have their own interpretation of the \`json:",omitzero"\` tag, so removing it may affect program behavior. For this reason, the omitzero modernizer will not make changes in any package that contains +kubebuilder annotations. Replacing \`omitempty\` with \`omitzero\` is a change in behavior. The original code would always encode the struct field, whereas the modified code will omit it if it is a zero-value. diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index 7df2fef656e..e4f72628c94 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -1576,7 +1576,7 @@ }, { "Name": "\"omitzero\"", - "Doc": "suggest replacing omitempty with omitzero for struct fields\n\nThe omitzero analyzer identifies uses of the `omitempty` JSON struct tag on\nfields that are themselves structs. The `omitempty` tag has no effect on\nstruct-typed fields. The analyzer offers two suggestions: either remove the\ntag, or replace it with `omitzero` (added in Go 1.24), which correctly\nomits the field if the struct value is zero.\n\nReplacing `omitempty` with `omitzero` is a change in behavior. The\noriginal code would always encode the struct field, whereas the\nmodified code will omit it if it is a zero-value.", + "Doc": "suggest replacing omitempty with omitzero for struct fields\n\nThe omitzero analyzer identifies uses of the `omitempty` JSON struct\ntag on fields that are themselves structs. For struct-typed fields,\nthe `omitempty` tag has no effect on the behavior of json.Marshal and\njson.Unmarshal. The analyzer offers two suggestions: either remove the\ntag, or replace it with `omitzero` (added in Go 1.24), which correctly\nomits the field if the struct value is zero.\n\nHowever, some other serialization packages (notably kubebuilder, see\nhttps://book.kubebuilder.io/reference/markers.html) may have their own\ninterpretation of the `json:\",omitzero\"` tag, so removing it may affect\nprogram behavior. For this reason, the omitzero modernizer will not\nmake changes in any package that contains +kubebuilder annotations.\n\nReplacing `omitempty` with `omitzero` is a change in behavior. The\noriginal code would always encode the struct field, whereas the\nmodified code will omit it if it is a zero-value.", "Default": "true", "Status": "" }, @@ -3479,7 +3479,7 @@ }, { "Name": "omitzero", - "Doc": "suggest replacing omitempty with omitzero for struct fields\n\nThe omitzero analyzer identifies uses of the `omitempty` JSON struct tag on\nfields that are themselves structs. The `omitempty` tag has no effect on\nstruct-typed fields. The analyzer offers two suggestions: either remove the\ntag, or replace it with `omitzero` (added in Go 1.24), which correctly\nomits the field if the struct value is zero.\n\nReplacing `omitempty` with `omitzero` is a change in behavior. The\noriginal code would always encode the struct field, whereas the\nmodified code will omit it if it is a zero-value.", + "Doc": "suggest replacing omitempty with omitzero for struct fields\n\nThe omitzero analyzer identifies uses of the `omitempty` JSON struct\ntag on fields that are themselves structs. For struct-typed fields,\nthe `omitempty` tag has no effect on the behavior of json.Marshal and\njson.Unmarshal. The analyzer offers two suggestions: either remove the\ntag, or replace it with `omitzero` (added in Go 1.24), which correctly\nomits the field if the struct value is zero.\n\nHowever, some other serialization packages (notably kubebuilder, see\nhttps://book.kubebuilder.io/reference/markers.html) may have their own\ninterpretation of the `json:\",omitzero\"` tag, so removing it may affect\nprogram behavior. For this reason, the omitzero modernizer will not\nmake changes in any package that contains +kubebuilder annotations.\n\nReplacing `omitempty` with `omitzero` is a change in behavior. The\noriginal code would always encode the struct field, whereas the\nmodified code will omit it if it is a zero-value.", "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#omitzero", "Default": true }, diff --git a/gopls/internal/protocol/tsserver.go b/gopls/internal/protocol/tsserver.go index 7b010e9ab14..cf6dce7098f 100644 --- a/gopls/internal/protocol/tsserver.go +++ b/gopls/internal/protocol/tsserver.go @@ -28,15 +28,14 @@ type Server interface { // See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification#callHierarchy_outgoingCalls OutgoingCalls(context.Context, *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) // To support microsoft/language-server-protocol#1164, the language server - // need to read the client-supplied form answers and either returns a - // CodeAction with errors in the form fields surfacing the error to the - // client, or a CodeAction with properties the language client is waiting - // for (e.g. edits, commands). + // need to read the form with client-supplied answers and either returns a + // CodeAction with errors in the form surfacing the error to the client, or a + // CodeAction with properties the language client is waiting for (e.g. edits, + // commands). // // The language client may call "codeAction/resolve" if the language server // returns a CodeAction with errors or try asking the user for completing the // form again. - // // The language client may call "codeAction/resolve" multiple times with user // filled (re-filled) answers in the form until it obtains a CodeAction with // properties (e.g. edits, commands) it's waiting for. From 51b51a373542f4721073e6015e9781a2911bf874 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Mon, 1 Dec 2025 09:40:36 -0500 Subject: [PATCH 50/60] go/analysis/passes/modernize: unsafefuncs: ptr+int => unsafe.Add This CL adds a new modernizer (only to gopls for now) that replaces unsafe pointer arithmetic by calls to helper functions such as go1.17's unsafe.Add. We really need some kind of template matcher like the one in staticcheck, ideally integrated with cursors. + tests, docs. For golang/go#76648 Change-Id: Ia3e7d603efd8a01e723370b485ab83b6838522b8 Reviewed-on: https://go-review.googlesource.com/c/tools/+/725680 Reviewed-by: Daniel Morsing Reviewed-by: Madeline Kalil LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/modernize/doc.go | 16 ++ go/analysis/passes/modernize/modernize.go | 1 + .../passes/modernize/modernize_test.go | 4 + .../testdata/src/unsafefuncs/unsafefuncs.go | 27 +++ .../src/unsafefuncs/unsafefuncs.go.golden | 27 +++ go/analysis/passes/modernize/unsafefuncs.go | 210 ++++++++++++++++++ go/types/typeutil/callee.go | 1 + gopls/doc/analyzers.md | 18 ++ gopls/internal/doc/api.json | 12 + gopls/internal/settings/analysis.go | 1 + internal/goplsexport/export.go | 1 + internal/versions/features.go | 1 + 12 files changed, 319 insertions(+) create mode 100644 go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go create mode 100644 go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go.golden create mode 100644 go/analysis/passes/modernize/unsafefuncs.go diff --git a/go/analysis/passes/modernize/doc.go b/go/analysis/passes/modernize/doc.go index 45aed7909c3..03c21fa73b1 100644 --- a/go/analysis/passes/modernize/doc.go +++ b/go/analysis/passes/modernize/doc.go @@ -482,6 +482,22 @@ with a single call to t.Context(), which was added in Go 1.24. This change is only suggested if the `cancel` function is not used for any other purpose. +# Analyzer unsafefuncs + +unsafefuncs: replace unsafe pointer arithmetic with function calls + +The unsafefuncs analyzer simplifies pointer arithmetic expressions by +replacing them with calls to helper functions such as unsafe.Add, +added in Go 1.17. + +Example: + + unsafe.Pointer(uintptr(ptr) + uintptr(n)) + +where ptr is an unsafe.Pointer, is replaced by: + + unsafe.Add(ptr, n) + # Analyzer waitgroup waitgroup: replace wg.Add(1)/go/wg.Done() with wg.Go diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go index 013ce79d6c7..da988a78ae6 100644 --- a/go/analysis/passes/modernize/modernize.go +++ b/go/analysis/passes/modernize/modernize.go @@ -53,6 +53,7 @@ var Suite = []*analysis.Analyzer{ StringsSeqAnalyzer, StringsBuilderAnalyzer, TestingContextAnalyzer, + unsafeFuncsAnalyzer, WaitGroupAnalyzer, } diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go index 14d04b34a2e..4a3f27b2847 100644 --- a/go/analysis/passes/modernize/modernize_test.go +++ b/go/analysis/passes/modernize/modernize_test.go @@ -109,6 +109,10 @@ func TestTestingContext(t *testing.T) { RunWithSuggestedFixes(t, TestData(), modernize.TestingContextAnalyzer, "testingcontext") } +func TestUnsafeFuncs(t *testing.T) { + RunWithSuggestedFixes(t, TestData(), goplsexport.UnsafeFuncsModernizer, "unsafefuncs") +} + func TestWaitGroup(t *testing.T) { RunWithSuggestedFixes(t, TestData(), modernize.WaitGroupAnalyzer, "waitgroup") } diff --git a/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go b/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go new file mode 100644 index 00000000000..dfbc131e279 --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go @@ -0,0 +1,27 @@ +package unsafefuncs + +import "unsafe" + +func _(ptr unsafe.Pointer) unsafe.Pointer { + return unsafe.Pointer(uintptr(ptr) + 1) // want `pointer \+ integer can be simplified using unsafe.Add` +} + +type uP = unsafe.Pointer + +func _(ptr uP) uP { + return uP(uintptr(ptr) + 1) // want `pointer \+ integer can be simplified using unsafe.Add` +} + +func _(ptr unsafe.Pointer, n int) unsafe.Pointer { + return unsafe.Pointer(uintptr(ptr) + uintptr(n)) // want `pointer \+ integer can be simplified using unsafe.Add` +} + +func _(ptr *byte, len int) *byte { + return (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(ptr)) + uintptr(len))) // want `pointer \+ integer can be simplified using unsafe.Add` +} + +type namedUP unsafe.Pointer + +func _(ptr namedUP) namedUP { + return namedUP(uintptr(ptr) + 1) // nope: Add does not accept named unsafe.Pointer types +} diff --git a/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go.golden b/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go.golden new file mode 100644 index 00000000000..f8aadcb308f --- /dev/null +++ b/go/analysis/passes/modernize/testdata/src/unsafefuncs/unsafefuncs.go.golden @@ -0,0 +1,27 @@ +package unsafefuncs + +import "unsafe" + +func _(ptr unsafe.Pointer) unsafe.Pointer { + return unsafe.Add(ptr, 1) // want `pointer \+ integer can be simplified using unsafe.Add` +} + +type uP = unsafe.Pointer + +func _(ptr uP) uP { + return unsafe.Add(ptr, 1) // want `pointer \+ integer can be simplified using unsafe.Add` +} + +func _(ptr unsafe.Pointer, n int) unsafe.Pointer { + return unsafe.Add(ptr, n) // want `pointer \+ integer can be simplified using unsafe.Add` +} + +func _(ptr *byte, len int) *byte { + return (*byte)(unsafe.Add(unsafe.Pointer(ptr), len)) // want `pointer \+ integer can be simplified using unsafe.Add` +} + +type namedUP unsafe.Pointer + +func _(ptr namedUP) namedUP { + return namedUP(uintptr(ptr) + 1) // nope: Add does not accept named unsafe.Pointer types +} diff --git a/go/analysis/passes/modernize/unsafefuncs.go b/go/analysis/passes/modernize/unsafefuncs.go new file mode 100644 index 00000000000..d3549e7103d --- /dev/null +++ b/go/analysis/passes/modernize/unsafefuncs.go @@ -0,0 +1,210 @@ +// 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 modernize + +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" + "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" +) + +// TODO(adonovan): also support: +// +// func String(ptr *byte, len IntegerType) string +// func StringData(str string) *byte +// func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType +// func SliceData(slice []ArbitraryType) *ArbitraryType + +var unsafeFuncsAnalyzer = &analysis.Analyzer{ + Name: "unsafefuncs", + Doc: analyzerutil.MustExtractDoc(doc, "unsafefuncs"), + Requires: []*analysis.Analyzer{inspect.Analyzer}, + Run: unsafefuncs, + URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#unsafefuncs", +} + +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, + // but let's ignore that.) + if !typesinternal.Imports(pass.Pkg, "unsafe") { + return nil, nil + } + + var ( + inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + info = pass.TypesInfo + tUnsafePointer = types.Typ[types.UnsafePointer] + ) + + isInteger := func(t types.Type) bool { + basic, ok := t.Underlying().(*types.Basic) + return ok && basic.Info()&types.IsInteger != 0 + } + + // isConversion reports whether e is a conversion T(x). + // If so, it returns T and x. + isConversion := func(curExpr inspector.Cursor) (t types.Type, x inspector.Cursor) { + e := curExpr.Node().(ast.Expr) + if conv, ok := ast.Unparen(e).(*ast.CallExpr); ok && len(conv.Args) == 1 { + if tv := pass.TypesInfo.Types[conv.Fun]; tv.IsType() { + return tv.Type, curExpr.ChildAt(edge.CallExpr_Args, 0) + } + } + return + } + + // The general form is where ptr and the result are of type unsafe.Pointer: + // + // unsafe.Pointer(uintptr(ptr) + uintptr(n)) + // => + // unsafe.Add(ptr, n) + + // Search for 'unsafe.Pointer(uintptr + uintptr)' + // where the left operand was converted from a pointer. + // + // (Start from sum, not conversion, as it is not + // uncommon to use a local type alias for unsafe.Pointer.) + for curSum := range inspect.Root().Preorder((*ast.BinaryExpr)(nil)) { + if sum, ok := curSum.Node().(*ast.BinaryExpr); ok && + sum.Op == token.ADD && + types.Identical(info.TypeOf(sum.X), types.Typ[types.Uintptr]) && + astutil.IsChildOf(curSum, edge.CallExpr_Args) { + // Have: sum ≡ T(x:...uintptr... + y:...uintptr...) + curX := curSum.ChildAt(edge.BinaryExpr_X, -1) + curY := curSum.ChildAt(edge.BinaryExpr_Y, -1) + + // Is sum converted to unsafe.Pointer? + curResult := curSum.Parent() + if t, _ := isConversion(curResult); !(t != nil && types.Identical(t, tUnsafePointer)) { + continue + } + // Have: result ≡ unsafe.Pointer(x:...uintptr... + y:...uintptr...) + + // Is sum.x converted from unsafe.Pointer? + _, curPtr := isConversion(curX) + if !astutil.CursorValid(curPtr) { + continue + } + ptr := curPtr.Node().(ast.Expr) + if !types.Identical(info.TypeOf(ptr), tUnsafePointer) { + continue + } + // Have: result ≡ unsafe.Pointer(x:uintptr(...unsafe.Pointer...) + y:...uintptr...) + + file := astutil.EnclosingFile(curSum) + if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_17) { + continue // unsafe.Add not available in this file + } + + // import "unsafe" + unsafedot, edits := refactor.AddImport(info, file, "unsafe", "unsafe", "Add", sum.Pos()) + + // unsafe.Pointer(x + y) + // --------------- - + // x + y + edits = append(edits, deleteConv(curResult)...) + + // uintptr (ptr) + offset + // ----------- ---- - + // unsafe.Add(ptr, offset) + edits = append(edits, []analysis.TextEdit{ + { + Pos: sum.Pos(), + End: ptr.Pos(), + NewText: fmt.Appendf(nil, "%sAdd(", unsafedot), + }, + { + Pos: ptr.End(), + End: sum.Y.Pos(), + NewText: []byte(", "), + }, + { + Pos: sum.Y.End(), + End: sum.Y.End(), + NewText: []byte(")"), + }, + }...) + + // Variant: sum.y operand was converted from another integer type. + // Discard the conversion, as Add is generic over integers. + // + // e.g. unsafe.Pointer(uintptr(ptr) + uintptr(len(s))) + // -------- - + // unsafe.Add ( ptr, len(s)) + if t, _ := isConversion(curY); t != nil && isInteger(t) { + edits = append(edits, deleteConv(curY)...) + } + + pass.Report(analysis.Diagnostic{ + Pos: sum.Pos(), + End: sum.End(), + Message: "pointer + integer can be simplified using unsafe.Add", + SuggestedFixes: []analysis.SuggestedFix{{ + Message: "Simplify pointer addition using unsafe.Add", + TextEdits: edits, + }}, + }) + } + } + + return nil, nil +} + +// deleteConv returns edits for changing T(x) to x, respecting precedence. +func deleteConv(cur inspector.Cursor) []analysis.TextEdit { + conv := cur.Node().(*ast.CallExpr) + + usesPrec := func(n ast.Node) bool { + switch n.(type) { + case *ast.BinaryExpr, *ast.UnaryExpr: + return true + } + return false + } + + // Be careful not to change precedence of e.g. T(1+2) * 3. + // TODO(adonovan): refine this. + if usesPrec(cur.Parent().Node()) && usesPrec(conv.Args[0]) { + // T(x+y) * z + // - + // (x+y) * z + return []analysis.TextEdit{{ + Pos: conv.Fun.Pos(), + End: conv.Fun.End(), + }} + } + + // T(x) + // -- - + // x + return []analysis.TextEdit{ + { + Pos: conv.Pos(), + End: conv.Args[0].Pos(), + }, + { + Pos: conv.Args[0].End(), + End: conv.End(), + }, + } +} diff --git a/go/types/typeutil/callee.go b/go/types/typeutil/callee.go index 5f10f56cbaf..3d24a8c6371 100644 --- a/go/types/typeutil/callee.go +++ b/go/types/typeutil/callee.go @@ -12,6 +12,7 @@ import ( // Callee returns the named target of a function call, if any: // a function, method, builtin, or variable. +// It returns nil for a T(x) conversion. // // Functions and methods may potentially have type parameters. // diff --git a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md index ddb4a1e0e93..685db2e39cb 100644 --- a/gopls/doc/analyzers.md +++ b/gopls/doc/analyzers.md @@ -4225,6 +4225,24 @@ Default: on. Package documentation: [unreachable](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unreachable) + +## `unsafefuncs`: replace unsafe pointer arithmetic with function calls + +The unsafefuncs analyzer simplifies pointer arithmetic expressions by replacing them with calls to helper functions such as unsafe.Add, added in Go 1.17. + +Example: + + unsafe.Pointer(uintptr(ptr) + uintptr(n)) + +where ptr is an unsafe.Pointer, is replaced by: + + unsafe.Add(ptr, n) + + +Default: on. + +Package documentation: [unsafefuncs](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#unsafefuncs) + ## `unsafeptr`: check for invalid conversions of uintptr to unsafe.Pointer diff --git a/gopls/internal/doc/api.json b/gopls/internal/doc/api.json index e4f72628c94..66253ec63cf 100644 --- a/gopls/internal/doc/api.json +++ b/gopls/internal/doc/api.json @@ -1766,6 +1766,12 @@ "Default": "true", "Status": "" }, + { + "Name": "\"unsafefuncs\"", + "Doc": "replace unsafe pointer arithmetic with function calls\n\nThe unsafefuncs analyzer simplifies pointer arithmetic expressions by\nreplacing them with calls to helper functions such as unsafe.Add,\nadded in Go 1.17.\n\nExample:\n\n\tunsafe.Pointer(uintptr(ptr) + uintptr(n))\n\nwhere ptr is an unsafe.Pointer, is replaced by:\n\n\tunsafe.Add(ptr, n)", + "Default": "true", + "Status": "" + }, { "Name": "\"unsafeptr\"", "Doc": "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.", @@ -3669,6 +3675,12 @@ "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unreachable", "Default": true }, + { + "Name": "unsafefuncs", + "Doc": "replace unsafe pointer arithmetic with function calls\n\nThe unsafefuncs analyzer simplifies pointer arithmetic expressions by\nreplacing them with calls to helper functions such as unsafe.Add,\nadded in Go 1.17.\n\nExample:\n\n\tunsafe.Pointer(uintptr(ptr) + uintptr(n))\n\nwhere ptr is an unsafe.Pointer, is replaced by:\n\n\tunsafe.Add(ptr, n)", + "URL": "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#unsafefuncs", + "Default": true + }, { "Name": "unsafeptr", "Doc": "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.", diff --git a/gopls/internal/settings/analysis.go b/gopls/internal/settings/analysis.go index c74e458ee6c..0e5749c10ec 100644 --- a/gopls/internal/settings/analysis.go +++ b/gopls/internal/settings/analysis.go @@ -271,6 +271,7 @@ var DefaultAnalyzers = []*Analyzer{ {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.WaitGroupAnalyzer, severity: protocol.SeverityHint}, // type-error analyzers diff --git a/internal/goplsexport/export.go b/internal/goplsexport/export.go index bca4d8a0b05..b0572f5968e 100644 --- a/internal/goplsexport/export.go +++ b/internal/goplsexport/export.go @@ -13,4 +13,5 @@ var ( StdIteratorsModernizer *analysis.Analyzer // = modernize.stditeratorsAnalyzer PlusBuildModernizer *analysis.Analyzer // = modernize.plusbuildAnalyzer StringsCutModernizer *analysis.Analyzer // = modernize.stringscutAnalyzer + UnsafeFuncsModernizer *analysis.Analyzer // = modernize.unsafeFuncsAnalyzer ) diff --git a/internal/versions/features.go b/internal/versions/features.go index a5f4e3252cc..cdd36c388ab 100644 --- a/internal/versions/features.go +++ b/internal/versions/features.go @@ -9,6 +9,7 @@ package versions // named constants, to avoid misspelling const ( + Go1_17 = "go1.17" Go1_18 = "go1.18" Go1_19 = "go1.19" Go1_20 = "go1.20" From 61df39e1228be707d85a75ad30070f9638f04da1 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 21 Nov 2025 09:51:55 -0500 Subject: [PATCH 51/60] go/ast/astutil: update BasicLit.ValueEnd if present This is a putative fix; I haven't been able to reproduce the problem locally yet. Fixes golang/go#76395 For golang/go#76031 Change-Id: I2fe3d96e60629fd360563021e42acc5b248d7ff2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/722840 Auto-Submit: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI --- go/ast/astutil/imports.go | 19 +++++++++++++++++-- internal/imports/sortimports.go | 23 +++++++++++++++++++---- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/go/ast/astutil/imports.go b/go/ast/astutil/imports.go index 5bacc0fa49e..e2790ce742a 100644 --- a/go/ast/astutil/imports.go +++ b/go/ast/astutil/imports.go @@ -9,6 +9,7 @@ import ( "fmt" "go/ast" "go/token" + "reflect" "slices" "strconv" "strings" @@ -149,7 +150,7 @@ func AddNamedImport(fset *token.FileSet, f *ast.File, name, path string) (added if newImport.Name != nil { newImport.Name.NamePos = pos } - newImport.Path.ValuePos = pos + updateBasicLitPos(newImport.Path, pos) newImport.EndPos = pos // Clean up parens. impDecl contains at least one spec. @@ -184,7 +185,7 @@ func AddNamedImport(fset *token.FileSet, f *ast.File, name, path string) (added first.Lparen = first.Pos() // Move the imports of the other import declaration to the first one. for _, spec := range gen.Specs { - spec.(*ast.ImportSpec).Path.ValuePos = first.Pos() + updateBasicLitPos(spec.(*ast.ImportSpec).Path, first.Pos()) first.Specs = append(first.Specs, spec) } f.Decls = slices.Delete(f.Decls, i, i+1) @@ -470,3 +471,17 @@ func Imports(fset *token.FileSet, f *ast.File) [][]*ast.ImportSpec { return groups } + +// setBasicLitValuePos updates lit.Pos, +// ensuring that lit.End (if set) is displaced by the same amount. +// (See https://go.dev/issue/76395.) +func updateBasicLitPos(lit *ast.BasicLit, pos token.Pos) { + len := lit.End() - lit.Pos() + lit.ValuePos = pos + // TODO(adonovan): after go1.26, simplify to: + // lit.ValueEnd = pos + len + v := reflect.ValueOf(lit).Elem().FieldByName("ValueEnd") + if v.IsValid() && v.Int() != 0 { + v.SetInt(int64(pos + len)) + } +} diff --git a/internal/imports/sortimports.go b/internal/imports/sortimports.go index 67c17bc4319..a09d95fd0f6 100644 --- a/internal/imports/sortimports.go +++ b/internal/imports/sortimports.go @@ -11,6 +11,7 @@ import ( "go/ast" "go/token" "log" + "reflect" "slices" "sort" "strconv" @@ -65,7 +66,7 @@ func sortImports(localPrefix string, tokFile *token.File, f *ast.File) { } // mergeImports merges all the import declarations into the first one. -// Taken from golang.org/x/tools/ast/astutil. +// Taken from golang.org/x/tools/go/ast/astutil. // This does not adjust line numbers properly func mergeImports(f *ast.File) { if len(f.Decls) <= 1 { @@ -89,7 +90,7 @@ func mergeImports(f *ast.File) { first.Lparen = first.Pos() // Move the imports of the other import declaration to the first one. for _, spec := range gen.Specs { - spec.(*ast.ImportSpec).Path.ValuePos = first.Pos() + updateBasicLitPos(spec.(*ast.ImportSpec).Path, first.Pos()) first.Specs = append(first.Specs, spec) } f.Decls = slices.Delete(f.Decls, i, i+1) @@ -98,7 +99,7 @@ func mergeImports(f *ast.File) { } // declImports reports whether gen contains an import of path. -// Taken from golang.org/x/tools/ast/astutil. +// Taken from golang.org/x/tools/go/ast/astutil. func declImports(gen *ast.GenDecl, path string) bool { if gen.Tok != token.IMPORT { return false @@ -221,7 +222,7 @@ func sortSpecs(localPrefix string, tokFile *token.File, f *ast.File, specs []ast if s.Name != nil { s.Name.NamePos = pos[i].Start } - s.Path.ValuePos = pos[i].Start + updateBasicLitPos(s.Path, pos[i].Start) s.EndPos = pos[i].End nextSpecPos := pos[i].End @@ -296,3 +297,17 @@ type byCommentPos []*ast.CommentGroup func (x byCommentPos) Len() int { return len(x) } func (x byCommentPos) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byCommentPos) Less(i, j int) bool { return x[i].Pos() < x[j].Pos() } + +// setBasicLitValuePos updates lit.Pos, +// ensuring that lit.End is displaced by the same amount. +// (See https://go.dev/issue/76395.) +func updateBasicLitPos(lit *ast.BasicLit, pos token.Pos) { + len := lit.End() - lit.Pos() + lit.ValuePos = pos + // TODO(adonovan): after go1.26, simplify to: + // lit.ValueEnd = pos + len + v := reflect.ValueOf(lit).Elem().FieldByName("ValueEnd") + if v.IsValid() { + v.SetInt(int64(pos + len)) + } +} From 8e819d2ae9820148bcca1d2a1168d8e5438052ea Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Wed, 3 Dec 2025 13:10:33 -0500 Subject: [PATCH 52/60] internal/refactor/inline: built-ins may affect inference This change fixes a bug that caused argument conversions to be neglected when inlining calls to functions whose body calls certain built-ins such as new: func f(x int64) *int64 { return new(x) } var _ *int64 = f(42) // => new(42), should be new(int64(42)) The solution is to consider built-in functions as well as ordinary non-method functions when computing whether the argument may affect type inference. This results in a slight style regression when dealing with complex numbers, but they are rare. + test Fixes golang/go#76287 Change-Id: I7bc763cd41a7ec39abf851cb25acd568477b658a Reviewed-on: https://go-review.googlesource.com/c/tools/+/726480 Commit-Queue: Alan Donovan Reviewed-by: Robert Findley LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- internal/refactor/inline/callee.go | 44 ++++++++++++++++++- internal/refactor/inline/falcon_test.go | 4 +- internal/refactor/inline/inline_test.go | 6 +++ .../refactor/inline/testdata/newexpr.txtar | 44 +++++++++++++++++++ 4 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 internal/refactor/inline/testdata/newexpr.txtar diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go index ce5beb27244..9a960bd293c 100644 --- a/internal/refactor/inline/callee.go +++ b/internal/refactor/inline/callee.go @@ -725,8 +725,48 @@ func analyzeAssignment(info *types.Info, stack []ast.Node) (assignable, ifaceAss paramType := paramTypeAtIndex(sig, call, i) ifaceAssign := paramType == nil || types.IsInterface(paramType) affectsInference := false - if fn := typeutil.StaticCallee(info, call); fn != nil { - if sig2 := fn.Type().(*types.Signature); sig2.Recv() == nil { + switch callee := typeutil.Callee(info, call).(type) { + case *types.Builtin: + // Consider this litmus test: + // + // func f(x int64) any { return max(x) } + // func main() { fmt.Printf("%T", f(42)) } + // + // If we lose the implicit conversion from untyped int + // to int64, the type inferred for the max(x) call changes, + // resulting in a different dynamic behavior: it prints + // int, not int64. + // + // Inferred result type affected: + // new + // complex, real, imag + // min, max + // + // Dynamic behavior change: + // append -- dynamic type of append([]any(nil), x)[0] + // delete(m, x) -- dynamic key type where m is map[any]unit + // panic -- dynamic type of panic value + // + // Unaffected: + // recover + // make + // len, cap + // clear + // close + // copy + // print, println -- only uses underlying types (?) + // + // The dynamic type cases are all covered by + // the ifaceAssign logic. + switch callee.Name() { + case "new", "complex", "real", "imag", "min", "max": + affectsInference = true + } + + case *types.Func: + // Only standalone (non-method) functions have type + // parameters affected by the call arguments. + if sig2 := callee.Signature(); sig2.Recv() == nil { originParamType := paramTypeAtIndex(sig2, call, i) affectsInference = originParamType == nil || new(typeparams.Free).Has(originParamType) } diff --git a/internal/refactor/inline/falcon_test.go b/internal/refactor/inline/falcon_test.go index 8a9bba8ec03..117fe32cff0 100644 --- a/internal/refactor/inline/falcon_test.go +++ b/internal/refactor/inline/falcon_test.go @@ -341,7 +341,9 @@ func TestFalconComplex(t *testing.T) { "Complex arithmetic (good).", `func f(re, im float64, z complex128) byte { return "x"[int(real(complex(re, im)*complex(re, -im)-z))] }`, `func _() { f(1, 2, 5+0i) }`, - `func _() { _ = "x"[int(real(complex(1, 2)*complex(1, -2)-(5+0i)))] }`, + // The float64 conversions are excessively conservative here + // but in general may affect the type of complex produced. + `func _() { _ = "x"[int(real(complex(float64(1), float64(2))*complex(float64(1), -2)-(5+0i)))] }`, }, { "Complex arithmetic (bad).", diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go index 797db4ac41f..0a1129a5a61 100644 --- a/internal/refactor/inline/inline_test.go +++ b/internal/refactor/inline/inline_test.go @@ -75,6 +75,12 @@ func TestData(t *testing.T) { testenv.NeedsTool(t, "cgo") } + // Some tests need specific Go versions. + // TODO(adonovan): remove when go1.26 is assured. + if strings.HasSuffix(t.Name(), "newexpr.txtar") { + testenv.NeedsGo1Point(t, 26) + } + // Extract archive to temporary tree. ar, err := txtar.ParseFile(file) if err != nil { diff --git a/internal/refactor/inline/testdata/newexpr.txtar b/internal/refactor/inline/testdata/newexpr.txtar new file mode 100644 index 00000000000..9911a30df3e --- /dev/null +++ b/internal/refactor/inline/testdata/newexpr.txtar @@ -0,0 +1,44 @@ +Regression test for https://go.dev/issue/76287: +ensure that inlining a function that wraps go1.26's new(expr) +correctly inserts a conversion as needed, just as it would for a +declared generic function equivalent to 'new'. + +- f uses a declared _new(expr) function. +- g uses the built-in new(expr) function. + +-- go.mod -- +module testdata +go 1.26 + +-- common.go -- +package p + +func _new[T any](x T) *T { return &x } + +-- f.go -- +package p + +func f(x int64) *int64 { return _new(x) } + +var _ *int64 = f(42) //@ inline(re"f", f) + +-- g.go -- +package p + +func g(x int64) *int64 { return new(x) } + +var _ *int64 = g(42) //@ inline(re"g", g) + +-- f -- +package p + +func f(x int64) *int64 { return _new(x) } + +var _ *int64 = _new(int64(42)) //@ inline(re"f", f) + +-- g -- +package p + +func g(x int64) *int64 { return new(x) } + +var _ *int64 = new(int64(42)) //@ inline(re"g", g) From 880ed70f69c11115ed2eea9fea4ee17fda5721ad Mon Sep 17 00:00:00 2001 From: Hongxiang Jiang Date: Wed, 3 Dec 2025 15:22:32 +0800 Subject: [PATCH 53/60] gopls/internal/golang: add util function NarrowestDeclaringPackage In complicated static analysis, gopls sometimes need to type check the second package which contains the decl of an interested obj. This utility function help gopls to easily type check the second package which contains the decl of given object. Change-Id: I96d5dd72661dbcdda611c44490bba096819dc6f2 Reviewed-on: https://go-review.googlesource.com/c/tools/+/726180 Auto-Submit: Hongxiang Jiang Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan --- gopls/internal/golang/call_hierarchy.go | 20 +------------------- gopls/internal/golang/hover.go | 16 ++++------------ gopls/internal/golang/inline.go | 19 +++++++++---------- gopls/internal/golang/snapshot.go | 24 ++++++++++++++++++++++++ 4 files changed, 38 insertions(+), 41 deletions(-) diff --git a/gopls/internal/golang/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go index 194935a2cd2..a36a304e284 100644 --- a/gopls/internal/golang/call_hierarchy.go +++ b/gopls/internal/golang/call_hierarchy.go @@ -17,9 +17,7 @@ import ( "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/file" "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/util/bug" "golang.org/x/tools/gopls/internal/util/moremaps" - "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/typesinternal" @@ -203,23 +201,7 @@ func OutgoingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle return nil, nil // built-in functions have no outgoing calls } - declFile := pkg.FileSet().File(obj.Pos()) - if declFile == nil { - return nil, bug.Errorf("file not found for %d", obj.Pos()) - } - - uri := protocol.URIFromPath(declFile.Name()) - offset, err := safetoken.Offset(declFile, obj.Pos()) - if err != nil { - return nil, err - } - - declPkg, declPGF, err := NarrowestPackageForFile(ctx, snapshot, uri) - if err != nil { - return nil, err - } - - declPos, err := safetoken.Pos(declPGF.Tok, offset) + declPkg, declPGF, declPos, err := NarrowestDeclaringPackage(ctx, snapshot, pkg, obj) if err != nil { return nil, err } diff --git a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go index a9e9b0c1085..c96b5591aae 100644 --- a/gopls/internal/golang/hover.go +++ b/gopls/internal/golang/hover.go @@ -173,7 +173,6 @@ func findRhsTypeDecl(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.P // hover information. // // TODO(adonovan): strength-reduce file.Handle to protocol.DocumentURI. -// TODO(hxjiang): return the type of selected expression based on the range. func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) (protocol.Range, *hoverResult, error) { // Check for hover inside the builtin file before attempting type checking // below. NarrowestPackageForFile may or may not succeed, depending on @@ -274,15 +273,14 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng pr // Find position in declaring file. hoverRange = &rng - objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) + var pos token.Pos // Subtle: updates pkg, which defines the FileSet used to resolve (start, end), // which were obtained from pkg. - pkg, pgf, err = NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(objURI.Filename)) + pkg, pgf, pos, err = NarrowestDeclaringPackage(ctx, snapshot, pkg, obj) if err != nil { return protocol.Range{}, nil, err } - pos := pgf.Tok.Pos(objURI.Offset) posRange = astutil.RangeOf(pos, pos) } @@ -394,21 +392,15 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng pr } // For all other objects, load type information for their declaring package - // in order to correctly compute their documentation, signature, and link. + // in order to correctly compute their documentation, signature, and link. // // Beware: positions derived from decl{Obj,Pkg,PGF,Pos} should be resolve // using declPkg.FileSet; other positions should use pkg.FileSet(). - objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) - - // TODO(hxjiang): create a helper function NarrowestDeclaringPackage - // which type-checks a second package that declares a symbol of interest - // found in current package. - declPkg, declPGF, err := NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(objURI.Filename)) + declPkg, declPGF, declPos, err := NarrowestDeclaringPackage(ctx, snapshot, pkg, obj) if err != nil { return protocol.Range{}, nil, err } - declPos := declPGF.Tok.Pos(objURI.Offset) decl, spec, field := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^3 var docText string diff --git a/gopls/internal/golang/inline.go b/gopls/internal/golang/inline.go index 520d1992e37..66cd9ef3c92 100644 --- a/gopls/internal/golang/inline.go +++ b/gopls/internal/golang/inline.go @@ -20,7 +20,6 @@ import ( "golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/parsego" - "golang.org/x/tools/gopls/internal/protocol" "golang.org/x/tools/gopls/internal/util/safetoken" "golang.org/x/tools/internal/astutil" "golang.org/x/tools/internal/event" @@ -65,20 +64,20 @@ func inlineCall(ctx context.Context, snapshot *cache.Snapshot, callerPkg *cache. return nil, nil, err } - // Locate callee by file/line and analyze it. - calleePosn := safetoken.StartPosition(callerPkg.FileSet(), fn.Pos()) - calleePkg, calleePGF, err := NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(calleePosn.Filename)) + calleePkg, calleePGF, calleePos, err := NarrowestDeclaringPackage(ctx, snapshot, callerPkg, fn) if err != nil { return nil, nil, err } + var calleeDecl *ast.FuncDecl for _, decl := range calleePGF.File.Decls { - if decl, ok := decl.(*ast.FuncDecl); ok { - posn := safetoken.StartPosition(calleePkg.FileSet(), decl.Name.Pos()) - if posn.Line == calleePosn.Line && posn.Column == calleePosn.Column { - calleeDecl = decl - break - } + funcDecl, ok := decl.(*ast.FuncDecl) + if !ok { + continue + } + if funcDecl.Name.Pos() == calleePos { + calleeDecl = funcDecl + break } } if calleeDecl == nil { diff --git a/gopls/internal/golang/snapshot.go b/gopls/internal/golang/snapshot.go index 0eb17227bdd..69258246ca3 100644 --- a/gopls/internal/golang/snapshot.go +++ b/gopls/internal/golang/snapshot.go @@ -7,11 +7,14 @@ package golang import ( "context" "fmt" + "go/token" + "go/types" "golang.org/x/tools/gopls/internal/cache" "golang.org/x/tools/gopls/internal/cache/metadata" "golang.org/x/tools/gopls/internal/cache/parsego" "golang.org/x/tools/gopls/internal/protocol" + "golang.org/x/tools/gopls/internal/util/safetoken" ) // NarrowestPackageForFile is a convenience function that selects the narrowest @@ -51,6 +54,27 @@ func WidestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri pro return selectPackageForFile(ctx, snapshot, uri, func(metas []*metadata.Package) *metadata.Package { return metas[len(metas)-1] }) } +// NarrowestDeclaringPackage facilitates cross-package analysis by "jumping" +// from the current package context to the package that declares the given object. +// +// This is essential when the object of interest is defined in a different +// package than the one currently being type-checked. +// +// This function performs the context switch: it locates the file declaring +// the object, loads its narrowest package (see [NarrowestPackageForFile]), +// and returns the type-checked result. +// +// It returns the new package, the file, and the object's position translated +// to be valid within the new package's file set. +func NarrowestDeclaringPackage(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, obj types.Object) (*cache.Package, *parsego.File, token.Pos, error) { + posn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) + pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(posn.Filename)) + if err != nil { + return nil, nil, token.NoPos, err + } + return pkg, pgf, pgf.Tok.Pos(posn.Offset), nil +} + func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, selector func([]*metadata.Package) *metadata.Package) (*cache.Package, *parsego.File, error) { mps, err := snapshot.MetadataForFile(ctx, uri, true) if err != nil { From 91192559255e5102e736db0c60faf84c03eb8337 Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Thu, 4 Dec 2025 10:54:20 -0500 Subject: [PATCH 54/60] go/analysis/passes/modernize: fix stringscut false positives This CL narrows the set of binary expressions (involving the index "i" returned by strings.Index) for which we suggest a stringscut modernization. Before, we determined if the binary expression establishes that index "i" is negative or non-negative. Now, we check that the condition is equivalent to i < 0 or i >= 0 by matching to an exact set of expressions. Suggesting a modernization when i is used in expressions like i > 1, or i > 0, may result in an invalid fix if the code intends to assert something about the location of the substring rather than just its existence. Fixes golang/go#76687 Change-Id: I598278a2d6751b1c2b2973099809c68d91832eb7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/726840 LUCI-TryBot-Result: Go LUCI Auto-Submit: Madeline Kalil Reviewed-by: Alan Donovan --- go/analysis/passes/modernize/stringscut.go | 128 ++++++++++-------- .../testdata/src/stringscut/stringscut.go | 27 ++++ .../src/stringscut/stringscut.go.golden | 27 ++++ 3 files changed, 123 insertions(+), 59 deletions(-) diff --git a/go/analysis/passes/modernize/stringscut.go b/go/analysis/passes/modernize/stringscut.go index bc4ad677cd0..954997ad6f1 100644 --- a/go/analysis/passes/modernize/stringscut.go +++ b/go/analysis/passes/modernize/stringscut.go @@ -50,9 +50,14 @@ func init() { // The following must hold for a replacement to occur: // // 1. All instances of i and s must be in one of these forms. -// Binary expressions: -// (a): establishing that i < 0: e.g.: i < 0, 0 > i, i == -1, -1 == i -// (b): establishing that i > -1: e.g.: i >= 0, 0 <= i, i == 0, 0 == i +// +// Binary expressions must be inequalities equivalent to +// "Index failed" (e.g. i < 0) or "Index succeeded" (i >= 0), +// or identities such as these (and their negations): +// +// 0 > i (flips left and right) +// i <= -1, -1 >= i (replace strict inequality by non-strict) +// i == -1, -1 == i (Index() guarantees i < 0 => i == -1) // // Slice expressions: // a: s[:i], s[0:i] @@ -86,9 +91,9 @@ func init() { // use(before, after) // } // -// If the condition involving `i` establishes that i > -1, then we replace it with -// `if ok“. Variants listed above include i >= 0, i > 0, and i == 0. -// If the condition is negated (e.g. establishes `i < 0`), we use `if !ok` instead. +// If the condition involving `i` is equivalent to i >= 0, then we replace it with +// `if ok“. +// If the condition is negated (e.g. equivalent to `i < 0`), we use `if !ok` instead. // If the slices of `s` match `s[:i]` or `s[i+len(substr):]` or their variants listed above, // then we replace them with before and after. // @@ -178,16 +183,16 @@ func stringscut(pass *analysis.Pass) (any, error) { // len(substr)]), then we can replace the call to Index() // with a call to Cut() and use the returned ok, before, // and after variables accordingly. - lessZero, greaterNegOne, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr) + negative, nonnegative, beforeSlice, afterSlice := checkIdxUses(pass.TypesInfo, index.Uses(iObj), s, substr) // Either there are no uses of before, after, or ok, or some use // of i does not match our criteria - don't suggest a fix. - if lessZero == nil && greaterNegOne == nil && beforeSlice == nil && afterSlice == nil { + if negative == nil && nonnegative == nil && beforeSlice == nil && afterSlice == nil { continue } // If the only uses are ok and !ok, don't suggest a Cut() fix - these should be using Contains() - isContains := (len(lessZero) > 0 || len(greaterNegOne) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0 + isContains := (len(negative) > 0 || len(nonnegative) > 0) && len(beforeSlice) == 0 && len(afterSlice) == 0 scope := iObj.Parent() var ( @@ -200,7 +205,7 @@ func stringscut(pass *analysis.Pass) (any, error) { // If there will be no uses of ok, before, or after, use the // blank identifier instead. - if len(lessZero) == 0 && len(greaterNegOne) == 0 { + if len(negative) == 0 && len(nonnegative) == 0 { okVarName = "_" } if len(beforeSlice) == 0 { @@ -226,8 +231,8 @@ func stringscut(pass *analysis.Pass) (any, error) { replacedFunc := "Cut" if isContains { replacedFunc = "Contains" - replace(lessZero, "!"+foundVarName) // idx < 0 -> !found - replace(greaterNegOne, foundVarName) // idx > -1 -> found + replace(negative, "!"+foundVarName) // idx < 0 -> !found + replace(nonnegative, foundVarName) // idx > -1 -> found // Replace the assignment with found, and replace the call to // Index or IndexByte with a call to Contains. @@ -244,8 +249,8 @@ func stringscut(pass *analysis.Pass) (any, error) { NewText: []byte("Contains"), }) } else { - replace(lessZero, "!"+okVarName) // idx < 0 -> !ok - replace(greaterNegOne, okVarName) // idx > -1 -> ok + replace(negative, "!"+okVarName) // idx < 0 -> !ok + replace(nonnegative, okVarName) // idx > -1 -> ok replace(beforeSlice, beforeVarName) // s[:idx] -> before replace(afterSlice, afterVarName) // s[idx+k:] -> after @@ -364,11 +369,11 @@ func indexArgValid(info *types.Info, index *typeindex.Index, expr ast.Expr, afte // one of the following four valid formats, it returns a list of occurrences for // each format. If any of the uses do not match one of the formats, return nil // for all values, since we should not offer a replacement. -// 1. lessZero - a condition involving i establishing that i is negative (e.g. i < 0, 0 > i, i == -1, -1 == i) -// 2. greaterNegOne - a condition involving i establishing that i is non-negative (e.g. i >= 0, 0 <= i, i == 0, 0 == i) +// 1. negative - a condition equivalent to i < 0 +// 2. nonnegative - a condition equivalent to i >= 0 // 3. beforeSlice - a slice of `s` that matches either s[:i], s[0:i] // 4. afterSlice - a slice of `s` that matches one of: s[i+len(substr):], s[len(substr) + i:], s[i + const], s[k + i] (where k = len(substr)) -func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr) (lessZero, greaterNegOne, beforeSlice, afterSlice []ast.Expr) { +func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr ast.Expr) (negative, nonnegative, beforeSlice, afterSlice []ast.Expr) { use := func(cur inspector.Cursor) bool { ek, _ := cur.ParentEdge() n := cur.Parent().Node() @@ -377,13 +382,13 @@ func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr a check := n.(*ast.BinaryExpr) switch checkIdxComparison(info, check) { case -1: - lessZero = append(lessZero, check) + negative = append(negative, check) return true case 1: - greaterNegOne = append(greaterNegOne, check) + nonnegative = append(nonnegative, check) return true } - // Check does not establish that i < 0 or i > -1. + // Check is not equivalent to that i < 0 or i >= 0. // Might be part of an outer slice expression like s[i + k] // which requires a different check. // Check that the thing being sliced is s and that the slice @@ -421,7 +426,7 @@ func checkIdxUses(info *types.Info, uses iter.Seq[inspector.Cursor], s, substr a return nil, nil, nil, nil } } - return lessZero, greaterNegOne, beforeSlice, afterSlice + return negative, nonnegative, beforeSlice, afterSlice } // hasModifyingUses reports whether any of the uses involve potential @@ -451,52 +456,57 @@ func hasModifyingUses(info *types.Info, uses iter.Seq[inspector.Cursor], afterPo return false } -// checkIdxComparison reports whether the check establishes that i is negative -// or non-negative. It returns -1 in the first case, 1 in the second, and 0 if -// we can confirm neither condition. We assume that a check passed to -// checkIdxComparison has i as one of its operands. +// checkIdxComparison reports whether the check is equivalent to i < 0 or its negation, or neither. +// For equivalent to i >= 0, we only accept this exact BinaryExpr since +// expressions like i > 0 or i >= 1 make a stronger statement about the value of i. +// We avoid suggesting a fix in this case since it may result in an invalid +// transformation (See golang/go#76687). +// Since strings.Index returns exactly -1 if the substring is not found, we +// don't need to handle expressions like i <= -3. +// We return 0 if the expression does not match any of these options. +// We assume that a check passed to checkIdxComparison has i as one of its operands. func checkIdxComparison(info *types.Info, check *ast.BinaryExpr) int { - // Check establishes that i is negative. - // e.g.: i < 0, 0 > i, i == -1, -1 == i - if check.Op == token.LSS && (isNegativeConst(info, check.Y) || isZeroIntConst(info, check.Y)) || //i < (0 or neg) - check.Op == token.GTR && (isNegativeConst(info, check.X) || isZeroIntConst(info, check.X)) || // (0 or neg) > i - check.Op == token.LEQ && (isNegativeConst(info, check.Y)) || //i <= (neg) - check.Op == token.GEQ && (isNegativeConst(info, check.X)) || // (neg) >= i - check.Op == token.EQL && - (isNegativeConst(info, check.X) || isNegativeConst(info, check.Y)) { // i == neg; neg == i - return -1 + // Ensure that the constant (if any) is on the right. + x, op, y := check.X, check.Op, check.Y + if info.Types[x].Value != nil { + x, op, y = y, flip(op), x } - // Check establishes that i is non-negative. - // e.g.: i >= 0, 0 <= i, i == 0, 0 == i - if check.Op == token.GTR && (isNonNegativeConst(info, check.Y) || isIntLiteral(info, check.Y, -1)) || // i > (non-neg or -1) - check.Op == token.LSS && (isNonNegativeConst(info, check.X) || isIntLiteral(info, check.X, -1)) || // (non-neg or -1) < i - check.Op == token.GEQ && isNonNegativeConst(info, check.Y) || // i >= (non-neg) - check.Op == token.LEQ && isNonNegativeConst(info, check.X) || // (non-neg) <= i - check.Op == token.EQL && - (isNonNegativeConst(info, check.X) || isNonNegativeConst(info, check.Y)) { // i == non-neg; non-neg == i - return 1 + + yIsInt := func(k int64) bool { + return isIntLiteral(info, y, k) } - return 0 -} -// isNegativeConst returns true if the expr is a const int with value < zero. -func isNegativeConst(info *types.Info, expr ast.Expr) bool { - if tv, ok := info.Types[expr]; ok && tv.Value != nil && tv.Value.Kind() == constant.Int { - if v, ok := constant.Int64Val(tv.Value); ok { - return v < 0 - } + if op == token.LSS && yIsInt(0) || // i < 0 + op == token.EQL && yIsInt(-1) || // i == -1 + op == token.LEQ && yIsInt(-1) { // i <= -1 + return -1 // check <=> i is negative } - return false + + if op == token.GEQ && yIsInt(0) || // i >= 0 + op == token.NEQ && yIsInt(-1) || // i != -1 + op == token.GTR && yIsInt(-1) { // i > -1 + return +1 // check <=> i is non-negative + } + + return 0 // unknown } -// isNonNegativeConst returns true if the expr is a const int with value >= zero. -func isNonNegativeConst(info *types.Info, expr ast.Expr) bool { - if tv, ok := info.Types[expr]; ok && tv.Value != nil && tv.Value.Kind() == constant.Int { - if v, ok := constant.Int64Val(tv.Value); ok { - return v >= 0 - } +// flip changes the comparison token as if the operands were flipped. +// It is defined only for == and the four inequalities. +func flip(op token.Token) token.Token { + switch op { + case token.EQL: + return token.EQL // (same) + case token.GEQ: + return token.LEQ + case token.GTR: + return token.LSS + case token.LEQ: + return token.GEQ + case token.LSS: + return token.GTR } - return false + return op } // isBeforeSlice reports whether the SliceExpr is of the form s[:i] or s[0:i]. diff --git a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go index 85fb82a9611..0390eab3009 100644 --- a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go +++ b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go @@ -262,6 +262,33 @@ func idx_used_other_substr(s string, sub string) string { } } +func idx_gtr_zero_invalid(s string, sub string) string { + i := strings.Index(s, sub) + if i > 0 { // don't modernize since this is a stronger claim than i >= 0 + return s[:i] + } + return "" +} + +func idx_gtreq_one_invalid(s string, sub string) string { + i := strings.Index(s, sub) + if i >= 1 { // don't modernize since this is a stronger claim than i >= 0 + return s[:i] + } + return "" +} + +func idx_gtr_negone(s string, sub string) string { + i := strings.Index(s, sub) // want "strings.Index can be simplified using strings.Cut" + if i > -1 { + return s[:i] + } + if i != -1 { + return s + } + return "" +} + func function(s string) {} func reference_str(s *string) {} diff --git a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden index c3ddd699bbd..a308fb22d7b 100644 --- a/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden +++ b/go/analysis/passes/modernize/testdata/src/stringscut/stringscut.go.golden @@ -262,6 +262,33 @@ func idx_used_other_substr(s string, sub string) string { } } +func idx_gtr_zero_invalid(s string, sub string) string { + i := strings.Index(s, sub) + if i > 0 { // don't modernize since this is a stronger claim than i >= 0 + return s[:i] + } + return "" +} + +func idx_gtreq_one_invalid(s string, sub string) string { + i := strings.Index(s, sub) + if i >= 1 { // don't modernize since this is a stronger claim than i >= 0 + return s[:i] + } + return "" +} + +func idx_gtr_negone(s string, sub string) string { + before, _, ok := strings.Cut(s, sub) // want "strings.Index can be simplified using strings.Cut" + if ok { + return before + } + if ok { + return s + } + return "" +} + func function(s string) {} func reference_str(s *string) {} From e2dd41635db76ec780a3e9fd537c7a2c25edf210 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Thu, 16 Oct 2025 21:20:39 -0400 Subject: [PATCH 55/60] go/analysis/unitchecker: write fixed files to an archive The unitchecker must not fix files directly as they may still be needed by the ongoing go fix "build". This change causes it to write fixes to a zip archive (when configured to do so). The go {vet,fix} command will read the archives and apply the fixes once the build is complete. For golang/go#71859 Change-Id: I377ccb3c7203325724c5dad0595d428d8162467a Reviewed-on: https://go-review.googlesource.com/c/tools/+/726940 LUCI-TryBot-Result: Go LUCI Auto-Submit: Alan Donovan Reviewed-by: Robert Findley --- go/analysis/internal/checker/checker.go | 5 ++- go/analysis/unitchecker/unitchecker.go | 41 +++++++++++++++++++++++-- internal/analysis/driverutil/fix.go | 13 ++++---- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/go/analysis/internal/checker/checker.go b/go/analysis/internal/checker/checker.go index d8c5a37d932..95ca57368c5 100644 --- a/go/analysis/internal/checker/checker.go +++ b/go/analysis/internal/checker/checker.go @@ -205,7 +205,10 @@ func Run(args []string, analyzers []*analysis.Analyzer) (exitcode int) { } } } - if err := driverutil.ApplyFixes(fixActions, analysisflags.Diff, dbg('v')); err != nil { + write := func(filename string, content []byte) error { + return os.WriteFile(filename, content, 0644) + } + if err := driverutil.ApplyFixes(fixActions, write, analysisflags.Diff, dbg('v')); err != nil { // Fail when applying fixes failed. log.Print(err) exitAtLeast(1) diff --git a/go/analysis/unitchecker/unitchecker.go b/go/analysis/unitchecker/unitchecker.go index 0180a341e56..bc15ef8b968 100644 --- a/go/analysis/unitchecker/unitchecker.go +++ b/go/analysis/unitchecker/unitchecker.go @@ -27,6 +27,7 @@ package unitchecker // printf checker. import ( + "archive/zip" "encoding/gob" "encoding/json" "flag" @@ -74,6 +75,7 @@ type Config struct { VetxOnly bool // run analysis only for facts, not diagnostics VetxOutput string // where to write file of fact information Stdout string // write stdout (e.g. JSON, unified diff) to this file + FixArchive string // write fixed files to this zip archive, if non-empty SucceedOnTypecheckFailure bool // obsolete awful hack; see #18395 and below } @@ -153,7 +155,7 @@ func Run(configFile string, analyzers []*analysis.Analyzer) { // In VetxOnly mode, the analysis is run only for facts. if !cfg.VetxOnly { - code = processResults(fset, cfg.ID, results) + code = processResults(fset, cfg.ID, cfg.FixArchive, results) } os.Exit(code) @@ -177,7 +179,7 @@ func readConfig(filename string) (*Config, error) { return cfg, nil } -func processResults(fset *token.FileSet, id string, results []result) (exit int) { +func processResults(fset *token.FileSet, id, fixArchive string, results []result) (exit int) { if analysisflags.Fix { // Don't print the diagnostics, // but apply all fixes from the root actions. @@ -194,7 +196,40 @@ func processResults(fset *token.FileSet, id string, results []result) (exit int) Diagnostics: res.diagnostics, } } - if err := driverutil.ApplyFixes(fixActions, analysisflags.Diff, false); err != nil { + + // By default, fixes overwrite the original file. + // With the -diff flag, print the diffs to stdout. + // If "go fix" provides a fix archive, we write files + // into it so that mutations happen after the build. + write := func(filename string, content []byte) error { + return os.WriteFile(filename, content, 0644) + } + if fixArchive != "" { + f, err := os.Create(fixArchive) + if err != nil { + log.Fatalf("can't create -fix archive: %v", err) + } + zw := zip.NewWriter(f) + zw.SetComment(id) // ignore error + defer func() { + if err := zw.Close(); err != nil { + log.Fatalf("closing -fix archive zip writer: %v", err) + } + if err := f.Close(); err != nil { + log.Fatalf("closing -fix archive file: %v", err) + } + }() + write = func(filename string, content []byte) error { + f, err := zw.Create(filename) + if err != nil { + return err + } + _, err = f.Write(content) + return err + } + } + + if err := driverutil.ApplyFixes(fixActions, write, analysisflags.Diff, false); err != nil { // Fail when applying fixes failed. log.Print(err) exit = 1 diff --git a/internal/analysis/driverutil/fix.go b/internal/analysis/driverutil/fix.go index 37b09588a7a..7769b39beb8 100644 --- a/internal/analysis/driverutil/fix.go +++ b/internal/analysis/driverutil/fix.go @@ -93,11 +93,13 @@ type FixAction struct { // // If printDiff (from the -diff flag) is set, instead of updating the // files it display the final patch composed of all the cleanly merged -// fixes. +// fixes. (It is tempting to factor printDiff as just a variant of +// writeFile that is provided the old and new content, but it's hard +// to generate a good summary that way.) // // TODO(adonovan): handle file-system level aliases such as symbolic // links using robustio.FileID. -func ApplyFixes(actions []FixAction, printDiff, verbose bool) error { +func ApplyFixes(actions []FixAction, writeFile func(filename string, content []byte) error, printDiff, verbose bool) error { generated := make(map[*token.File]bool) // Select fixes to apply. @@ -264,12 +266,11 @@ fixloop: os.Stdout.WriteString(unified) } else { - // write + // write file totalFiles++ - // TODO(adonovan): abstract the I/O. - if err := os.WriteFile(file, final, 0644); err != nil { + if err := writeFile(file, final); err != nil { log.Println(err) - continue + continue // (causes ApplyFix to return an error) } filesUpdated++ } From e5a08637a57b68119e771bf855e5bbbaeb37e8c2 Mon Sep 17 00:00:00 2001 From: Madeline Kalil Date: Thu, 4 Dec 2025 17:44:37 -0500 Subject: [PATCH 56/60] go/analysis/passes/modernize: disable reflecttypefor modernizer for unnamed struct/interface When a type contains an unnamed struct or an unnamed interface, we should not suggest a fix because it will involve writing out the entire type and will be more verbose than the existing code. Fixes golang/go#76698 Change-Id: I5fdeb8dc5b7e70b2201fdf1fe714335aef439cc7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/726941 Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- go/analysis/passes/modernize/reflect.go | 57 ++++++++++++++++++ .../src/reflecttypefor/reflecttypefor.go | 58 +++++++++++++++++++ .../reflecttypefor/reflecttypefor.go.golden | 58 +++++++++++++++++++ 3 files changed, 173 insertions(+) diff --git a/go/analysis/passes/modernize/reflect.go b/go/analysis/passes/modernize/reflect.go index 0fc781813f6..2611f6cf090 100644 --- a/go/analysis/passes/modernize/reflect.go +++ b/go/analysis/passes/modernize/reflect.go @@ -102,6 +102,23 @@ func reflecttypefor(pass *analysis.Pass) (any, error) { continue // e.g. reflect was dot-imported } + // Don't offer a fix if the type contains an unnamed struct or unnamed + // interface because the replacement would be significantly more verbose. + // (See golang/go#76698) + if isComplicatedType(t) { + continue + } + + // Don't offer the fix if the type string is too long. We define "too + // long" as more than three times the length of the original expression + // and at least 16 characters (a 3x length increase of a very + // short expression should not be cause for skipping the fix). + oldLen := int(expr.End() - expr.Pos()) + newLen := len(tstr) + if newLen >= 16 && newLen > 3*oldLen { + continue + } + // If the call argument contains the last use // of a variable, as in: // var zero T @@ -137,3 +154,43 @@ func reflecttypefor(pass *analysis.Pass) (any, error) { return nil, nil } + +// 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 { + var check func(typ types.Type) bool + check = func(typ types.Type) bool { + switch t := typ.(type) { + case typesinternal.NamedOrAlias: + for ta := range t.TypeArgs().Types() { + if check(ta) { + return true + } + } + return false + case *types.Struct, *types.Interface, *types.Signature: + // These are complex types with potentially many elements + // so we should avoid duplicating their definition. + return true + case *types.Pointer: + return check(t.Elem()) + case *types.Slice: + return check(t.Elem()) + case *types.Array: + return check(t.Elem()) + case *types.Chan: + return check(t.Elem()) + case *types.Map: + return check(t.Key()) || check(t.Elem()) + case *types.Basic: + return false + case *types.TypeParam: + return false + default: + // Includes types.Union + return true + } + } + + return check(t) +} diff --git a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go index ee97a18f682..5e1a351e618 100644 --- a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go +++ b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go @@ -6,8 +6,14 @@ import ( "time" ) +type A string + +type B[T any] int + 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(uint(0)) // want "reflect.TypeOf call can be simplified using TypeFor" @@ -18,6 +24,8 @@ var ( _ = 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" ) // Eliminate local var if we deleted its last use. @@ -29,3 +37,53 @@ func _() { _ = reflect.TypeOf(z2) // want "reflect.TypeOf call can be simplified using TypeFor" _ = z2 // z2 has multiple uses } + +type T struct { + f struct { + A bool + B int + C string + } +} + +type S struct { + f [2]struct { + A bool + B int + C string + } +} + +type R []struct { + A int +} + +type M[T struct{ F int }] int + +type P struct { + f interface { + } + g func() // fine length + + long func(a int, b int, c int) (bool, string, int) // too long + + s func(a struct{}) + + q func() struct{} +} + +func f(t *T, r R, m *M[struct{ F int }], s *S, p *P) { + // No suggested fix for all of the following because the type is complicated -- e.g. has an unnamed struct, + // interface, or signature -- so the fix would be more verbose than the original expression. + // Also because structs and interfaces often acquire new fields and methods, and the type string + // produced by this modernizer won't get updated automatically, potentially causing a bug. + _ = reflect.TypeOf(&t.f) + _ = reflect.TypeOf(r[0]) + _ = reflect.TypeOf(m) + _ = reflect.TypeOf(&s.f) + _ = reflect.TypeOf(&p.f) + _ = reflect.TypeOf(&p.g) + _ = reflect.TypeOf(&p.long) + _ = reflect.TypeOf(&p.q) + _ = reflect.TypeOf(&p.s) +} 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 2f8f35add80..7256f964a0c 100644 --- a/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden +++ b/go/analysis/passes/modernize/testdata/src/reflecttypefor/reflecttypefor.go.golden @@ -6,8 +6,14 @@ import ( "time" ) +type A string + +type B[T any] int + 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.TypeFor[uint]() // want "reflect.TypeOf call can be simplified using TypeFor" @@ -18,6 +24,8 @@ var ( _ = 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" ) // Eliminate local var if we deleted its last use. @@ -28,3 +36,53 @@ func _() { _ = reflect.TypeFor[string]() // want "reflect.TypeOf call can be simplified using TypeFor" _ = z2 // z2 has multiple uses } + +type T struct { + f struct { + A bool + B int + C string + } +} + +type S struct { + f [2]struct { + A bool + B int + C string + } +} + +type R []struct { + A int +} + +type M[T struct{ F int }] int + +type P struct { + f interface { + } + g func() // fine length + + long func(a int, b int, c int) (bool, string, int) // too long + + s func(a struct{}) + + q func() struct{} +} + +func f(t *T, r R, m *M[struct{ F int }], s *S, p *P) { + // No suggested fix for all of the following because the type is complicated -- e.g. has an unnamed struct, + // interface, or signature -- so the fix would be more verbose than the original expression. + // Also because structs and interfaces often acquire new fields and methods, and the type string + // produced by this modernizer won't get updated automatically, potentially causing a bug. + _ = reflect.TypeOf(&t.f) + _ = reflect.TypeOf(r[0]) + _ = reflect.TypeOf(m) + _ = reflect.TypeOf(&s.f) + _ = reflect.TypeOf(&p.f) + _ = reflect.TypeOf(&p.g) + _ = reflect.TypeOf(&p.long) + _ = reflect.TypeOf(&p.q) + _ = reflect.TypeOf(&p.s) +} From ebdeef31ea22db1bf3ba3a0eab59418baa3c07f0 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 5 Dec 2025 13:30:39 -0500 Subject: [PATCH 57/60] gopls/doc/release/v0.21.0.md: update relnotes for RC2 For golang/go#76367 Change-Id: I5f313f1880f1ae35c52714362778ed1244ccb862 Reviewed-on: https://go-review.googlesource.com/c/tools/+/727320 Auto-Submit: Alan Donovan Reviewed-by: Madeline Kalil LUCI-TryBot-Result: Go LUCI --- gopls/doc/release/v0.21.0.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/gopls/doc/release/v0.21.0.md b/gopls/doc/release/v0.21.0.md index c84d795f382..611937eaa8d 100644 --- a/gopls/doc/release/v0.21.0.md +++ b/gopls/doc/release/v0.21.0.md @@ -1,5 +1,5 @@ --- -title: "Gopls release v0.21.0 (expected Nov 2025)" +title: "Gopls release v0.21.0 (expected Dec 2025)" --- ## Configuration changes @@ -149,6 +149,19 @@ by the more modern `errors.AsType`, when the type is known statically: if myErr, ok := errors.AsType[*MyError](err) { ... } + +### `unsafefuncs` analyzer + + + +The `unsafefuncs` analyzer replaces arithmetic such as: + + unsafe.Pointer(uintptr(ptr) + uintptr(n)) + +where ptr is an unsafe.Pointer, by calls to `unsafe.Add`, added in Go 1.17. + + unsafe.Add(ptr, n) + ### Miscellaneous - The `minmax` modernizer now removes user-defined min/max functions when they are redundant. From 748477b5833b69b301843d15c9ed5b0e78f783cf Mon Sep 17 00:00:00 2001 From: cuishuang Date: Mon, 8 Dec 2025 12:32:44 +0800 Subject: [PATCH 58/60] all: fix function name mismatch in updateBasicLitPos comment Change-Id: I7735670b5f58cf661b5005ef6287ec696ebb3f9a Reviewed-on: https://go-review.googlesource.com/c/tools/+/727601 Auto-Submit: Alan Donovan Commit-Queue: Alan Donovan Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI Reviewed-by: Alan Donovan --- go/ast/astutil/imports.go | 2 +- internal/imports/sortimports.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go/ast/astutil/imports.go b/go/ast/astutil/imports.go index e2790ce742a..adb47110190 100644 --- a/go/ast/astutil/imports.go +++ b/go/ast/astutil/imports.go @@ -472,7 +472,7 @@ func Imports(fset *token.FileSet, f *ast.File) [][]*ast.ImportSpec { return groups } -// setBasicLitValuePos updates lit.Pos, +// updateBasicLitPos updates lit.Pos, // ensuring that lit.End (if set) is displaced by the same amount. // (See https://go.dev/issue/76395.) func updateBasicLitPos(lit *ast.BasicLit, pos token.Pos) { diff --git a/internal/imports/sortimports.go b/internal/imports/sortimports.go index a09d95fd0f6..f390be90f17 100644 --- a/internal/imports/sortimports.go +++ b/internal/imports/sortimports.go @@ -298,8 +298,8 @@ func (x byCommentPos) Len() int { return len(x) } func (x byCommentPos) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x byCommentPos) Less(i, j int) bool { return x[i].Pos() < x[j].Pos() } -// setBasicLitValuePos updates lit.Pos, -// ensuring that lit.End is displaced by the same amount. +// updateBasicLitPos updates lit.Pos, +// ensuring that lit.End (if set) is displaced by the same amount. // (See https://go.dev/issue/76395.) func updateBasicLitPos(lit *ast.BasicLit, pos token.Pos) { len := lit.End() - lit.Pos() @@ -307,7 +307,7 @@ func updateBasicLitPos(lit *ast.BasicLit, pos token.Pos) { // TODO(adonovan): after go1.26, simplify to: // lit.ValueEnd = pos + len v := reflect.ValueOf(lit).Elem().FieldByName("ValueEnd") - if v.IsValid() { + if v.IsValid() && v.Int() != 0 { v.SetInt(int64(pos + len)) } } From 36bb345678464722ad18fa4c7f4c0344c0acffb7 Mon Sep 17 00:00:00 2001 From: Gopher Robot Date: Mon, 8 Dec 2025 14:02:29 -0800 Subject: [PATCH 59/60] go.mod: update golang.org/x dependencies Update golang.org/x dependencies to their latest tagged versions. Change-Id: I5c2ff4c508ecf39d31e2eff37e6c20595ca04ea3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/728361 Reviewed-by: David Chase Auto-Submit: Gopher Robot LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- gopls/go.mod | 14 +++++++------- gopls/go.sum | 23 +++++++++++++++-------- 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index b4a4c9be59d..a4c14c0f6f0 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ go 1.24.0 require ( github.com/google/go-cmp v0.6.0 github.com/yuin/goldmark v1.4.13 - golang.org/x/mod v0.30.0 - golang.org/x/net v0.47.0 - golang.org/x/sync v0.18.0 - golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 + golang.org/x/mod v0.31.0 + golang.org/x/net v0.48.0 + golang.org/x/sync v0.19.0 + golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc ) -require golang.org/x/sys v0.38.0 // indirect +require golang.org/x/sys v0.39.0 // indirect diff --git a/go.sum b/go.sum index cf3fb66bede..eb0383e615e 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,13 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= diff --git a/gopls/go.mod b/gopls/go.mod index 35a60f7df7a..dc63e075574 100644 --- a/gopls/go.mod +++ b/gopls/go.mod @@ -8,11 +8,11 @@ require ( github.com/google/go-cmp v0.7.0 github.com/jba/templatecheck v0.7.1 github.com/modelcontextprotocol/go-sdk v0.8.0 - golang.org/x/mod v0.30.0 - golang.org/x/sync v0.18.0 - golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 - golang.org/x/text v0.31.0 - golang.org/x/tools v0.38.0 + golang.org/x/mod v0.31.0 + golang.org/x/sync v0.19.0 + golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc + golang.org/x/text v0.32.0 + golang.org/x/tools v0.39.0 golang.org/x/vuln v1.1.4 gopkg.in/yaml.v3 v3.0.1 honnef.co/go/tools v0.7.0-0.dev.0.20251022135355-8273271481d0 @@ -27,8 +27,8 @@ require ( github.com/google/jsonschema-go v0.3.0 // indirect github.com/google/safehtml v0.1.0 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20251125195548-87e1e737ad39 // indirect + golang.org/x/sys v0.39.0 // indirect golang.org/x/tools/go/expect v0.1.1-deprecated // indirect golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect diff --git a/gopls/go.sum b/gopls/go.sum index 99fd1b300d9..623deef1739 100644 --- a/gopls/go.sum +++ b/gopls/go.sum @@ -37,16 +37,18 @@ golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sU golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= -golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE= -golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp/typeparams v0.0.0-20251125195548-87e1e737ad39 h1:yzGKB4T4r1nFi65o7dQ96ERTfU2trk8Ige9aqqADqf4= +golang.org/x/exp/typeparams v0.0.0-20251125195548-87e1e737ad39/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= 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= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= 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= @@ -55,11 +57,13 @@ golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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= @@ -68,10 +72,11 @@ golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo= -golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= 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= @@ -80,6 +85,7 @@ golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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= @@ -88,8 +94,9 @@ golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= 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= From 00b22d96a3616723b0ee0341fb34c40b73e19c96 Mon Sep 17 00:00:00 2001 From: Ilya Ilyinykh Date: Sun, 7 Dec 2025 16:31:22 +0000 Subject: [PATCH 60/60] gopls/internal/golang: add support for variadic functions and constructors in generated tests Add IsVariadic flag to function metadata and expand variadic arguments with ... in test templates. Update constructor handling to propagate variadic information. Fixes golang/go#76682 Change-Id: I3cab68e8729fe9092dedd2b1a40a1cbce79fb41b GitHub-Last-Rev: acb706d0ca00af12405b5adecce5d2bcc0429af1 GitHub-Pull-Request: golang/tools#609 Reviewed-on: https://go-review.googlesource.com/c/tools/+/727280 Reviewed-by: David Chase Reviewed-by: Hongxiang Jiang Auto-Submit: Hongxiang Jiang LUCI-TryBot-Result: Go LUCI --- gopls/internal/golang/addtest.go | 36 +++- .../marker/testdata/codeaction/addtest.txt | 179 ++++++++++++++++++ 2 files changed, 210 insertions(+), 5 deletions(-) diff --git a/gopls/internal/golang/addtest.go b/gopls/internal/golang/addtest.go index c84a0f0be29..51664c58d98 100644 --- a/gopls/internal/golang/addtest.go +++ b/gopls/internal/golang/addtest.go @@ -91,6 +91,7 @@ func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) { {{- if ne $index 0}}, {{end}} {{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}} {{- end -}} + {{- if and .Receiver.Constructor.IsVariadic (not .Receiver.Constructor.IsUnusedVariadic) }}...{{- end -}} ) {{- /* Handles the error return from constructor. */}} @@ -123,6 +124,7 @@ func {{.TestFuncName}}(t *{{.TestingPackageName}}.T) { {{- if ne $index 0}}, {{end}} {{- if .Name}}tt.{{.Name}}{{else}}{{.Value}}{{end}} {{- end -}} + {{- if and .Func.IsVariadic (not .Func.IsUnusedVariadic) }}...{{- end -}} ) {{- /* Handles the returned error before the rest of return value. */}} @@ -167,6 +169,12 @@ type function struct { Name string Args []field Results []field + // IsVariadic holds information if the function is variadic. + // In case of the IsVariadic=true must expand it in the function-under-test call. + IsVariadic bool + // IsUnusedVariadic holds information if the function's variadic args is not used. + // In this case ... must be skipped. + IsUnusedVariadic bool } type receiver struct { @@ -238,7 +246,7 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. extraImports = make(map[string]string) // imports to add to test file ) - var collectImports = func(file *ast.File) (map[string]string, error) { + collectImports := func(file *ast.File) (map[string]string, error) { imps := make(map[string]string) for _, spec := range file.Imports { // TODO(hxjiang): support dot imports. @@ -478,7 +486,8 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. PackageName: qual(pkg.Types()), TestFuncName: testName, Func: function{ - Name: fn.Name(), + Name: fn.Name(), + IsVariadic: sig.Variadic(), }, } @@ -486,13 +495,22 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. 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 name == "" || name == "_" { + } 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 @@ -619,14 +637,22 @@ func AddTestForFunc(ctx context.Context, snapshot *cache.Snapshot, loc protocol. } if constructor != nil { - data.Receiver.Constructor = &function{Name: constructor.Name()} + data.Receiver.Constructor = &function{ + 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 name == "" || name == "_" { + } 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 diff --git a/gopls/internal/test/marker/testdata/codeaction/addtest.txt b/gopls/internal/test/marker/testdata/codeaction/addtest.txt index c084339fdb2..7e75cd17f9e 100644 --- a/gopls/internal/test/marker/testdata/codeaction/addtest.txt +++ b/gopls/internal/test/marker/testdata/codeaction/addtest.txt @@ -1540,3 +1540,182 @@ type Foo struct {} func NewFoo() func (*Foo) Method[T any]() {} // no suggested fix +-- varargsinput/varargsinput.go -- +package main + +func Function(args ...string) (out, out1, out2 string) {return args[0], "", ""} //@codeaction("Function", "source.addTest", edit=function_varargsinput) + +type Foo struct {} + +func NewFoo(args ...string) (*Foo, error) { + _ = args + return nil, nil +} + +func (*Foo) Method(args ...string) (out, out1, out2 string) {return args[0], "", ""} //@codeaction("Method", "source.addTest", edit=method_varargsinput) +-- varargsinput/varargsinput_test.go -- +package main_test +-- @function_varargsinput/varargsinput/varargsinput_test.go -- +@@ -2 +2,34 @@ ++ ++import ( ++ "testing" ++ ++ "golang.org/lsptests/addtest/varargsinput" ++) ++ ++func TestFunction(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ // Named input parameters for target function. ++ args []string ++ want string ++ want2 string ++ want3 string ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ got, got2, got3 := main.Function(tt.args...) ++ // TODO: update the condition below to compare got with tt.want. ++ if true { ++ t.Errorf("Function() = %v, want %v", got, tt.want) ++ } ++ if true { ++ t.Errorf("Function() = %v, want %v", got2, tt.want2) ++ } ++ if true { ++ t.Errorf("Function() = %v, want %v", got3, tt.want3) ++ } ++ }) ++ } ++} +-- @method_varargsinput/varargsinput/varargsinput_test.go -- +@@ -2 +2,40 @@ ++ ++import ( ++ "testing" ++ ++ "golang.org/lsptests/addtest/varargsinput" ++) ++ ++func TestFoo_Method(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ // Named input parameters for receiver constructor. ++ cargs []string ++ // Named input parameters for target function. ++ args []string ++ want string ++ want2 string ++ want3 string ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ f, err := main.NewFoo(tt.cargs...) ++ if err != nil { ++ t.Fatalf("could not construct receiver type: %v", err) ++ } ++ got, got2, got3 := f.Method(tt.args...) ++ // TODO: update the condition below to compare got with tt.want. ++ if true { ++ t.Errorf("Method() = %v, want %v", got, tt.want) ++ } ++ if true { ++ t.Errorf("Method() = %v, want %v", got2, tt.want2) ++ } ++ if true { ++ t.Errorf("Method() = %v, want %v", got3, tt.want3) ++ } ++ }) ++ } ++} +-- unusedvarargsinput/unusedvarargsinput.go -- +package main + +func Function(_ ...string) (out, out1, out2 string) {return "", "", ""} //@codeaction("Function", "source.addTest", edit=function_unusedvarargsinput) + +type Foo struct {} + +func NewFoo(_ ...string) (*Foo, error) { + return nil, nil +} + +func (*Foo) Method(_ ...string) (out, out1, out2 string) {return "", "", ""} //@codeaction("Method", "source.addTest", edit=method_unusedvarargsinput) +-- unusedvarargsinput/unusedvarargsinput_test.go -- +package main_test +-- @function_unusedvarargsinput/unusedvarargsinput/unusedvarargsinput_test.go -- +@@ -2 +2,32 @@ ++ ++import ( ++ "testing" ++ ++ "golang.org/lsptests/addtest/unusedvarargsinput" ++) ++ ++func TestFunction(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ want string ++ want2 string ++ want3 string ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ got, got2, got3 := main.Function() ++ // TODO: update the condition below to compare got with tt.want. ++ if true { ++ t.Errorf("Function() = %v, want %v", got, tt.want) ++ } ++ if true { ++ t.Errorf("Function() = %v, want %v", got2, tt.want2) ++ } ++ if true { ++ t.Errorf("Function() = %v, want %v", got3, tt.want3) ++ } ++ }) ++ } ++} +-- @method_unusedvarargsinput/unusedvarargsinput/unusedvarargsinput_test.go -- +@@ -2 +2,36 @@ ++ ++import ( ++ "testing" ++ ++ "golang.org/lsptests/addtest/unusedvarargsinput" ++) ++ ++func TestFoo_Method(t *testing.T) { ++ tests := []struct { ++ name string // description of this test case ++ want string ++ want2 string ++ want3 string ++ }{ ++ // TODO: Add test cases. ++ } ++ for _, tt := range tests { ++ t.Run(tt.name, func(t *testing.T) { ++ f, err := main.NewFoo() ++ if err != nil { ++ t.Fatalf("could not construct receiver type: %v", err) ++ } ++ got, got2, got3 := f.Method() ++ // TODO: update the condition below to compare got with tt.want. ++ if true { ++ t.Errorf("Method() = %v, want %v", got, tt.want) ++ } ++ if true { ++ t.Errorf("Method() = %v, want %v", got2, tt.want2) ++ } ++ if true { ++ t.Errorf("Method() = %v, want %v", got3, tt.want3) ++ } ++ }) ++ } ++}